UNPKG

@inrupt/solid-client

Version:

Make your web apps work with Solid Pods.

253 lines (226 loc) • 8.66 kB
// Copyright Inrupt Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the // Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import { describe, it, jest, expect } from "@jest/globals"; import { rdf, security } from "../constants"; import { mockSolidDatasetFrom } from "../resource/mock"; import { buildThing } from "../thing/build"; import { getUrl } from "../thing/get"; import { mockThingFrom } from "../thing/mock"; import { getThing, setThing } from "../thing/thing"; import { addJwkToJwks, addPublicKeyToProfileJwks, getProfileJwksIri, setProfileJwks, } from "./jwks"; jest.spyOn(globalThis, "fetch").mockImplementation( async () => new Response(JSON.stringify({ keys: [] }), { headers: { Location: "https://arbitrary.pod/resource" }, }), ); jest.mock("../resource/solidDataset", () => { const actualResourceModule = jest.requireActual( "../resource/solidDataset", ) as any; return { ...actualResourceModule, getSolidDataset: jest.fn(), saveSolidDatasetAt: jest.fn(), }; }); jest.mock("../resource/file", () => { const actualResourceModule = jest.requireActual("../resource/file") as any; return { ...actualResourceModule, getFile: jest.fn(), }; }); describe("setProfileJwks", () => { it("overwrites an existing JWKS IRI value", () => { let dataset = mockSolidDatasetFrom("https://example.org/pod/"); const profile = buildThing(mockThingFrom("https://example.org/pod/me")) .setIri(security.publicKey, "https://example.org/pod/jwks") .build(); dataset = setThing(dataset, profile); dataset = setProfileJwks( dataset, "https://example.org/pod/me", "https://example.org/pod/new-jwks", ); const updatedProfile = getThing(dataset, "https://example.org/pod/me"); expect(getUrl(updatedProfile!, security.publicKey)).toBe( "https://example.org/pod/new-jwks", ); }); }); describe("getProfileJwksIri", () => { it("returns the JWKS IRI attached to a profile", () => { let dataset = mockSolidDatasetFrom("https://example.org/pod/"); const profile = buildThing(mockThingFrom("https://example.org/pod/me")) .setIri(security.publicKey, "https://example.org/pod/jwks") .build(); dataset = setThing(dataset, profile); expect(getProfileJwksIri(dataset, "https://example.org/pod/me")).toBe( "https://example.org/pod/jwks", ); }); it("returns null if no JWKS IRI is attached to a profile", () => { let dataset = mockSolidDatasetFrom("https://example.org/pod/"); const profile = mockThingFrom("https://example.org/pod/me"); dataset = setThing(dataset, profile); expect(getProfileJwksIri(dataset, "https://example.org/pod/me")).toBeNull(); }); }); describe("addJwkToJwks", () => { it("returns an updated JWKS with the provided JWK", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce(new Response(JSON.stringify({ keys: [] }))); const jwks = await addJwkToJwks( { kid: "..." }, "https://example.org/jwks", { fetch: mockFetch, }, ); expect(jwks).toEqual({ keys: [{ kid: "..." }] }); }); it("uses the default fetch if none is provided", async () => { await addJwkToJwks({ kid: "..." }, "https://example.org/jwks"); expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledWith("https://example.org/jwks"); }); it("throws if the given IRI does not resolve to a JWKS", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce(new Response(JSON.stringify("Not a JWKS"))); await expect( addJwkToJwks({ kid: "..." }, "https://example.org/jwks", { fetch: mockFetch, }), ).rejects.toThrow(/example.org.*Not a JWKS/); }); it("throws if the given IRI cannot be resolved", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( new Response("", { status: 400, statusText: "Bad request" }), ); await expect( addJwkToJwks({ kid: "..." }, "https://example.org/jwks", { fetch: mockFetch, }), ).rejects.toThrow(/400.*Bad request/); }); it("throws if the given IRI resolves to a JSON document which isn't a JWKS", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( new Response(JSON.stringify({ someKey: "some value" })), ); await expect( addJwkToJwks({ kid: "..." }, "https://example.org/jwks", { fetch: mockFetch, }), ).rejects.toThrow(/example.org.*valid JWKS.*some value/); }); }); describe("addPublicKeyToProfileJwks", () => { it("throws if the profile cannot be fetched", async () => { const mockedDatasetModule = jest.requireMock( "../resource/solidDataset", ) as any; mockedDatasetModule.getSolidDataset.mockResolvedValue(null); await expect( addPublicKeyToProfileJwks( { kid: "..." }, "https://some.pod/resource#webId", ), ).rejects.toThrow(/profile document.*webId.*retrieved/); await expect( addPublicKeyToProfileJwks( { kid: "..." }, "https://some.pod/resource#webId", {}, ), ).rejects.toThrow(/profile document.*webId.*retrieved/); }); it("throws if the profile does not have a JWKS", async () => { let mockedDataset = mockSolidDatasetFrom("https://some.pod/resource"); const profile = buildThing({ name: "webId" }) .addUrl(rdf.type, "https://example.org/ns/Person") .build(); mockedDataset = setThing(mockedDataset, profile); const mockedDatasetModule = jest.requireMock( "../resource/solidDataset", ) as any; mockedDatasetModule.getSolidDataset.mockResolvedValueOnce(mockedDataset); await expect( addPublicKeyToProfileJwks( { kid: "..." }, "https://some.pod/resource#webId", ), ).rejects.toThrow(/No key set.*webId/); }); it("adds the public key to JWKS file", async () => { let mockedDataset = mockSolidDatasetFrom("https://some.pod/resource"); const profile = buildThing({ name: "webId" }) .addUrl(security.publicKey, "https://example.org/pod/jwks") .build(); mockedDataset = setThing(mockedDataset, profile); const mockedDatasetModule = jest.requireMock( "../resource/solidDataset", ) as any; mockedDatasetModule.getSolidDataset.mockResolvedValueOnce(mockedDataset); const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce(new Response(JSON.stringify({ keys: [] }))) .mockResolvedValueOnce(new Response("")); const mockedFileModule = jest.requireMock("../resource/file") as any; const spiedOverWrite = jest.spyOn(mockedFileModule, "overwriteFile"); await addPublicKeyToProfileJwks( { kid: "..." }, "https://some.pod/resource#webId", { fetch: mockFetch, }, ); // Intercept the saved dataset const savedJwks = spiedOverWrite.mock.calls[0][1]; // check public key matches expect(savedJwks).toEqual( new Blob([JSON.stringify({ keys: [{ kid: "..." }] })]), ); }); it("throws if the profile doc does not include the WebId", async () => { const mockedDataset = mockSolidDatasetFrom("https://some.pod/resource"); const mockedDatasetModule = jest.requireMock( "../resource/solidDataset", ) as any; mockedDatasetModule.getSolidDataset.mockResolvedValueOnce(mockedDataset); await expect( addPublicKeyToProfileJwks( { kid: "..." }, "https://some.pod/resource#webId", ), ).rejects.toThrow(/Profile document.*webId/); }); });