UNPKG

@inrupt/solid-client

Version:

Make your web apps work with Solid Pods.

710 lines (621 loc) • 21.3 kB
// Copyright Inrupt Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the // Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import { describe, it, expect } from "@jest/globals"; import * as fc from "fast-check"; import { DataFactory } from "n3"; import { isNamedNode, isLiteral, isLocalNode, asNamedNode, resolveIriForLocalNode, resolveLocalIri, serializeBoolean, deserializeBoolean, serializeDatetime, deserializeDatetime, serializeDecimal, deserializeDecimal, serializeInteger, deserializeInteger, normalizeLocale, ValidUrlExpectedError, serializeDate, deserializeDate, deserializeTime, serializeTime, } from "./datatypes"; import type { LocalNode } from "./interfaces"; import { localNodeSkolemPrefix } from "./rdf.internal"; describe("stress-testing serialisations", () => { it("should always return the input value when serializing, then deserializing a boolean", () => { const runs = 100; expect.assertions(runs + 2); const fcResult = fc.check( fc.property(fc.boolean(), (inputBoolean) => { expect(deserializeBoolean(serializeBoolean(inputBoolean))).toBe( inputBoolean, ); }), { numRuns: runs }, ); expect(fcResult.counterexample).toBeNull(); expect(fcResult.failed).toBe(false); }); it("should always return the input value when serializing, then deserializing a datetime", () => { const runs = 100; expect.assertions(runs + 2); const fcResult = fc.check( fc.property(fc.date({ noInvalidDate: true }), (inputDatetime) => { expect( deserializeDatetime(serializeDatetime(inputDatetime))?.getTime(), ).toBe(inputDatetime.getTime()); }), { numRuns: runs }, ); expect(fcResult.counterexample).toBeNull(); expect(fcResult.failed).toBe(false); }); it("should always return the input value when serializing -0, then deserializing to 0", () => { expect(deserializeDecimal(serializeDecimal(-0))).toBe(0); }); it("should always return the input value when serializing, then deserializing a decimal", () => { const runs = 100; expect.assertions(runs + 2); const fcResult = fc.check( fc.property(fc.float({ noNaN: true }), (inputDecimal) => { expect(deserializeDecimal(serializeDecimal(inputDecimal))).toBe( inputDecimal === 0 ? 0 : inputDecimal, ); }), { numRuns: runs }, ); expect(fcResult.counterexample).toBeNull(); expect(fcResult.failed).toBe(false); }); it("should always return the input value when serializing, then deserializing a integer", () => { const runs = 100; expect.assertions(runs + 2); const fcResult = fc.check( fc.property(fc.integer(), (inputInteger) => { expect(deserializeInteger(serializeInteger(inputInteger))).toBe( inputInteger, ); }), { numRuns: runs }, ); expect(fcResult.counterexample).toBeNull(); expect(fcResult.failed).toBe(false); }); }); describe("serializeBoolean", () => { it("serializes true as `'true'`", () => { expect(serializeBoolean(true)).toBe("true"); }); it("serializes false as `'false'`", () => { expect(serializeBoolean(false)).toBe("false"); }); }); describe("deserializeBoolean", () => { it("parses `1` as true", () => { expect(deserializeBoolean("1")).toBe(true); }); it("parses `0` as false", () => { expect(deserializeBoolean("0")).toBe(false); }); it("parses `true` as true", () => { expect(deserializeBoolean("true")).toBe(true); }); it("parses `false` as false", () => { expect(deserializeBoolean("false")).toBe(false); }); it("returns null if a value is not a serialised boolean", () => { expect(deserializeBoolean("")).toBeNull(); expect(deserializeBoolean("Not a serialised boolean")).toBeNull(); }); }); describe("serializeDatetime", () => { it("properly serialises a given datetime", () => { expect( serializeDatetime(new Date(Date.UTC(1990, 10, 12, 13, 37, 42, 0))), ).toBe("1990-11-12T13:37:42.000Z"); expect( serializeDatetime(new Date(Date.UTC(1990, 10, 12, 13, 37, 42, 42))), ).toBe("1990-11-12T13:37:42.042Z"); }); }); describe("deserializeDatetime", () => { it("properly parses a serialised datetime", () => { const expectedDate = new Date(Date.UTC(1990, 10, 12, 13, 37, 42, 0)); expect(deserializeDatetime("1990-11-12T13:37:42.000Z")).toEqual( expectedDate, ); const expectedDateWithNegativeYear = new Date( Date.UTC(-42, 10, 12, 13, 37, 42, 0), ); expect(deserializeDatetime("-0042-11-12T13:37:42.000Z")).toEqual( expectedDateWithNegativeYear, ); const expectedDateWithHour24 = new Date(Date.UTC(1990, 10, 13, 0, 0, 0, 0)); expect(deserializeDatetime("1990-11-12T24:00:00Z")).toEqual( expectedDateWithHour24, ); const expectedDateWithFractionalSeconds = new Date( Date.UTC(1990, 10, 12, 13, 37, 42, 42), ); expect(deserializeDatetime("1990-11-12T13:37:42.42Z")).toEqual( expectedDateWithFractionalSeconds, ); const expectedDateWithPositive0Timezone = new Date( Date.UTC(1990, 10, 12, 10, 0, 0, 0), ); expect(deserializeDatetime("1990-11-12T10:00:00+00:00")).toEqual( expectedDateWithPositive0Timezone, ); const expectedDateWithNegative0Timezone = new Date( Date.UTC(1990, 10, 12, 10, 0, 0, 0), ); expect(deserializeDatetime("1990-11-12T10:00:00-00:00")).toEqual( expectedDateWithNegative0Timezone, ); const expectedDateWithNegativeTimezone = new Date( Date.UTC(1990, 10, 12, 8, 30, 0, 0), ); expect(deserializeDatetime("1990-11-12T10:00:00-01:30")).toEqual( expectedDateWithNegativeTimezone, ); const expectedDateWithMaxNegativeTimezone = new Date( Date.UTC(1990, 10, 11, 20, 0, 0, 0), ); expect(deserializeDatetime("1990-11-12T10:00:00-14:00")).toEqual( expectedDateWithMaxNegativeTimezone, ); const expectedDateWithPositiveTimezone = new Date( Date.UTC(1990, 10, 12, 11, 30, 0, 0), ); expect(deserializeDatetime("1990-11-12T10:00:00+01:30")).toEqual( expectedDateWithPositiveTimezone, ); const expectedDateWithMaxPositiveTimezone = new Date( Date.UTC(1990, 10, 13, 0, 0, 0, 0), ); expect(deserializeDatetime("1990-11-12T10:00:00+14:00")).toEqual( expectedDateWithMaxPositiveTimezone, ); const expectedDateWithoutTimezone = new Date( Date.UTC(1990, 10, 12, 13, 37, 42, 0), ); expect(deserializeDatetime("1990-11-12T13:37:42.000")).toEqual( expectedDateWithoutTimezone, ); const dateBeforeTheYear100 = new Date(-59042995200000); expect(deserializeDatetime("0099-01-01T00:00:00.000Z")).toEqual( dateBeforeTheYear100, ); const expectedEarliestRepresentableDate = new Date(-8640000000000000); expect(deserializeDatetime("-271821-04-20T00:00:00.000Z")).toEqual( expectedEarliestRepresentableDate, ); // Same date, earlier timezone expect(deserializeDatetime("-271821-04-20T01:00:00.000-01:00")).toEqual( expectedEarliestRepresentableDate, ); // Same date, later timezone expect(deserializeDatetime("-271821-04-19T23:00:00.000+01:00")).toEqual( expectedEarliestRepresentableDate, ); const expectedLatestRepresentableDate = new Date(8640000000000000); expect(deserializeDatetime("275760-09-13T00:00:00.000Z")).toEqual( expectedLatestRepresentableDate, ); // Same date, earlier timezone expect(deserializeDatetime("275760-09-13T01:00:00.000-01:00")).toEqual( expectedLatestRepresentableDate, ); // Same date, later timezone expect(deserializeDatetime("275760-09-12T23:00:00.000+01:00")).toEqual( expectedLatestRepresentableDate, ); }); it("returns null if a value is not a serialised datetime", () => { expect(deserializeDatetime("1990-11-12")).toBeNull(); expect(deserializeDatetime("Not a serialised datetime")).toBeNull(); }); }); describe("serializeDate", () => { it("properly serialises a given date", () => { expect(serializeDate(new Date(Date.UTC(1990, 10, 12)))).toBe("1990-11-12Z"); expect(serializeDate(new Date(Date.UTC(1990, 1, 3)))).toBe("1990-02-03Z"); }); }); describe("deserializeDate", () => { it("properly parses a serialised date", () => { const expectedDate = new Date(Date.UTC(1990, 1, 3, 12)); expect(deserializeDate("1990-02-03Z")).toEqual(expectedDate); const expectedDateWithNegativeYear = new Date(Date.UTC(-42, 10, 12, 12)); expect(deserializeDate("-0042-11-12Z")).toEqual( expectedDateWithNegativeYear, ); const dateBeforeTheYear100 = new Date(Date.UTC(0, 0, 1, 12)); dateBeforeTheYear100.setUTCFullYear(99); expect(deserializeDate("0099-01-01Z")).toEqual(dateBeforeTheYear100); const expectedEarliestRepresentableDate = new Date(Date.UTC(0, 3, 20, 12)); expectedEarliestRepresentableDate.setUTCFullYear(-271821); expect(deserializeDate("-271821-04-20Z")).toEqual( expectedEarliestRepresentableDate, ); // find largest date and set time to 12 const expectedLatestRepresentableDate = new Date(Date.UTC(0, 11, 31, 12)); expectedLatestRepresentableDate.setUTCFullYear(275759); expect(deserializeDate("275759-12-31Z")).toEqual( expectedLatestRepresentableDate, ); }); it("returns null if a value is not a serialised date", () => { expect(deserializeDate("Not a serialised date")).toBeNull(); }); }); describe("serializeTime", () => { it("properly serialises a given time", () => { expect( serializeTime({ hour: 2, minute: 37, second: 5, }), ).toBe("02:37:05"); expect( serializeTime({ hour: 2, minute: 37, second: 5, timezoneHourOffset: 2, }), ).toBe("02:37:05+02:00"); expect( serializeTime({ hour: 2, minute: 37, second: 5, timezoneHourOffset: 10, }), ).toBe("02:37:05+10:00"); expect( serializeTime({ hour: 2, minute: 37, second: 5, timezoneHourOffset: -2, }), ).toBe("02:37:05-02:00"); expect( serializeTime({ hour: 13, minute: 1, second: 42, millisecond: 42, }), ).toBe("13:01:42.042"); expect( serializeTime({ hour: 13, minute: 1, second: 42, millisecond: 0, }), ).toBe("13:01:42"); expect( serializeTime({ hour: 13, minute: 1, second: 42, millisecond: 9, }), ).toBe("13:01:42.009"); expect( serializeTime({ hour: 13, minute: 1, second: 42, timezoneHourOffset: 5, timezoneMinuteOffset: 30, }), ).toBe("13:01:42+05:30"); expect( serializeTime({ hour: 13, minute: 1, second: 42, timezoneHourOffset: 0, timezoneMinuteOffset: 5, }), ).toBe("13:01:42+00:05"); }); }); describe("deserializeTime", () => { it("properly parses a serialised time", () => { const expectedTime = { hour: 13, minute: 37, second: 42, }; expect(deserializeTime("13:37:42")).toStrictEqual(expectedTime); const expectedTimeWithAll = { hour: 13, minute: 37, second: 42, millisecond: 20, timezoneHourOffset: 2, timezoneMinuteOffset: 30, }; expect(deserializeTime("13:37:42.020+02:30")).toStrictEqual( expectedTimeWithAll, ); const expectedTimeWithAllNoMinutes = { hour: 13, minute: 37, second: 42, millisecond: 20, timezoneHourOffset: 2, timezoneMinuteOffset: 0, }; expect(deserializeTime("13:37:42.020+02:00")).toStrictEqual( expectedTimeWithAllNoMinutes, ); const expectedTimeWithHour24 = { hour: 0, minute: 0, second: 0, }; expect(deserializeTime("00:00:00")).toStrictEqual(expectedTimeWithHour24); const expectedTimeWithFractionalSeconds = { hour: 13, minute: 37, second: 42, millisecond: 42, }; expect(deserializeTime("13:37:42.42")).toStrictEqual( expectedTimeWithFractionalSeconds, ); const expectedTimeWithPositive0Timezone = { hour: 10, minute: 0, second: 0, timezoneHourOffset: 0, timezoneMinuteOffset: 0, }; expect(deserializeTime("10:00:00+00:00")).toStrictEqual( expectedTimeWithPositive0Timezone, ); const expectedTimeWithNegative0Timezone = { hour: 10, minute: 0, second: 0, timezoneHourOffset: -0, timezoneMinuteOffset: -0, }; expect(deserializeTime("10:00:00-00:00")).toStrictEqual( expectedTimeWithNegative0Timezone, ); const expectedTimeWithNegativeTimezone = { hour: 10, minute: 0, second: 0, timezoneHourOffset: -1, timezoneMinuteOffset: -30, }; expect(deserializeTime("10:00:00-01:30")).toStrictEqual( expectedTimeWithNegativeTimezone, ); const expectedTimeWithMaxNegativeTimezone = { hour: 10, minute: 0, second: 0, timezoneHourOffset: -14, timezoneMinuteOffset: -0, }; expect(deserializeTime("10:00:00-14:00")).toStrictEqual( expectedTimeWithMaxNegativeTimezone, ); const expectedTimeWithPositiveTimezone = { hour: 10, minute: 0, second: 0, timezoneHourOffset: 1, timezoneMinuteOffset: 30, }; expect(deserializeTime("10:00:00+01:30")).toStrictEqual( expectedTimeWithPositiveTimezone, ); const expectedTimeWithMaxPositiveTimezone = { hour: 10, minute: 0, second: 0, timezoneHourOffset: 14, timezoneMinuteOffset: 0, }; expect(deserializeTime("10:00:00+14:00")).toStrictEqual( expectedTimeWithMaxPositiveTimezone, ); const expectedTimeWithMinuteOverload = { hour: 10, minute: 30, second: 0, timezoneHourOffset: 0, timezoneMinuteOffset: 35, }; expect(deserializeTime("10:30:00+00:35")).toStrictEqual( expectedTimeWithMinuteOverload, ); const expectedTimeWithMinuteGreaterThanSixty = { hour: 11, minute: 30, second: 0, }; expect(deserializeTime("10:90:00")).toStrictEqual( expectedTimeWithMinuteGreaterThanSixty, ); expect(deserializeTime("10:00:00+10:60")).toBeNull(); }); it("returns null if a value is not a serialised time", () => { expect(deserializeTime("1990-11-12")).toBeNull(); expect(deserializeTime("Not a serialised datetime")).toBeNull(); }); }); describe("serializeDecimal", () => { it("properly serialises a given decimal", () => { expect(serializeDecimal(13.37)).toBe("13.37"); expect(serializeDecimal(-13.37)).toBe("-13.37"); expect(serializeDecimal(0.1337)).toBe("0.1337"); // https://www.w3.org/TR/xmlschema-2/#decimal-lexical-representation // > If the fractional part is zero, the period and following zero(es) can be omitted. expect(serializeDecimal(1337.0)).toBe("1337"); }); }); describe("deserializeDecimal", () => { it("properly parses a serialised decimal", () => { expect(deserializeDecimal("13.37")).toBe(13.37); expect(deserializeDecimal("+13.37")).toBe(13.37); expect(deserializeDecimal("-13.37")).toBe(-13.37); expect(deserializeDecimal("0.1337")).toBe(0.1337); expect(deserializeDecimal("1337")).toBe(1337); }); it("return null if a value is not a serialised decimal", () => { expect(deserializeDecimal("Not a serialised decimal")).toBeNull(); }); }); describe("serializeInteger", () => { it("properly serialises a given integer", () => { expect(serializeInteger(42)).toBe("42"); expect(serializeInteger(-42)).toBe("-42"); expect(serializeInteger(0)).toBe("0"); }); }); describe("deserializeInteger", () => { it("properly parses a serialised integer", () => { expect(deserializeInteger("42")).toBe(42); expect(deserializeInteger("-42")).toBe(-42); expect(deserializeInteger("+42")).toBe(42); expect(deserializeInteger("0")).toBe(0); }); it("return null if a value is not a serialised integer", () => { expect(deserializeInteger("Not a serialised integer")).toBeNull(); }); }); describe("normalizeLocale", () => { // The RDF/JS spec mandates lowercase locales: // https://rdf.js.org/data-model-spec/#dom-literal-language it("lowercases a given locale", () => { expect(normalizeLocale("EN-GB")).toBe("en-gb"); expect(normalizeLocale("nl-NL")).toBe("nl-nl"); }); }); describe("isNamedNode", () => { it("recognises a NamedNode", () => { expect( isNamedNode(DataFactory.namedNode("https://arbitrary.pod/resource#node")), ).toBe(true); }); it("recognises non-NamedNodes", () => { expect(isNamedNode(DataFactory.blankNode())).toBe(false); expect(isNamedNode(DataFactory.literal("Arbitrary value"))).toBe(false); expect(isNamedNode(DataFactory.variable("Arbitrary name"))).toBe(false); expect(isNamedNode("Arbitrary string")).toBe(false); }); }); describe("isLiteral", () => { it("recognises a Literal", () => { expect(isLiteral(DataFactory.literal("Arbitrary value"))).toBe(true); }); it("recognises non-Literals", () => { expect(isLiteral(DataFactory.blankNode())).toBe(false); expect( isLiteral(DataFactory.namedNode("https://arbitrary.pod/resource#node")), ).toBe(false); expect(isLiteral(DataFactory.variable("Arbitrary name"))).toBe(false); expect(isLiteral("Arbitrary string")).toBe(false); }); }); describe("isLocalNode", () => { it("recognises a LocalNode", () => { expect( isLocalNode(DataFactory.namedNode(`${localNodeSkolemPrefix}localNode`)), ).toBe(true); }); it("recognises non-LocalNodes", () => { expect(isLocalNode(DataFactory.blankNode())).toBe(false); expect( isLocalNode(DataFactory.namedNode("https://arbitrary.pod/resource#node")), ).toBe(false); expect(isLocalNode(DataFactory.literal("Arbitrary value"))).toBe(false); expect(isLocalNode(DataFactory.variable("Arbitrary name"))).toBe(false); expect(isLocalNode("Arbitrary string")).toBe(false); }); }); describe("asNamedNode", () => { it("constructs a proper NamedNode from an IRI", () => { const namedNode = asNamedNode("https://some.pod/resource#node"); expect(namedNode.termType).toBe("NamedNode"); expect(namedNode.value).toBe("https://some.pod/resource#node"); }); it("preserves an existing NamedNode", () => { const originalNode = DataFactory.namedNode( "https://some.pod/resource#node", ); const newNode = asNamedNode(originalNode); expect(newNode).toStrictEqual(originalNode); }); it("throws an error on invalid IRIs", () => { expect(() => asNamedNode("Not an IRI")).toThrow("Not an IRI"); }); }); describe("resolveIriForLocalNode", () => { it("properly resolves the IRI for a LocalNode", () => { const localNode = DataFactory.namedNode( `${localNodeSkolemPrefix}some-name`, ) as LocalNode; expect( resolveIriForLocalNode(localNode, "https://some.pod/resource").value, ).toBe("https://some.pod/resource#some-name"); }); }); describe("resolveLocalIri", () => { it("properly resolves the IRI for a given name and resource IRI", () => { expect(resolveLocalIri("some-name", "https://some.pod/resource")).toBe( "https://some.pod/resource#some-name", ); }); }); describe("ValidUrlExpectedError", () => { it("logs the invalid property in its error message", () => { const error = new ValidUrlExpectedError(null); expect(error.message).toBe("Expected a valid URL, but received: [null]."); }); it("logs the value of an invalid URL inside a Named Node in its error message", () => { const error = new ValidUrlExpectedError(DataFactory.namedNode("not-a-url")); expect(error.message).toBe( "Expected a valid URL, but received: [not-a-url].", ); }); it("exposes the invalid property", () => { const error = new ValidUrlExpectedError({ not: "a-url" }); expect(error.receivedValue).toEqual({ not: "a-url" }); }); });