From a78a604a186fce72fe84d3d9c0e42fce77313285 Mon Sep 17 00:00:00 2001 From: Jackson Morgan Date: Fri, 2 May 2025 15:52:15 -0400 Subject: [PATCH] Refactored tests to use the test-solid-server library --- packages/connected-solid/jest.config.js | 1 - packages/connected-solid/package.json | 3 +- .../connected-solid/test/Integration.test.ts | 330 ++++++++---------- .../connected-solid/test/authFetch.helper.ts | 112 ------ .../test/configs/server-config.json | 43 --- .../test/configs/solid-css-seed.json | 9 - packages/connected-solid/test/setup-tests.ts | 3 - .../test/solidServer.helper.ts | 42 --- packages/test-solid-server/package.json | 5 +- packages/test-solid-server/src/index.ts | 1 + .../test-solid-server/src/resourceUtils.ts | 7 +- .../test-solid-server/src/setupTestServer.ts | 14 +- 12 files changed, 164 insertions(+), 406 deletions(-) delete mode 100644 packages/connected-solid/test/authFetch.helper.ts delete mode 100644 packages/connected-solid/test/configs/server-config.json delete mode 100644 packages/connected-solid/test/configs/solid-css-seed.json delete mode 100644 packages/connected-solid/test/setup-tests.ts delete mode 100644 packages/connected-solid/test/solidServer.helper.ts diff --git a/packages/connected-solid/jest.config.js b/packages/connected-solid/jest.config.js index c55a5f7..9bfe763 100644 --- a/packages/connected-solid/jest.config.js +++ b/packages/connected-solid/jest.config.js @@ -3,7 +3,6 @@ const sharedConfig = require("../../jest.config.js"); module.exports = { ...sharedConfig, rootDir: "./", - setupFiles: ["/test/setup-tests.ts"], transform: { "^.+\\.(ts|tsx)?$": "ts-jest", "^.+\\.(js|jsx)$": "babel-jest", diff --git a/packages/connected-solid/package.json b/packages/connected-solid/package.json index d485103..2c8fe20 100644 --- a/packages/connected-solid/package.json +++ b/packages/connected-solid/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@inrupt/solid-client-authn-core": "^2.2.6", "@ldo/cli": "^1.0.0-alpha.3", + "@ldo/test-solid-server": "^1.0.0-alpha.8", "@rdfjs/data-model": "^1.2.0", "@rdfjs/types": "^1.0.1", "@solid-notifications/types": "^0.1.2", @@ -53,4 +54,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/connected-solid/test/Integration.test.ts b/packages/connected-solid/test/Integration.test.ts index 20146d2..004e934 100644 --- a/packages/connected-solid/test/Integration.test.ts +++ b/packages/connected-solid/test/Integration.test.ts @@ -1,5 +1,3 @@ -import type { App } from "@solid/community-server"; -import { ROOT_CONTAINER, WEB_ID, createApp } from "./solidServer.helper"; import { namedNode, quad as createQuad, @@ -16,9 +14,7 @@ import type { } from "../src/requester/results/error/HttpErrorResult"; import type { NoncompliantPodError } from "../src/requester/results/error/NoncompliantPodError"; import type { GetStorageContainerFromWebIdSuccess } from "../src/requester/results/success/CheckRootContainerSuccess"; -import { generateAuthFetch } from "./authFetch.helper"; import { wait } from "./utils.helper"; -import fs from "fs/promises"; import path from "path"; import type { GetWacRuleSuccess, UpdateResultError, WacRule } from "../src"; import { @@ -45,7 +41,11 @@ import { ConnectedLdoTransactionDataset, } from "@ldo/connected"; import { getStorageFromWebId } from "../src/getStorageFromWebId"; +import type { ResourceInfo } from "@ldo/test-solid-server"; +import { createApp, setupServer } from "@ldo/test-solid-server"; +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; @@ -93,7 +93,7 @@ const TEST_CONTAINER_TTL = `@prefix dc: . posix:size 522. posix:mtime 1697810234; posix:size 10.`; -const TEST_CONTAINER_ACL_URI = `${TEST_CONTAINER_URI}.acl`; +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}>; @@ -106,6 +106,57 @@ const SAMPLE_PROFILE_TTL = ` <${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, @@ -149,98 +200,13 @@ async function testRequestLoads( } describe("Integration", () => { - let app: App; - let authFetch: typeof fetch; - let fetchMock: jest.Mock< - Promise, - [input: RequestInfo | URL, init?: RequestInit | undefined] - >; let solidLdoDataset: ConnectedLdoDataset; - let previousJestId: string | undefined; - let previousNodeEnv: string | undefined; - beforeAll(async () => { - // Remove Jest ID so that community solid server doesn't use the Jest Import - previousJestId = process.env.JEST_WORKER_ID; - previousNodeEnv = process.env.NODE_ENV; - delete process.env.JEST_WORKER_ID; - process.env.NODE_ENV = "other_test"; - // Start up the server - app = await createApp(); - await app.start(); - authFetch = await generateAuthFetch(); - }); - - afterAll(async () => { - app.stop(); - process.env.JEST_WORKER_ID = previousJestId; - process.env.NODE_ENV = previousNodeEnv; - const testDataPath = path.join(__dirname, "./data"); - await fs.rm(testDataPath, { recursive: true, force: true }); - }); + const s = setupServer(3001, resourceInfo); beforeEach(async () => { - fetchMock = jest.fn(authFetch); solidLdoDataset = createSolidLdoDataset(); - solidLdoDataset.setContext("solid", { fetch: fetchMock }); - // Create a new document called sample.ttl - await authFetch(ROOT_CONTAINER, { - method: "POST", - headers: { - link: '; rel="type"', - slug: TEST_CONTAINER_SLUG, - }, - }); - await authFetch(TEST_CONTAINER_ACL_URI, { - method: "PUT", - headers: { - "content-type": "text/turtle", - }, - body: TEST_CONTAINER_ACL, - }); - await Promise.all([ - authFetch(TEST_CONTAINER_URI, { - method: "POST", - headers: { "content-type": "text/turtle", slug: "sample.ttl" }, - body: SPIDER_MAN_TTL, - }), - authFetch(TEST_CONTAINER_URI, { - method: "POST", - headers: { "content-type": "text/plain", slug: "sample.txt" }, - body: "some text.", - }), - authFetch(TEST_CONTAINER_URI, { - method: "POST", - headers: { "content-type": "text/turtle", slug: "profile.ttl" }, - body: SAMPLE_PROFILE_TTL, - }), - ]); - }); - - afterEach(async () => { - await Promise.all([ - authFetch(SAMPLE_DATA_URI, { - method: "DELETE", - }), - authFetch(SAMPLE2_DATA_URI, { - method: "DELETE", - }), - authFetch(SAMPLE_BINARY_URI, { - method: "DELETE", - }), - authFetch(SAMPLE2_BINARY_URI, { - method: "DELETE", - }), - authFetch(SAMPLE_PROFILE_URI, { - method: "DELETE", - }), - authFetch(SAMPLE_CONTAINER_URI, { - method: "DELETE", - }), - ]); - await authFetch(TEST_CONTAINER_URI, { - method: "DELETE", - }); + solidLdoDataset.setContext("solid", { fetch: s.fetchMock }); }); /** @@ -338,7 +304,7 @@ describe("Integration", () => { }); it("Returns an ServerError when an 500 error is returned", async () => { - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, @@ -350,7 +316,7 @@ describe("Integration", () => { }); it("Returns an Unauthorized error if a 403 error is returned", async () => { - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 403 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 403 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, @@ -362,7 +328,7 @@ describe("Integration", () => { }); it("Returns an UnauthenticatedError on an 401 error is returned", async () => { - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 401 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 401 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, @@ -374,7 +340,7 @@ describe("Integration", () => { }); it("Returns an UnexpectedHttpError on a strange number error is returned", async () => { - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 399 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 399 })); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, @@ -386,7 +352,7 @@ describe("Integration", () => { }); it("Returns a NoncompliantPod error when no content type is returned", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(undefined, { status: 200, headers: {} }), ); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); @@ -404,7 +370,7 @@ describe("Integration", () => { }); it("Returns a NoncompliantPod error if invalid turtle is provided", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("Error", { status: 200, headers: new Headers({ "content-type": "text/turtle" }), @@ -425,7 +391,7 @@ describe("Integration", () => { }); it("Parses Turtle even when the content type contains parameters", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(SPIDER_MAN_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle;charset=utf-8" }), @@ -443,7 +409,7 @@ describe("Integration", () => { }); it("Returns an UnexpectedResourceError if an unknown error is triggered", async () => { - fetchMock.mockRejectedValueOnce(new Error("Something happened.")); + s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); const result = await testRequestLoads(() => resource.read(), resource, { isLoading: true, @@ -457,7 +423,7 @@ describe("Integration", () => { }); it("Does not return an error if there is no link header for a container request", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), @@ -489,7 +455,7 @@ describe("Integration", () => { resource.read(), ]); - expect(fetchMock).toHaveBeenCalledTimes(1); + expect(s.fetchMock).toHaveBeenCalledTimes(1); expect(result.type).toBe("dataReadSuccess"); expect(result1.type).toBe("dataReadSuccess"); }); @@ -502,7 +468,7 @@ describe("Integration", () => { resource.read(), ]); - expect(fetchMock).toHaveBeenCalledTimes(3); + expect(s.fetchMock).toHaveBeenCalledTimes(3); expect(result.type).toBe("dataReadSuccess"); expect(result1.type).toBe("dataReadSuccess"); }); @@ -551,9 +517,9 @@ describe("Integration", () => { it("returns a cached existing container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); await resource.read(); - fetchMock.mockClear(); + s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); - expect(fetchMock).not.toHaveBeenCalled(); + expect(s.fetchMock).not.toHaveBeenCalled(); expect(result.type).toBe("containerReadSuccess"); expect(resource.children().length).toBe(3); }); @@ -561,7 +527,7 @@ describe("Integration", () => { it("returns a cached existing data leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); await resource.read(); - fetchMock.mockClear(); + s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); expect(result.type).toBe("dataReadSuccess"); expect( @@ -576,7 +542,7 @@ describe("Integration", () => { it("returns a cached existing binary leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); await resource.read(); - fetchMock.mockClear(); + s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); expect(result.type).toBe("binaryReadSuccess"); }); @@ -584,18 +550,18 @@ describe("Integration", () => { it("returns a cached absent container", async () => { const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI); await resource.read(); - fetchMock.mockClear(); + s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); - expect(fetchMock).not.toHaveBeenCalled(); + 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(); - fetchMock.mockClear(); + s.fetchMock.mockClear(); const result = await resource.readIfUnfetched(); - expect(fetchMock).not.toHaveBeenCalled(); + expect(s.fetchMock).not.toHaveBeenCalled(); expect(result.type).toBe("absentReadSuccess"); }); }); @@ -614,19 +580,19 @@ describe("Integration", () => { }); it("Returns an error if there is no root container", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), }), ); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), }), ); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ "content-type": "text/turtle" }), @@ -641,7 +607,7 @@ describe("Integration", () => { }); it("An error to be returned if a common http error is encountered", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), @@ -653,7 +619,7 @@ describe("Integration", () => { }); it("Returns an UnexpectedResourceError if an unknown error is triggered", async () => { - fetchMock.mockRejectedValueOnce(new Error("Something happened.")); + s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); const result = await resource.getRootContainer(); expect(result.isError).toBe(true); @@ -663,7 +629,7 @@ describe("Integration", () => { }); it("returns a NonCompliantPodError when there is no root", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 200, headers: new Headers({ @@ -711,7 +677,7 @@ describe("Integration", () => { }); it("Passes any errors returned from the read method", async () => { - fetchMock.mockRejectedValueOnce(new Error("Something happened.")); + s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const result = await getStorageFromWebId( SAMPLE_DATA_URI, solidLdoDataset, @@ -720,8 +686,8 @@ describe("Integration", () => { }); it("Passes any errors returned from the getRootContainer method", async () => { - fetchMock.mockResolvedValueOnce(new Response("")); - fetchMock.mockRejectedValueOnce(new Error("Something happened.")); + s.fetchMock.mockResolvedValueOnce(new Response("")); + s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const result = await getStorageFromWebId( SAMPLE_DATA_URI, solidLdoDataset, @@ -828,7 +794,7 @@ describe("Integration", () => { it("returns and error if creating a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), @@ -840,7 +806,7 @@ describe("Integration", () => { it("returns a delete error if delete failed", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), @@ -852,10 +818,10 @@ describe("Integration", () => { it("returns an error if the create fetch fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockImplementationOnce(async (...args) => { - return authFetch(...args); + s.fetchMock.mockImplementationOnce(async (...args) => { + return s.authFetch(...args); }); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), @@ -867,10 +833,10 @@ describe("Integration", () => { it("returns an unexpected error if some unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockImplementationOnce(async (...args) => { - return authFetch(...args); + s.fetchMock.mockImplementationOnce(async (...args) => { + return s.authFetch(...args); }); - fetchMock.mockImplementationOnce(async () => { + s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some Unknown"); }); const result = await resource.createAndOverwrite(); @@ -889,7 +855,7 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for read, 1 for delete in createAndOverwrite, 1 for create - expect(fetchMock).toHaveBeenCalledTimes(3); + expect(s.fetchMock).toHaveBeenCalledTimes(3); }); it("batches the create request while waiting on a similar request", async () => { @@ -902,7 +868,7 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for delete in createAndOverwrite, 1 for create - expect(fetchMock).toHaveBeenCalledTimes(2); + expect(s.fetchMock).toHaveBeenCalledTimes(2); }); }); @@ -999,7 +965,7 @@ describe("Integration", () => { it("returns an error if creating a container", async () => { const resource = solidLdoDataset.getResource(SAMPLE_CONTAINER_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE_CONTAINER_URI, { status: 500, }), @@ -1011,7 +977,7 @@ describe("Integration", () => { it("returns an error if creating a leaf", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_DATA_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE2_DATA_URI, { status: 500, }), @@ -1028,7 +994,7 @@ describe("Integration", () => { describe("deleteResource", () => { it("returns an unexpected http error if an unexpected value is returned", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 214, }), @@ -1040,7 +1006,7 @@ describe("Integration", () => { it("returns an unexpected resource error if an unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockImplementationOnce(async () => { + s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some unknwon"); }); const result = await resource.delete(); @@ -1056,7 +1022,7 @@ describe("Integration", () => { it("returns an error on container read when deleting a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); - fetchMock.mockImplementation(async (input, init) => { + s.fetchMock.mockImplementation(async (input, init) => { if ( (init?.method === "get" || !init?.method) && input === TEST_CONTAINER_URI @@ -1065,7 +1031,7 @@ describe("Integration", () => { status: 500, }); } - return authFetch(input, init); + return s.authFetch(input, init); }); const result = await resource.delete(); expect(result.isError).toBe(true); @@ -1082,13 +1048,13 @@ describe("Integration", () => { it("returns an error on child delete when deleting a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); - fetchMock.mockImplementation(async (input, init) => { + s.fetchMock.mockImplementation(async (input, init) => { if (init?.method === "delete" && input === SAMPLE_DATA_URI) { return new Response(SAMPLE_DATA_URI, { status: 500, }); } - return authFetch(input, init); + return s.authFetch(input, init); }); const result = await resource.delete(); expect(result.isError).toBe(true); @@ -1105,13 +1071,13 @@ describe("Integration", () => { it("returns an error on container delete when deleting a container", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); - fetchMock.mockImplementation(async (input, init) => { + s.fetchMock.mockImplementation(async (input, init) => { if (init?.method === "delete" && input === TEST_CONTAINER_URI) { return new Response(SAMPLE_DATA_URI, { status: 500, }); } - return authFetch(input, init); + return s.authFetch(input, init); }); const result = await resource.delete(); expect(result.isError).toBe(true); @@ -1184,7 +1150,7 @@ describe("Integration", () => { }); it("handles an HTTP error", async () => { - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const transaction = solidLdoDataset.startTransaction(); transaction.add(normanQuad); @@ -1202,7 +1168,7 @@ describe("Integration", () => { }); it("handles an unknown request", async () => { - fetchMock.mockImplementationOnce(() => { + s.fetchMock.mockImplementationOnce(() => { throw new Error("Some Error"); }); const transaction = solidLdoDataset.startTransaction(); @@ -1286,7 +1252,7 @@ describe("Integration", () => { ]); expect(updateResult1.type).toBe("aggregateSuccess"); expect(updateResult2.type).toBe("aggregateSuccess"); - expect(fetchMock).toHaveBeenCalledTimes(2); + expect(s.fetchMock).toHaveBeenCalledTimes(2); expect( solidLdoDataset.has( createQuad( @@ -1394,7 +1360,7 @@ describe("Integration", () => { it("returns a delete error if delete failed", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), @@ -1409,10 +1375,10 @@ describe("Integration", () => { it("returns an error if the create fetch fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); - fetchMock.mockImplementationOnce(async (...args) => { - return authFetch(...args); + s.fetchMock.mockImplementationOnce(async (...args) => { + return s.authFetch(...args); }); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(TEST_CONTAINER_TTL, { status: 500, }), @@ -1427,10 +1393,10 @@ describe("Integration", () => { it("returns an unexpected error if some unknown error is triggered", async () => { const resource = solidLdoDataset.getResource(SAMPLE_BINARY_URI); - fetchMock.mockImplementationOnce(async (...args) => { - return authFetch(...args); + s.fetchMock.mockImplementationOnce(async (...args) => { + return s.authFetch(...args); }); - fetchMock.mockImplementationOnce(async () => { + s.fetchMock.mockImplementationOnce(async () => { throw new Error("Some Unknown"); }); const result = await resource.uploadAndOverwrite( @@ -1458,7 +1424,7 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for read, 1 for delete in createAndOverwrite, 1 for create - expect(fetchMock).toHaveBeenCalledTimes(3); + expect(s.fetchMock).toHaveBeenCalledTimes(3); expect(resource.getBlob()?.toString()).toBe("some text 2."); }); @@ -1478,7 +1444,7 @@ describe("Integration", () => { expect(result1.type).toBe("createSuccess"); expect(result2.type).toBe("createSuccess"); // 1 for delete in createAndOverwrite, 1 for create - expect(fetchMock).toHaveBeenCalledTimes(2); + expect(s.fetchMock).toHaveBeenCalledTimes(2); expect(resource.getBlob()?.toString()).toBe("some text 2."); }); }); @@ -1552,7 +1518,7 @@ describe("Integration", () => { it("returns an error if an error is encountered", async () => { const resource = solidLdoDataset.getResource(SAMPLE2_BINARY_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE2_BINARY_URI, { status: 500, }), @@ -1596,7 +1562,7 @@ describe("Integration", () => { }); it("handles an error when committing data", async () => { - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response(SAMPLE_DATA_URI, { status: 500, }), @@ -1847,7 +1813,7 @@ describe("Integration", () => { it("returns an error when an error is encountered fetching the aclUri", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); @@ -1862,7 +1828,7 @@ describe("Integration", () => { it("returns a non-compliant error if a response is returned without a link header", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("Error", { status: 200, }), @@ -1879,7 +1845,7 @@ describe("Integration", () => { it("returns a non-compliant error if a response is returned without an ACL link", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("Error", { status: 200, headers: { link: `; rel="describedBy"` }, @@ -1896,7 +1862,7 @@ describe("Integration", () => { }); it("Returns an UnexpectedResourceError if an unknown error is triggered while getting the wac URI", async () => { - fetchMock.mockRejectedValueOnce(new Error("Something happened.")); + s.fetchMock.mockRejectedValueOnce(new Error("Something happened.")); const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const result = await resource.getWac(); expect(result.isError).toBe(true); @@ -1907,13 +1873,13 @@ describe("Integration", () => { it("Returns an error if the request to get the ACL fails", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); @@ -1923,13 +1889,13 @@ describe("Integration", () => { it("Returns an error if the request to the ACL resource returns invalid turtle", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("BAD TURTLE", { status: 200 }), ); const wacResult = await resource.getWac(); @@ -1944,14 +1910,14 @@ describe("Integration", () => { it("Returns an error if there was a problem getting the parent resource", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); - fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); - fetchMock.mockResolvedValueOnce(new Response("", { status: 500 })); + 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"); @@ -1959,13 +1925,13 @@ describe("Integration", () => { it("returns a NonCompliantPodError when this is the root resource and it doesn't have an ACL", async () => { const resource = solidLdoDataset.getResource(ROOT_CONTAINER); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); - fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); + s.fetchMock.mockResolvedValueOnce(new Response("", { status: 404 })); const wacResult = await resource.getWac(); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("noncompliantPodError"); @@ -2039,7 +2005,7 @@ describe("Integration", () => { it("returns an error when an error is encountered fetching the aclUri", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); - fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); + s.fetchMock.mockResolvedValueOnce(new Response("Error", { status: 500 })); const wacResult = await resource.setWac(newRules); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); @@ -2047,13 +2013,13 @@ describe("Integration", () => { it("Returns an error when the request to write the access rules throws an error", async () => { const resource = solidLdoDataset.getResource(TEST_CONTAINER_URI); - fetchMock.mockResolvedValueOnce( + s.fetchMock.mockResolvedValueOnce( new Response("", { status: 200, headers: { link: `; rel="acl"` }, }), ); - fetchMock.mockResolvedValueOnce(new Response("", { status: 500 })); + s.fetchMock.mockResolvedValueOnce(new Response("", { status: 500 })); const wacResult = await resource.setWac(newRules); expect(wacResult.isError).toBe(true); expect(wacResult.type).toBe("serverError"); @@ -2083,7 +2049,7 @@ describe("Integration", () => { expect(resource.isSubscribedToNotifications()).toBe(true); - await authFetch(SAMPLE_DATA_URI, { + await s.authFetch(SAMPLE_DATA_URI, { method: "PATCH", body: 'INSERT DATA { "Peter Parker" . }', headers: { @@ -2105,7 +2071,7 @@ describe("Integration", () => { spidermanCallback.mockClear(); await resource.unsubscribeFromNotifications(subscriptionId); expect(resource.isSubscribedToNotifications()).toBe(false); - await authFetch(SAMPLE_DATA_URI, { + await s.authFetch(SAMPLE_DATA_URI, { method: "PATCH", body: 'INSERT DATA { "Miles Morales" . }', headers: { @@ -2143,7 +2109,7 @@ describe("Integration", () => { await resource.subscribeToNotifications(); - await authFetch(SAMPLE_DATA_URI, { + await s.authFetch(SAMPLE_DATA_URI, { method: "DELETE", }); await wait(1000); @@ -2177,7 +2143,7 @@ describe("Integration", () => { await testContainer.subscribeToNotifications(); - await authFetch(SAMPLE_DATA_URI, { + await s.authFetch(SAMPLE_DATA_URI, { method: "DELETE", }); await wait(1000); @@ -2211,7 +2177,7 @@ describe("Integration", () => { await testContainer.subscribeToNotifications(); - await authFetch(TEST_CONTAINER_URI, { + await s.authFetch(TEST_CONTAINER_URI, { method: "POST", headers: { "content-type": "text/turtle", slug: "sample2.ttl" }, body: SPIDER_MAN_TTL, @@ -2234,18 +2200,19 @@ describe("Integration", () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const onError = jest.fn(); - await app.stop(); + await s.app.stop(); await resource.subscribeToNotifications({ onNotificationError: onError }); expect(onError).toHaveBeenCalledTimes(2); - await app.start(); + await s.app.start(); }); it("returns an error when the server doesnt support websockets", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const onError = jest.fn(); - await app.stop(); + await s.app.stop(); const disabledWebsocketsApp = await createApp( + 3001, path.join(__dirname, "./configs/server-config-without-websocket.json"), ); await disabledWebsocketsApp.start(); @@ -2254,15 +2221,16 @@ describe("Integration", () => { expect(onError).toHaveBeenCalledTimes(2); await disabledWebsocketsApp.stop(); - await app.start(); + await s.app.start(); }); it("attempts to reconnect multiple times before giving up.", async () => { const resource = solidLdoDataset.getResource(SAMPLE_DATA_URI); const onError = jest.fn(); - await app.stop(); + await s.app.stop(); const disabledWebsocketsApp = await createApp( + 3001, path.join(__dirname, "./configs/server-config-without-websocket.json"), ); await disabledWebsocketsApp.start(); @@ -2282,7 +2250,7 @@ describe("Integration", () => { ); await disabledWebsocketsApp.stop(); - await app.start(); + await s.app.start(); }); it("causes no problems when unsubscribing when not subscribed", async () => { diff --git a/packages/connected-solid/test/authFetch.helper.ts b/packages/connected-solid/test/authFetch.helper.ts deleted file mode 100644 index fffee6a..0000000 --- a/packages/connected-solid/test/authFetch.helper.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { KeyPair } from "@inrupt/solid-client-authn-core"; -import { - buildAuthenticatedFetch, - createDpopHeader, - generateDpopKeyPair, -} from "@inrupt/solid-client-authn-core"; -import fetch from "cross-fetch"; - -const config = { - podName: process.env.USER_NAME || "example", - email: process.env.EMAIL || "hello@example.com", - password: process.env.PASSWORD || "abc123", -}; - -async function getAuthorization(): Promise { - // First we request the account API controls to find out where we can log in - const indexResponse = await fetch("http://localhost:3001/.account/"); - const { controls } = await indexResponse.json(); - - // And then we log in to the account API - const response = await fetch(controls.password.login, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - email: config.email, - password: config.password, - }), - }); - // This authorization value will be used to authenticate in the next step - const result = await response.json(); - return result.authorization; -} - -async function getSecret( - authorization: string, -): Promise<{ id: string; secret: string; resource: string }> { - // Now that we are logged in, we need to request the updated controls from the server. - // These will now have more values than in the previous example. - const indexResponse = await fetch("http://localhost:3001/.account/", { - headers: { authorization: `CSS-Account-Token ${authorization}` }, - }); - const { controls } = await indexResponse.json(); - - // Here we request the server to generate a token on our account - const response = await fetch(controls.account.clientCredentials, { - method: "POST", - headers: { - authorization: `CSS-Account-Token ${authorization}`, - "content-type": "application/json", - }, - // The name field will be used when generating the ID of your token. - // The WebID field determines which WebID you will identify as when using the token. - // Only WebIDs linked to your account can be used. - body: JSON.stringify({ - name: "my-token", - webId: `http://localhost:3001/${config.podName}/profile/card#me`, - }), - }); - - // These are the identifier and secret of your token. - // Store the secret somewhere safe as there is no way to request it again from the server! - // The `resource` value can be used to delete the token at a later point in time. - const response2 = await response.json(); - return response2; -} - -async function getAccessToken( - id: string, - secret: string, -): Promise<{ accessToken: string; dpopKey: KeyPair }> { - try { - // A key pair is needed for encryption. - // This function from `solid-client-authn` generates such a pair for you. - const dpopKey = await generateDpopKeyPair(); - - // These are the ID and secret generated in the previous step. - // Both the ID and the secret need to be form-encoded. - const authString = `${encodeURIComponent(id)}:${encodeURIComponent( - secret, - )}`; - // This URL can be found by looking at the "token_endpoint" field at - // http://localhost:3001/.well-known/openid-configuration - // if your server is hosted at http://localhost:3000/. - const tokenUrl = "http://localhost:3001/.oidc/token"; - const response = await fetch(tokenUrl, { - method: "POST", - headers: { - // The header needs to be in base64 encoding. - authorization: `Basic ${Buffer.from(authString).toString("base64")}`, - "content-type": "application/x-www-form-urlencoded", - dpop: await createDpopHeader(tokenUrl, "POST", dpopKey), - }, - body: "grant_type=client_credentials&scope=webid", - }); - - // This is the Access token that will be used to do an authenticated request to the server. - // The JSON also contains an "expires_in" field in seconds, - // which you can use to know when you need request a new Access token. - const response2 = await response.json(); - return { accessToken: response2.access_token, dpopKey }; - } catch (err) { - console.error(err); - throw err; - } -} - -export async function generateAuthFetch() { - const authorization = await getAuthorization(); - const { id, secret } = await getSecret(authorization); - const { accessToken, dpopKey } = await getAccessToken(id, secret); - return await buildAuthenticatedFetch(accessToken, { dpopKey }); -} diff --git a/packages/connected-solid/test/configs/server-config.json b/packages/connected-solid/test/configs/server-config.json deleted file mode 100644 index 5e96784..0000000 --- a/packages/connected-solid/test/configs/server-config.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "@context": [ - "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld" - ], - "import": [ - "css:config/app/init/static-root.json", - "css:config/app/main/default.json", - "css:config/app/variables/default.json", - "css:config/http/handler/default.json", - "css:config/http/middleware/default.json", - "css:config/http/notifications/all.json", - "css:config/http/server-factory/http.json", - "css:config/http/static/default.json", - "css:config/identity/access/public.json", - "css:config/identity/email/default.json", - "css:config/identity/handler/default.json", - "css:config/identity/oidc/default.json", - "css:config/identity/ownership/token.json", - "css:config/identity/pod/static.json", - "css:config/ldp/authentication/dpop-bearer.json", - "css:config/ldp/authorization/webacl.json", - "css:config/ldp/handler/default.json", - "css:config/ldp/metadata-parser/default.json", - "css:config/ldp/metadata-writer/default.json", - "css:config/ldp/modes/default.json", - "css:config/storage/backend/memory.json", - "css:config/storage/key-value/resource-store.json", - "css:config/storage/location/pod.json", - "css:config/storage/middleware/default.json", - "css:config/util/auxiliary/acl.json", - "css:config/util/identifiers/suffix.json", - "css:config/util/index/default.json", - "css:config/util/logging/winston.json", - "css:config/util/representation-conversion/default.json", - "css:config/util/resource-locker/memory.json", - "css:config/util/variables/default.json" - ], - "@graph": [ - { - "comment": "A Solid server that stores its resources on disk and uses WAC for authorization." - } - ] -} \ No newline at end of file diff --git a/packages/connected-solid/test/configs/solid-css-seed.json b/packages/connected-solid/test/configs/solid-css-seed.json deleted file mode 100644 index 5894d0d..0000000 --- a/packages/connected-solid/test/configs/solid-css-seed.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "email": "hello@example.com", - "password": "abc123", - "pods": [ - { "name": "example" } - ] - } -] \ No newline at end of file diff --git a/packages/connected-solid/test/setup-tests.ts b/packages/connected-solid/test/setup-tests.ts deleted file mode 100644 index f4378ae..0000000 --- a/packages/connected-solid/test/setup-tests.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { config } from "dotenv"; - -config(); diff --git a/packages/connected-solid/test/solidServer.helper.ts b/packages/connected-solid/test/solidServer.helper.ts deleted file mode 100644 index 848abc1..0000000 --- a/packages/connected-solid/test/solidServer.helper.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Taken from https://github.com/comunica/comunica/blob/b237be4265c353a62a876187d9e21e3bc05123a3/engines/query-sparql/test/QuerySparql-solid-test.ts#L9 - -import * as path from "path"; -import type { App } from "@solid/community-server"; -import { AppRunner, resolveModulePath } from "@solid/community-server"; -import "jest-rdf"; -import type { SolidContainerUri } from "../src"; - -export const SERVER_DOMAIN = process.env.SERVER || "http://localhost:3001/"; -export const ROOT_ROUTE = process.env.ROOT_CONTAINER || ""; -export const ROOT_CONTAINER = - `${SERVER_DOMAIN}${ROOT_ROUTE}` as SolidContainerUri; -export const WEB_ID = - process.env.WEB_ID || `${SERVER_DOMAIN}example/profile/card#me`; - -// Use an increased timeout, since the CSS server takes too much setup time. -jest.setTimeout(40_000); - -export async function createApp(customConfigPath?: string): Promise { - if (process.env.SERVER) { - return { - start: () => {}, - stop: () => {}, - } as App; - } - const appRunner = new AppRunner(); - - return appRunner.create({ - loaderProperties: { - mainModulePath: resolveModulePath(""), - typeChecking: false, - }, - config: customConfigPath ?? resolveModulePath("config/file-root.json"), - variableBindings: {}, - shorthand: { - port: 3_001, - loggingLevel: "off", - seedConfig: path.join(__dirname, "configs", "solid-css-seed.json"), - rootFilePath: path.join(__dirname, "./data"), - }, - }); -} diff --git a/packages/test-solid-server/package.json b/packages/test-solid-server/package.json index f1d9efe..1d8af49 100644 --- a/packages/test-solid-server/package.json +++ b/packages/test-solid-server/package.json @@ -1,11 +1,12 @@ { "name": "@ldo/test-solid-server", - "version": "1.0.0-alpha.3", + "version": "1.0.0-alpha.8", "description": "A solid server to be used in jest tests", "main": "dist/index.js", "scripts": { - "build": "tsc --project tsconfig.build.json", + "build": "tsc --project tsconfig.build.json && npm run copy-configs", "prepublishOnly": "npm run build", + "copy-configs": "cp -r src/configs dist/configs", "lint": "eslint src/** --fix --no-error-on-unmatched-pattern" }, "repository": { diff --git a/packages/test-solid-server/src/index.ts b/packages/test-solid-server/src/index.ts index 1489745..c019d75 100644 --- a/packages/test-solid-server/src/index.ts +++ b/packages/test-solid-server/src/index.ts @@ -1,2 +1,3 @@ export * from "./createServer"; export * from "./setupTestServer"; +export * from "./resourceUtils"; diff --git a/packages/test-solid-server/src/resourceUtils.ts b/packages/test-solid-server/src/resourceUtils.ts index 3e1ac50..868919c 100644 --- a/packages/test-solid-server/src/resourceUtils.ts +++ b/packages/test-solid-server/src/resourceUtils.ts @@ -3,7 +3,7 @@ export type ResourceInfo = ContainerInfo | LeafInfo; interface ContainerInfo { slug: string; isContainer: true; - shouldNotInit: boolean; + shouldNotInit?: boolean; contains: (ContainerInfo | LeafInfo)[]; } @@ -39,11 +39,10 @@ export async function initResources( ), ); } else { - authFetch(rootUri, { - method: "POST", + await authFetch(`${rootUri}${resourceInfo.slug}`, { + method: "PUT", headers: { "content-type": resourceInfo.mimeType, - slug: resourceInfo.slug, }, body: resourceInfo.data, }); diff --git a/packages/test-solid-server/src/setupTestServer.ts b/packages/test-solid-server/src/setupTestServer.ts index ac6fb65..ccbf7da 100644 --- a/packages/test-solid-server/src/setupTestServer.ts +++ b/packages/test-solid-server/src/setupTestServer.ts @@ -18,13 +18,11 @@ export function setupServer( Promise, [input: RequestInfo | URL, init?: RequestInit | undefined] >; + authFetch: typeof fetch; rootUri: string; - rootContainer: string; } = { - rootUri: `https://localhost:${port}`, - rootContainer: `https://localhost:${port}/example/`, + rootUri: `http://localhost:${port}/`, } as any; - let authFetch: typeof fetch; let previousJestId: string | undefined; let previousNodeEnv: string | undefined; @@ -37,7 +35,7 @@ export function setupServer( // Start up the server data.app = await createApp(port, customConfigPath); await data.app.start(); - authFetch = await generateAuthFetch(port); + data.authFetch = await generateAuthFetch(port); }); afterAll(async () => { @@ -49,13 +47,13 @@ export function setupServer( }); beforeEach(async () => { - data.fetchMock = jest.fn(authFetch); + data.fetchMock = jest.fn(data.authFetch); // Create a new document called sample.ttl - await initResources(data.rootUri, resourceInfo, authFetch); + await initResources(data.rootUri, resourceInfo, data.authFetch); }); afterEach(async () => { - await cleanResources(data.rootUri, resourceInfo, authFetch); + await cleanResources(data.rootUri, resourceInfo, data.authFetch); }); return data;