import { namedNode, quad as createQuad, literal, defaultGraph, } from "@ldo/rdf-utils"; import type { CreateSuccess } from "../src/requester/results/success/CreateSuccess.js"; import { Buffer } from "buffer"; import { PostShShapeType } from "./.ldo/post.shapeTypes.js"; import type { ServerHttpError, UnauthenticatedHttpError, UnexpectedHttpError, } from "../src/requester/results/error/HttpErrorResult.js"; import type { NoncompliantPodError } from "../src/requester/results/error/NoncompliantPodError.js"; import type { GetStorageContainerFromWebIdSuccess } from "../src/requester/results/success/CheckRootContainerSuccess.js"; import { wait } from "./utils.helper.js"; import path from "path"; import type { GetWacRuleSuccess, UpdateResultError, WacRule, } from "../src/index.js"; import { createSolidLdoDataset, type SolidConnectedPlugin, type SolidContainer, type SolidContainerUri, type SolidLeaf, type SolidLeafUri, } from "../src/index.js"; import type { AggregateError, AggregateSuccess, IgnoredInvalidUpdateSuccess, InvalidUriError, UnexpectedResourceError, UpdateDefaultGraphSuccess, UpdateSuccess, ConnectedLdoDataset, } from "@ldo/connected"; import { changeData, commitData, ConnectedLdoTransactionDataset, } from "@ldo/connected"; import { getStorageFromWebId } from "../src/getStorageFromWebId.js"; import type { ResourceInfo } from "@ldo/test-solid-server"; import { createApp, setupServer } from "@ldo/test-solid-server"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; const ROOT_CONTAINER = "http://localhost:3001/"; const WEB_ID = "http://localhost:3001/example/profile/card#me"; const TEST_CONTAINER_SLUG = "test_ldo/"; const TEST_CONTAINER_URI = `${ROOT_CONTAINER}${TEST_CONTAINER_SLUG}` as SolidContainerUri; const SAMPLE_DATA_URI = `${TEST_CONTAINER_URI}sample.ttl` as SolidLeafUri; const SAMPLE2_DATA_SLUG = "sample2.ttl"; const SAMPLE2_DATA_URI = `${TEST_CONTAINER_URI}${SAMPLE2_DATA_SLUG}` as SolidLeafUri; const SAMPLE_BINARY_URI = `${TEST_CONTAINER_URI}sample.txt` as SolidLeafUri; const SAMPLE2_BINARY_SLUG = `sample2.txt`; const SAMPLE2_BINARY_URI = `${TEST_CONTAINER_URI}${SAMPLE2_BINARY_SLUG}` as SolidLeafUri; const SAMPLE_CONTAINER_URI = `${TEST_CONTAINER_URI}sample_container/` as SolidContainerUri; const SAMPLE_PROFILE_URI = `${TEST_CONTAINER_URI}profile.ttl` as SolidLeafUri; const SPIDER_MAN_TTL = `@base . @prefix rdf: . @prefix rdfs: . @prefix foaf: . @prefix rel: . <#green-goblin> rel:enemyOf <#spiderman> ; a foaf:Person ; # in the context of the Marvel universe foaf:name "Green Goblin" . <#spiderman> rel:enemyOf <#green-goblin> ; a foaf:Person ; foaf:name "Spiderman", "Человек-паук"@ru .`; const TEST_CONTAINER_TTL = `@prefix dc: . @prefix ldp: . @prefix posix: . @prefix xsd: . <> "sample.txt"; a ldp:Container, ldp:BasicContainer, ldp:Resource; dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. a ldp:Resource, ; dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. a ldp:Resource, ; dc:modified "2023-10-20T13:57:14.000Z"^^xsd:dateTime. <> posix:mtime 1697810234; ldp:contains , . posix:mtime 1697810234; posix:size 522. posix:mtime 1697810234; posix:size 10.`; const _TEST_CONTAINER_ACL_URI = `${TEST_CONTAINER_URI}.acl`; const TEST_CONTAINER_ACL = `<#b30e3fd1-b5a8-4763-ad9d-e95de9cf7933> a ; <${TEST_CONTAINER_URI}>; <${TEST_CONTAINER_URI}>; , , , ; <${WEB_ID}>; , .`; const SAMPLE_PROFILE_TTL = ` @prefix pim: . <${SAMPLE_PROFILE_URI}> pim:storage , . `; const resourceInfo: ResourceInfo = { slug: TEST_CONTAINER_SLUG, isContainer: true, contains: [ { slug: ".acl", isContainer: false, mimeType: "text/turtle", data: TEST_CONTAINER_ACL, }, { slug: "sample.ttl", isContainer: false, mimeType: "text/turtle", data: SPIDER_MAN_TTL, }, { slug: "sample.txt", isContainer: false, mimeType: "text/plain", data: "some text.", }, { slug: "profile.ttl", isContainer: false, mimeType: "text/turtle", data: SAMPLE_PROFILE_TTL, }, { slug: "sample_container/", isContainer: true, shouldNotInit: true, contains: [], }, { slug: SAMPLE2_DATA_SLUG, isContainer: false, shouldNotInit: true, mimeType: "text/turtle", data: "", }, { slug: SAMPLE2_BINARY_SLUG, isContainer: false, shouldNotInit: true, mimeType: "text/plain", data: "", }, ], }; async function testRequestLoads( request: () => Promise, loadingResource: SolidLeaf | SolidContainer, loadingValues: Partial<{ isLoading: boolean; isCreating: boolean; isReading: boolean; isUploading: boolean; isReloading: boolean; isDeleting: boolean; isUpdating: boolean; isDoingInitialFetch: boolean; }>, ): Promise { const allLoadingValues = { isLoading: false, isCreating: false, isReading: false, isUploading: false, isReloading: false, isDeleting: false, isUpdating: false, isDoingInitialFetch: false, ...loadingValues, }; const [returnVal] = await Promise.all([ request(), (async () => { Object.entries(allLoadingValues).forEach(([key, value]) => { if ( loadingResource.type === "SolidContainer" && (key === "isUploading" || key === "isUpdating") ) { return; } expect(loadingResource[key]()).toBe(value); }); })(), ]); return returnVal; } describe("Integration", () => { let solidLdoDataset: ConnectedLdoDataset; const s = setupServer(3001, resourceInfo); beforeEach(async () => { solidLdoDataset = createSolidLdoDataset(); solidLdoDataset.setContext("solid", { fetch: s.fetchMock }); }); /** * General */ describe("General", () => { it("Does not include the hash when creating a resource", () => { const resource = solidLdoDataset.getResource( "https://example.com/thing#hash", ); expect(resource.uri).toBe("https://example.com/thing"); }); }); /** * Read */ describe("read", () => { it("Reads a data leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.type).toBe("dataReadSuccess"); expect( solidLdoDataset.match( namedNode("http://example.org/#spiderman"), namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), namedNode("http://example.org/#green-goblin"), ).size, ).toBe(1); expect(resource.isBinary()).toBe(false); expect(resource.isDataResource()).toBe(true); expect(resource.isPresent()).toBe(true); }); // TODO: Possibly re-enable if Auto-read is required, but it might not be // it("Auto reads a resource", async () => { // const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI, { // autoLoad: true, // }); // // Wait until the resource is auto-loaded // await new Promise((resolve) => { // const interval = setInterval(() => { // if (!resource.isReading()) { // clearInterval(interval); // resolve(); // } // }, 250); // }); // expect( // solidLdoDataset.match( // namedNode("http://example.org/#spiderman"), // namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), // namedNode("http://example.org/#green-goblin"), // ).size, // ).toBe(1); // }); it("Reads a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.type).toBe("containerReadSuccess"); expect(resource.children().length).toBe(3); }); it("Reads a binary leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.type).toBe("binaryReadSuccess"); expect(resource.isBinary()).toBe(true); expect(await resource.getBlob()?.text()).toBe("some text."); }); it("Returns an absent result if the document doesn't exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.type).toBe("absentReadSuccess"); if (result.type !== "absentReadSuccess") return; expect(result.resource.isAbsent()).toBe(true); }); it("Returns an ServerError when an 500 error is returned", async () => { s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("Returns an Unauthorized error if a 403 error is returned", async () => { s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 403 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(true); expect(result.type).toBe("unauthorizedError"); }); it("Returns an UnauthenticatedError on an 401 error is returned", async () => { s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 401 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(true); expect(result.type).toBe("unauthenticatedError"); }); it("Returns an UnexpectedHttpError on a strange number error is returned", async () => { s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 399 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(true); expect(result.type).toBe("unexpectedHttpError"); }); it("Returns a NoncompliantPod error when no content type is returned", async () => { s.fetchMock.mockResolvedValueOnce( new Response(undefined, { status: 200, headers: {} }), ); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("noncompliantPodError"); expect(result.message).toMatch( /\Response from .* is not compliant with the Solid Specification: Resource requests must return a content-type header\./, ); }); it("Returns a NoncompliantPod error if invalid turtle is provided", async () => { s.fetchMock.mockResolvedValueOnce( new Response("Error", { status: 200, headers: new Headers({ "content-type": "text/turtle" }), }), ); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("noncompliantPodError"); expect(result.message).toMatch( /\Response from .* is not compliant with the Solid Specification: Request returned noncompliant turtle: Unexpected "Error" on line 1\./, ); }); it("Parses Turtle even when the content type contains parameters", async () => { s.fetchMock.mockResolvedValueOnce( new Response(SPIDER_MAN_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle;charset=utf-8" }), }), ); const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(false); if (result.isError) return; expect(result.type).toBe("dataReadSuccess"); }); it("Returns an UnexpectedResourceError if an unknown error is triggered", async () => { s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("unexpectedResourceError"); expect(result.message).toBe("Something happened."); }); it("Does not return an error if there is no link header for a container request", async () => { s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), }), ); const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }); expect(result.isError).toBe(false); if (result.isError) return; expect(result.resource.isRootContainer()).toBe(false); }); it("knows nothing about a leaf resource if it is not fetched", () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); expect(resource.isBinary()).toBe(undefined); expect(resource.isDataResource()).toBe(undefined); expect(resource.isUnfetched()).toBe(true); expect(resource.isPresent()).toBe(undefined); }); it("batches the read request when a read request is currently happening", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const [result, result1] = await Promise.all([ resource.read(), resource.read(), ]); expect(s.fetchMock).toHaveBeenCalledTimes(1); expect(result.type).toBe("dataReadSuccess"); expect(result1.type).toBe("dataReadSuccess"); }); it("batches the read request when a read request is in queue", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const [, result, result1] = await Promise.all([ resource.createAndOverwrite(), resource.read(), resource.read(), ]); expect(s.fetchMock).toHaveBeenCalledTimes(3); expect(result.type).toBe("dataReadSuccess"); expect(result1.type).toBe("dataReadSuccess"); }); }); /** * readIfUnfetched */ describe("readIfUnfetched", () => { it("reads an unfetched container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.readIfUnfetched(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }, ); expect(result.type).toBe("containerReadSuccess"); expect(resource.children().length).toBe(3); }); it("reads an unfetched leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const result = await testRequestLoads( () => resource.readIfUnfetched(), resource, { isLoading: true, isReading: true, isDoingInitialFetch: true, }, ); expect(result.type).toBe("dataReadSuccess"); expect( solidLdoDataset.match( namedNode("http://example.org/#spiderman"), namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), namedNode("http://example.org/#green-goblin"), ).size, ).toBe(1); }); it("returns a cached existing container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); await resource.read(); s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); expect(s.fetchMock).not.toHaveBeenCalled(); expect(result.type).toBe("containerReadSuccess"); expect(resource.children().length).toBe(3); }); it("returns a cached existing data leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); await resource.read(); s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); expect(result.type).toBe("dataReadSuccess"); expect( solidLdoDataset.match( namedNode("http://example.org/#spiderman"), namedNode("http://www.perceive.net/schemas/relationship/enemyOf"), namedNode("http://example.org/#green-goblin"), ).size, ).toBe(1); }); it("returns a cached existing binary leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); await resource.read(); s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); expect(result.type).toBe("binaryReadSuccess"); }); it("returns a cached absent container", async () => { const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI); await resource.read(); s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); expect(s.fetchMock).not.toHaveBeenCalled(); expect(result.type).toBe("absentReadSuccess"); }); it("returns a cached absent leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); await resource.read(); s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); expect(s.fetchMock).not.toHaveBeenCalled(); expect(result.type).toBe("absentReadSuccess"); }); }); /** * Get Root Container */ describe("rootContainer", () => { it("Finds the root container", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_BINARY_URI); const result = await resource.getRootContainer(); expect(result.type).toBe("SolidContainer"); if (result.type !== "SolidContainer") return; expect(result.uri).toBe(ROOT_CONTAINER); expect(result.isRootContainer()).toBe(true); }); it("Returns an error if there is no root container", async () => { s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), }), ); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), }), ); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), }), ); const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.getRootContainer(); expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("noRootContainerError"); expect(result.message).toMatch(/\.* has not root container\./); }); it("An error to be returned if a common http error is encountered", async () => { s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), ); const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.getRootContainer(); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("Returns an UnexpectedResourceError if an unknown error is triggered", async () => { s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.getRootContainer(); expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("unexpectedResourceError"); expect(result.message).toBe("Something happened."); }); it("returns a NonCompliantPodError when there is no root", async () => { s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle", link: '; rel="type"', }), }), ); const resource = solidLdoDataset.getResource(ROOT_CONTAINER); const result = await resource.getRootContainer(); expect(result.isError).toBe(true); expect(result.type).toBe("noRootContainerError"); }); }); /** * Get Storage From WebId */ describe("getStorageFromWebId", () => { it("Gets storage when a pim:storage field isn't present", async () => { const result = await getStorageFromWebId( SAMPLE_DATA_URI, solidLdoDataset, ); expect(result.type).toBe("getStorageContainerFromWebIdSuccess"); const realResult = result as GetStorageContainerFromWebIdSuccess; expect(realResult.storageContainers.length).toBe(1); expect(realResult.storageContainers[0].uri).toBe(ROOT_CONTAINER); }); it("Gets storage when a pim:storage field is present", async () => { const result = await getStorageFromWebId( SAMPLE_PROFILE_URI, solidLdoDataset, ); expect(result.type).toBe("getStorageContainerFromWebIdSuccess"); const realResult = result as GetStorageContainerFromWebIdSuccess; expect(realResult.storageContainers.length).toBe(2); expect(realResult.storageContainers[0].uri).toBe( "https://example.com/A/", ); expect(realResult.storageContainers[1].uri).toBe( "https://example.com/B/", ); }); it("Passes any errors returned from the read method", async () => { s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const result = await getStorageFromWebId( SAMPLE_DATA_URI, solidLdoDataset, ); expect(result.isError).toBe(true); }); it("Passes any errors returned from the getRootContainer method", async () => { s.fetchMock.mockResolvedValueOnce(new Response("")); s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const result = await getStorageFromWebId( SAMPLE_DATA_URI, solidLdoDataset, ); expect(result.isError).toBe(true); }); }); /** * =========================================================================== * Create * =========================================================================== */ describe("createAndOverwrite", () => { it("creates a document that doesn't exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.createAndOverwrite(), resource, { isLoading: true, isCreating: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_DATA_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE2_DATA_URI), ).toBe(true); }); it("creates a data resource that doesn't exist while overwriting", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.createAndOverwrite(), resource, { isLoading: true, isCreating: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(true); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE_DATA_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE_DATA_URI), ).toBe(true); }); it("creates a container", async () => { const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.createAndOverwrite(), resource, { isLoading: true, isCreating: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE_CONTAINER_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container .children() .some((child) => child.uri === SAMPLE_CONTAINER_URI), ).toBe(true); }); it("returns and error if creating a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), ); const result = await resource.createAndOverwrite(); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("returns a delete error if delete failed", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), ); const result = await resource.createAndOverwrite(); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("returns an error if the create fetch fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockImplementationOnce(async (...args) => { return s.authFetch(...args); }); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), ); const result = await resource.createAndOverwrite(); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("returns an unexpected error if some unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockImplementationOnce(async (...args) => { return s.authFetch(...args); }); s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some Unknown"); }); const result = await resource.createAndOverwrite(); expect(result.isError).toBe(true); expect(result.type).toBe("unexpectedResourceError"); }); it("batches the create request while waiting on another request", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const [, result1, result2] = await Promise.all([ resource.read(), resource.createAndOverwrite(), resource.createAndOverwrite(), ]); expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for read, 1 for delete in createAndOverwrite, 1 for create expect(s.fetchMock).toHaveBeenCalledTimes(3); }); it("batches the create request while waiting on a similar request", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const [result1, result2] = await Promise.all([ resource.createAndOverwrite(), resource.createAndOverwrite(), ]); expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for delete in createAndOverwrite, 1 for create expect(s.fetchMock).toHaveBeenCalledTimes(2); }); }); describe("createIfAbsent", () => { it("creates a data resource that doesn't exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.createIfAbsent(), resource, { isLoading: true, isCreating: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_DATA_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE2_DATA_URI), ).toBe(true); }); it("doesn't overwrite a resources that does exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.createIfAbsent(), resource, { isLoading: true, isCreating: true, }, ); expect(result.type).toBe("dataReadSuccess"); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE_DATA_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE_DATA_URI), ).toBe(true); }); it("creates a container that doesn't exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.createIfAbsent(), resource, { isLoading: true, isCreating: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE_CONTAINER_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container .children() .some((child) => child.uri === SAMPLE_CONTAINER_URI), ).toBe(true); }); it("returns an error if creating a container", async () => { const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI); s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE_CONTAINER_URI, { status: 500, }), ); const result = await resource.createIfAbsent(); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("returns an error if creating a leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE2_DATA_URI, { status: 500, }), ); const result = await resource.createIfAbsent(); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); }); /** * Delete */ describe("deleteResource", () => { it("returns an unexpected http error if an unexpected value is returned", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 214, }), ); const result = await resource.delete(); expect(result.isError).toBe(true); expect(result.type).toBe("unexpectedHttpError"); }); it("returns an unexpected resource error if an unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some unknwon"); }); const result = await resource.delete(); expect(result.isError).toBe(true); expect(result.type).toBe("unexpectedResourceError"); }); it("deletes a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.delete(); expect(result.type === "deleteSuccess"); }); it("returns an error on container read when deleting a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); s.fetchMock.mockImplementation(async (input, init) => { if ( (init?.method === "get" || !init?.method) && input === TEST_CONTAINER_URI ) { return new Response(SAMPLE_DATA_URI, { status: 500, }); } return s.authFetch(input, init); }); const result = await resource.delete(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< | ServerHttpError | UnexpectedHttpError | UnauthenticatedHttpError | UnexpectedResourceError | NoncompliantPodError >; expect(aggregateError.errors[0].type).toBe("serverError"); }); it("returns an error on child delete when deleting a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); s.fetchMock.mockImplementation(async (input, init) => { if (init?.method === "delete" && input === SAMPLE_DATA_URI) { return new Response(SAMPLE_DATA_URI, { status: 500, }); } return s.authFetch(input, init); }); const result = await resource.delete(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< | ServerHttpError | UnexpectedHttpError | UnauthenticatedHttpError | UnexpectedResourceError | NoncompliantPodError >; expect(aggregateError.errors[0].type).toBe("serverError"); }); it("returns an error on container delete when deleting a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); s.fetchMock.mockImplementation(async (input, init) => { if (init?.method === "delete" && input === TEST_CONTAINER_URI) { return new Response(SAMPLE_DATA_URI, { status: 500, }); } return s.authFetch(input, init); }); const result = await resource.delete(); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); }); /** * Update */ describe("updateDataResource", () => { const normanQuad = createQuad( namedNode("http://example.org/#green-goblin"), namedNode("http://xmlns.com/foaf/0.1/name"), literal("Norman Osborn"), namedNode(SAMPLE_DATA_URI), ); const goblinQuad = createQuad( namedNode("http://example.org/#green-goblin"), namedNode("http://xmlns.com/foaf/0.1/name"), literal("Green Goblin"), namedNode(SAMPLE_DATA_URI), ); it("applies changes to a Pod", async () => { const result = await testRequestLoads( () => { const transaction = solidLdoDataset.startTransaction(); transaction.add(normanQuad); transaction.delete(goblinQuad); return transaction.commitToRemote(); }, solidLdoDataset.getResource(SAMPLE_DATA_URI), { isLoading: true, isUpdating: true, }, ); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< UpdateSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type === "updateSuccess").toBe(true); expect(solidLdoDataset.has(normanQuad)).toBe(true); expect(solidLdoDataset.has(goblinQuad)).toBe(false); }); it("applies only remove changes to the Pod", async () => { const result = await testRequestLoads( () => { const transaction = solidLdoDataset.startTransaction(); transaction.delete(goblinQuad); return transaction.commitToRemote(); }, solidLdoDataset.getResource(SAMPLE_DATA_URI), { isLoading: true, isUpdating: true, }, ); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< UpdateSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type === "updateSuccess").toBe(true); expect(solidLdoDataset.has(goblinQuad)).toBe(false); }); it("handles an HTTP error", async () => { s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const transaction = solidLdoDataset.startTransaction(); transaction.add(normanQuad); transaction.delete(goblinQuad); const result = await transaction.commitToRemote(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< | UpdateResultError | InvalidUriError >; expect(aggregateError.errors.length).toBe(1); expect(aggregateError.errors[0].type).toBe("serverError"); }); it("handles an unknown request", async () => { s.fetchMock.mockImplementationOnce(() => { throw new Error("Some Error"); }); const transaction = solidLdoDataset.startTransaction(); transaction.add(normanQuad); transaction.delete(goblinQuad); const result = await transaction.commitToRemote(); expect(result.isError).toBe(true); expect(result.type).toBe("aggregateError"); const aggregateError = result as AggregateError< | UpdateResultError | InvalidUriError >; expect(aggregateError.errors.length).toBe(1); expect(aggregateError.errors[0].type).toBe("unexpectedResourceError"); }); it("ignores update when trying to update a container", async () => { const badContainerQuad = createQuad( namedNode("http://example.org/#green-goblin"), namedNode("http://xmlns.com/foaf/0.1/name"), literal("Norman Osborn"), namedNode(SAMPLE_CONTAINER_URI), ); const transaction = solidLdoDataset.startTransaction(); transaction.add(badContainerQuad); const result = await transaction.commitToRemote(); expect(result.isError).toBe(false); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< | UpdateSuccess | IgnoredInvalidUpdateSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type).toBe( "ignoredInvalidUpdateSuccess", ); }); it("writes to the default graph without fetching", async () => { const defaultGraphQuad = createQuad( namedNode("http://example.org/#green-goblin"), namedNode("http://xmlns.com/foaf/0.1/name"), literal("Norman Osborn"), defaultGraph(), ); const transaction = solidLdoDataset.startTransaction(); transaction.add(defaultGraphQuad); const result = await transaction.commitToRemote(); expect(result.type).toBe("aggregateSuccess"); const aggregateSuccess = result as AggregateSuccess< UpdateSuccess | UpdateDefaultGraphSuccess >; expect(aggregateSuccess.results.length).toBe(1); expect(aggregateSuccess.results[0].type).toBe( "updateDefaultGraphSuccess", ); expect( solidLdoDataset.has( createQuad( namedNode("http://example.org/#green-goblin"), namedNode("http://xmlns.com/foaf/0.1/name"), literal("Norman Osborn"), defaultGraph(), ), ), ).toBe(true); }); it("batches data update changes", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const transaction1 = solidLdoDataset.startTransaction(); transaction1.delete(goblinQuad); const transaction2 = solidLdoDataset.startTransaction(); transaction2.add(normanQuad); const [, updateResult1, updateResult2] = await Promise.all([ resource.read(), transaction1.commitToRemote(), transaction2.commitToRemote(), ]); expect(updateResult1.type).toBe("aggregateSuccess"); expect(updateResult2.type).toBe("aggregateSuccess"); expect(s.fetchMock).toHaveBeenCalledTimes(2); expect( solidLdoDataset.has( createQuad( namedNode("http://example.org/#green-goblin"), namedNode("http://xmlns.com/foaf/0.1/name"), literal("Norman Osborn"), namedNode(SAMPLE_DATA_URI), ), ), ).toBe(true); expect( solidLdoDataset.has( createQuad( namedNode("http://example.org/#green-goblin"), namedNode("http://xmlns.com/foaf/0.1/name"), literal("Green Goblin"), namedNode(SAMPLE_DATA_URI), ), ), ).toBe(false); }); }); it("allows a transaction on a transaction", () => { const transaction = solidLdoDataset.startTransaction(); const transaction2 = transaction.startTransaction(); expect(transaction2).toBeInstanceOf(ConnectedLdoTransactionDataset); }); /** * =========================================================================== * Upload * =========================================================================== */ describe("uploadAndOverwrite", () => { it("uploads a document that doesn't exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_BINARY_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.uploadAndOverwrite( Buffer.from("some text.") as unknown as Blob, "text/plain", ), resource, { isLoading: true, isUploading: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_BINARY_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE2_BINARY_URI), ).toBe(true); expect(resource.getMimeType()).toBe("text/plain"); expect(resource.isBinary()).toBe(true); expect(resource.isDataResource()).toBe(false); }); it("creates a binary resource that doesn't exist while overwriting", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.uploadAndOverwrite( Buffer.from("some text.") as unknown as Blob, "text/plain", ), resource, { isLoading: true, isUploading: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(true); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE_BINARY_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE_BINARY_URI), ).toBe(true); }); it("returns a delete error if delete failed", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), ); const result = await resource.uploadAndOverwrite( Buffer.from("some text.") as unknown as Blob, "text/plain", ); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("returns an error if the create fetch fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); s.fetchMock.mockImplementationOnce(async (...args) => { return s.authFetch(...args); }); s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), ); const result = await resource.uploadAndOverwrite( Buffer.from("some text.") as unknown as Blob, "text/plain", ); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); it("returns an unexpected error if some unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); s.fetchMock.mockImplementationOnce(async (...args) => { return s.authFetch(...args); }); s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some Unknown"); }); const result = await resource.uploadAndOverwrite( Buffer.from("some text.") as unknown as Blob, "text/plain", ); expect(result.isError).toBe(true); expect(result.type).toBe("unexpectedResourceError"); }); it("batches the upload request while waiting on another request", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const [, result1, result2] = await Promise.all([ resource.read(), resource.uploadAndOverwrite( Buffer.from("some text.") as unknown as Blob, "text/plain", ), resource.uploadAndOverwrite( Buffer.from("some text 2.") as unknown as Blob, "text/plain", ), ]); expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for read, 1 for delete in createAndOverwrite, 1 for create expect(s.fetchMock).toHaveBeenCalledTimes(3); expect(resource.getBlob()?.toString()).toBe("some text 2."); }); it("batches the upload request while waiting on a similar request", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const [result1, result2] = await Promise.all([ resource.uploadAndOverwrite( Buffer.from("some text.") as unknown as Blob, "text/plain", ), resource.uploadAndOverwrite( Buffer.from("some text 2.") as unknown as Blob, "text/plain", ), ]); expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for delete in createAndOverwrite, 1 for create expect(s.fetchMock).toHaveBeenCalledTimes(2); expect(resource.getBlob()?.toString()).toBe("some text 2."); }); }); describe("uploadIfAbsent", () => { it("creates a binary resource that doesn't exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_BINARY_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.uploadIfAbsent( Buffer.from("some text.") as unknown as Blob, "text/plain", ), resource, { isLoading: true, isUploading: true, }, ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_BINARY_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE2_BINARY_URI), ).toBe(true); }); it("doesn't overwrite a binary resource that does exist", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await testRequestLoads( () => resource.uploadIfAbsent( Buffer.from("some text.") as unknown as Blob, "text/plain", ), resource, { isLoading: true, isUploading: true, }, ); expect(result.type).toBe("binaryReadSuccess"); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE_BINARY_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( container.children().some((child) => child.uri === SAMPLE_BINARY_URI), ).toBe(true); }); it("returns an error if an error is encountered", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_BINARY_URI); s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE2_BINARY_URI, { status: 500, }), ); const result = await resource.uploadIfAbsent( Buffer.from("some text.") as unknown as Blob, "text/plain", ); expect(result.isError).toBe(true); expect(result.type).toBe("serverError"); }); }); /** * =========================================================================== * Methods * =========================================================================== */ describe("methods", () => { it("creates a data object for a specific subject", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const post = solidLdoDataset.createData( PostShShapeType, "https://example.com/subject", resource, ); post.type = { "@id": "CreativeWork" }; expect(post.type["@id"]).toBe("CreativeWork"); const result = await commitData(post); expect(result.type).toBe("aggregateSuccess"); expect( solidLdoDataset.has( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/CreativeWork"), namedNode(SAMPLE_DATA_URI), ), ), ).toBe(true); }); it("handles an error when committing data", async () => { s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE_DATA_URI, { status: 500, }), ); const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const post = solidLdoDataset.createData( PostShShapeType, "https://example.com/subject", resource, ); post.type = { "@id": "CreativeWork" }; expect(post.type["@id"]).toBe("CreativeWork"); const result = await commitData(post); expect(result.isError).toBe(true); }); it("uses changeData to start a transaction", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); solidLdoDataset.add( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/CreativeWork"), namedNode(SAMPLE_DATA_URI), ), ); const post = solidLdoDataset .usingType(PostShShapeType) .fromSubject("https://example.com/subject"); const cPost = changeData(post, resource); cPost.type = { "@id": "SocialMediaPosting" }; expect(cPost.type["@id"]).toBe("SocialMediaPosting"); const result = await commitData(cPost); expect(result.isError).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode("https://example.com/subject"), namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), namedNode("http://schema.org/SocialMediaPosting"), namedNode(SAMPLE_DATA_URI), ), ), ).toBe(true); }); }); /** * =========================================================================== * Container-Specific Methods * =========================================================================== */ describe("container specific", () => { it("returns the child with the child method", () => { const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const child = container.child(SAMPLE2_DATA_SLUG); expect(child.uri).toBe(SAMPLE2_DATA_URI); }); it("runs createAndOverwrite for a child via the createChildAndOverwrite method", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.createChildAndOverwrite(SAMPLE2_DATA_SLUG); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_DATA_URI); expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_DATA_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( resource.children().some((child) => child.uri === SAMPLE2_DATA_URI), ).toBe(true); }); it("runs createIfAbsent for a child via the createChildIfAbsent method", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.createChildIfAbsent(SAMPLE2_DATA_SLUG); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_DATA_URI); expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_DATA_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( resource.children().some((child) => child.uri === SAMPLE2_DATA_URI), ).toBe(true); }); it("runs uploadAndOverwrite for a child via the uploadChildAndOverwrite method", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.uploadChildAndOverwrite( SAMPLE2_BINARY_SLUG, Buffer.from("some text.") as unknown as Blob, "text/plain", ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_BINARY_URI); expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_BINARY_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( resource.children().some((child) => child.uri === SAMPLE2_BINARY_URI), ).toBe(true); }); it("runs uploadIfAbsent for a child via the uploadChildIfAbsent method", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.uploadChildIfAbsent( SAMPLE2_BINARY_SLUG, Buffer.from("some text.") as unknown as Blob, "text/plain", ); expect(result.type).toBe("createSuccess"); const createSuccess = result as CreateSuccess; expect(createSuccess.resource.uri).toBe(SAMPLE2_BINARY_URI); expect(createSuccess.didOverwrite).toBe(false); expect( solidLdoDataset.has( createQuad( namedNode(TEST_CONTAINER_URI), namedNode("http://www.w3.org/ns/ldp#contains"), namedNode(SAMPLE2_BINARY_URI), namedNode(TEST_CONTAINER_URI), ), ), ).toBe(true); expect( resource.children().some((child) => child.uri === SAMPLE2_BINARY_URI), ).toBe(true); }); }); /** * =========================================================================== * ACCESS CONTROL * =========================================================================== */ describe("getWacRule", () => { it("Fetches a wac rules for a container that has a corresponding acl", async () => { const container = solidLdoDataset.getResource(TEST_CONTAINER_URI); const wacResult = await container.getWac(); expect(wacResult.isError).toBe(false); const wacSuccess = wacResult as GetWacRuleSuccess< SolidLeaf | SolidContainer >; expect(wacSuccess.wacRule.public).toEqual({ read: true, write: true, append: true, control: true, }); expect(wacSuccess.wacRule.authenticated).toEqual({ read: true, write: true, append: true, control: true, }); expect(wacSuccess.wacRule.agent[WEB_ID]).toEqual({ read: true, write: true, append: true, control: true, }); }); it("Gets wac rules of a parent resource for a resource that does not have a corresponding acl", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(false); const wacSuccess = wacResult as GetWacRuleSuccess< SolidLeaf | SolidContainer >; expect(wacSuccess.wacRule.public).toEqual({ read: true, write: true, append: true, control: true, }); expect(wacSuccess.wacRule.authenticated).toEqual({ read: true, write: true, append: true, control: true, }); expect(wacSuccess.wacRule.agent[WEB_ID]).toEqual({ read: true, write: true, append: true, control: true, }); }); it("uses cached values for a retrieved resource", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); await resource.getWac(); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(false); const wacSuccess = wacResult as GetWacRuleSuccess< SolidLeaf | SolidContainer >; expect(wacSuccess.wacRule.public).toEqual({ read: true, write: true, append: true, control: true, }); expect(wacSuccess.wacRule.authenticated).toEqual({ read: true, write: true, append: true, control: true, }); expect(wacSuccess.wacRule.agent[WEB_ID]).toEqual({ read: true, write: true, append: true, control: true, }); }); it("returns an error when an error is encountered fetching the aclUri", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); }); it("returns an error when a document is not found", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("notFoundError"); }); it("returns a non-compliant error if a response is returned without a link header", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce( new Response("Error", { status: 200, }), ); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); expect( (wacResult as NoncompliantPodError).message, ).toBe( `Response from ${SAMPLE_DATA_URI} is not compliant with the Solid Specification: No link header present in request.`, ); }); it("returns a non-compliant error if a response is returned without an ACL link", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce( new Response("Error", { status: 200, headers: { link: `; rel="describedBy"` }, }), ); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); expect( (wacResult as NoncompliantPodError).message, ).toBe( `Response from ${SAMPLE_DATA_URI} is not compliant with the Solid Specification: There must be one link with a rel="acl"`, ); }); it("Returns an UnexpectedResourceError if an unknown error is triggered while getting the wac URI", async () => { s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const result = await resource.getWac(); expect(result.isError).toBe(true); if (!result.isError) return; expect(result.type).toBe("unexpectedResourceError"); expect(result.message).toBe("Something happened."); }); it("Returns an error if the request to get the ACL fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); }); it("Returns a non-compliant error if the root uri has no ACL", () => {}); it("Returns an error if the request to the ACL resource returns invalid turtle", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); s.fetchMock.mockResolvedValueOnce( new Response("BAD TURTLE", { status: 200 }), ); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); expect( (wacResult as NoncompliantPodError).message, ).toBe( `Response from http://localhost:3001/test_ldo/sample.ttl is not compliant with the Solid Specification: Request returned noncompliant turtle: Unexpected "BAD" on line 1.\nBAD TURTLE`, ); }); it("Returns an error if there was a problem getting the parent resource", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); s.fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); s.fetchMock.mockResolvedValueOnce(new Response("", { status: 500 })); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); }); it("returns a NonCompliantPodError when this is the root resource and it doesn't have an ACL", async () => { const resource = solidLdoDataset.getResource(ROOT_CONTAINER); s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); s.fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); expect( (wacResult as NoncompliantPodError).message, ).toBe( `Response from ${ROOT_CONTAINER} is not compliant with the Solid Specification: Resource "${ROOT_CONTAINER}" has no Effective ACL resource`, ); }); }); describe("setWacRule", () => { const newRules: WacRule = { public: { read: true, write: false, append: false, control: false }, authenticated: { read: true, write: false, append: true, control: false, }, agent: { [WEB_ID]: { read: true, write: true, append: true, control: true }, }, }; it("sets wac rules for a resource that didn't have one before", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const result = await resource.setWac(newRules); expect(result.isError).toBe(false); expect(result.type).toBe("setWacRuleSuccess"); const readResult = await resource.getWac({ ignoreCache: true }); expect(readResult.isError).toBe(false); expect(readResult.type).toBe("getWacRuleSuccess"); const rules = ( readResult as GetWacRuleSuccess ).wacRule; expect(rules).toEqual(newRules); }); it("overwrites an existing access control rule", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.setWac(newRules); expect(result.isError).toBe(false); expect(result.type).toBe("setWacRuleSuccess"); const readResult = await resource.getWac({ ignoreCache: true }); expect(readResult.isError).toBe(false); expect(readResult.type).toBe("getWacRuleSuccess"); const rules = ( readResult as GetWacRuleSuccess ).wacRule; expect(rules).toEqual(newRules); }); it("Does not write a rule when access is not granted to an agent", async () => { const moreRules = { ...newRules, public: { read: false, write: false, append: false, control: false }, }; const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const result = await resource.setWac(moreRules); expect(result.isError).toBe(false); expect(result.type).toBe("setWacRuleSuccess"); const readResult = await resource.getWac({ ignoreCache: true }); expect(readResult.isError).toBe(false); expect(readResult.type).toBe("getWacRuleSuccess"); const rules = ( readResult as GetWacRuleSuccess ).wacRule; expect(rules).toEqual(moreRules); }); it("returns an error when an error is encountered fetching the aclUri", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const wacResult = await resource.setWac(newRules); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); }); it("Returns an error when the request to write the access rules throws an error", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); s.fetchMock.mockResolvedValueOnce(new Response("", { status: 500 })); const wacResult = await resource.setWac(newRules); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); }); }); /** * =========================================================================== * NOTIFICATION SUBSCRIPTIONS * =========================================================================== */ describe("Notification Subscriptions", () => { const spidermanNode = namedNode("http://example.org/#spiderman"); const foafNameNode = namedNode("http://xmlns.com/foaf/0.1/name"); afterEach(async () => { await Promise.all( solidLdoDataset.getResources().map(async (resource) => { await resource.unsubscribeFromAllNotifications(); }), ); }); it("handles notification when a resource is updated", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); await resource.read(); const spidermanCallback = vi.fn(); solidLdoDataset.addListener( [spidermanNode, null, null, null], spidermanCallback, ); const subscriptionId = await resource.subscribeToNotifications(); expect(resource.isSubscribedToNotifications()).toBe(true); await s.authFetch(SAMPLE_DATA_URI, { method: "PATCH", body: 'INSERT DATA { "Peter Parker" . }', headers: { "Content-Type": "application/sparql-update", }, }); await wait(1000); expect( solidLdoDataset.match( spidermanNode, foafNameNode, literal("Peter Parker"), ).size, ).toBe(1); expect(spidermanCallback).toHaveBeenCalledTimes(1); // Notification is not propogated after unsubscribe spidermanCallback.mockClear(); await resource.unsubscribeFromNotifications(subscriptionId); expect(resource.isSubscribedToNotifications()).toBe(false); await s.authFetch(SAMPLE_DATA_URI, { method: "PATCH", body: 'INSERT DATA { "Miles Morales" . }', headers: { "Content-Type": "application/sparql-update", }, }); await wait(50); expect(spidermanCallback).not.toHaveBeenCalled(); expect( solidLdoDataset.match( spidermanNode, foafNameNode, literal("Miles Morales"), ).size, ).toBe(0); }); it("handles notification when subscribed to a child that is deleted", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const testContainer = solidLdoDataset.getResource(TEST_CONTAINER_URI); await resource.read(); const spidermanCallback = vi.fn(); solidLdoDataset.addListener( [spidermanNode, null, null, null], spidermanCallback, ); const containerCallback = vi.fn(); solidLdoDataset.addListener( [namedNode(TEST_CONTAINER_URI), null, null, null], containerCallback, ); await resource.subscribeToNotifications(); await s.authFetch(SAMPLE_DATA_URI, { method: "DELETE", }); await wait(1000); expect(solidLdoDataset.match(spidermanNode, null, null).size).toBe(0); expect( testContainer.children().some((child) => child.uri === SAMPLE_DATA_URI), ).toBe(false); expect(spidermanCallback).toHaveBeenCalledTimes(1); expect(containerCallback).toHaveBeenCalledTimes(1); await resource.unsubscribeFromAllNotifications(); }); it("handles notification when subscribed to a parent with a deleted child", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const testContainer = solidLdoDataset.getResource(TEST_CONTAINER_URI); await resource.read(); const spidermanCallback = vi.fn(); solidLdoDataset.addListener( [spidermanNode, null, null, null], spidermanCallback, ); const containerCallback = vi.fn(); solidLdoDataset.addListener( [namedNode(TEST_CONTAINER_URI), null, null, null], containerCallback, ); await testContainer.subscribeToNotifications(); await s.authFetch(SAMPLE_DATA_URI, { method: "DELETE", }); await wait(1000); expect(solidLdoDataset.match(spidermanNode, null, null).size).toBe(0); expect( testContainer.children().some((child) => child.uri === SAMPLE_DATA_URI), ).toBe(false); expect(spidermanCallback).toHaveBeenCalledTimes(1); expect(containerCallback).toHaveBeenCalledTimes(1); await testContainer.unsubscribeFromAllNotifications(); }); it("handles notification when subscribed to a parent with an added child", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const testContainer = solidLdoDataset.getResource(TEST_CONTAINER_URI); await resource.read(); const spidermanCallback = vi.fn(); solidLdoDataset.addListener( [spidermanNode, null, null, null], spidermanCallback, ); const containerCallback = vi.fn(); solidLdoDataset.addListener( [namedNode(TEST_CONTAINER_URI), null, null, null], containerCallback, ); await testContainer.subscribeToNotifications(); await s.authFetch(TEST_CONTAINER_URI, { method: "POST", headers: { "content-type": "text/turtle", slug: "sample2.ttl" }, body: SPIDER_MAN_TTL, }); await wait(1000); expect(solidLdoDataset.match(spidermanNode, null, null).size).toBe(4); expect( testContainer .children() .some((child) => child.uri === SAMPLE2_DATA_URI), ).toBe(true); expect(spidermanCallback).toHaveBeenCalledTimes(1); expect(containerCallback).toHaveBeenCalledTimes(1); await testContainer.unsubscribeFromAllNotifications(); }); it.skip("returns an error when it cannot subscribe to a notification", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const onError = vi.fn(); await s.app.stop(); await resource.subscribeToNotifications({ onNotificationError: onError, }); expect(onError).toHaveBeenCalledTimes(2); await s.app.start(); }); it.skip("returns an error when the server doesnt support websockets", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const onError = vi.fn(); await s.app.stop(); const disabledWebsocketsApp = await createApp( 3001, path.join(__dirname, "./configs/server-config-without-websocket.json"), ); await disabledWebsocketsApp.start(); await resource.subscribeToNotifications({ onNotificationError: onError }); expect(onError).toHaveBeenCalledTimes(2); await disabledWebsocketsApp.stop(); await s.app.start(); }); it.skip("attempts to reconnect multiple times before giving up.", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const onError = vi.fn(); await s.app.stop(); const disabledWebsocketsApp = await createApp( 3001, path.join(__dirname, "./configs/server-config-without-websocket.json"), ); await disabledWebsocketsApp.start(); await resource.subscribeToNotifications({ onNotificationError: onError }); // TODO: This is a bad test because of the wait. Instead inject better // numbers into the websocket class. await wait(35000); expect(onError).toHaveBeenCalledTimes(14); expect(onError.mock.calls[1][0].type).toBe( "disconnectedAttemptingReconnectError", ); expect(onError.mock.calls[13][0].type).toBe( "disconnectedNotAttemptingReconnectError", ); await disabledWebsocketsApp.stop(); await s.app.start(); }); it("causes no problems when unsubscribing when not subscribed", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); await resource.unsubscribeFromAllNotifications(); expect(resource.isSubscribedToNotifications()).toBe(false); }); }); });