UNPKG

@inrupt/solid-client

Version:

Make your web apps work with Solid Pods.

771 lines (709 loc) • 25.5 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 { jest, describe, it, expect, afterEach, beforeEach, } from "@jest/globals"; import type { SolidDataset, WithServerResourceInfo } from ".."; import { buildThing, createSolidDataset, createThing, getSourceIri, getThingAll, mockSolidDatasetFrom, setStringNoLocale, setThing, } from ".."; import { foaf, pim, rdf, rdfs } from "../constants"; import { triplesToTurtle } from "../formats/turtle"; import { toRdfJsQuads } from "../rdfjs.internal"; import { getAltProfileUrlAllFrom, getPodUrlAll, getPodUrlAllFrom, getProfileAll, getWebIdDataset, } from "./webid"; import { mockResponse } from "../tests.internal"; const MOCK_WEBID = "https://example.org/some.webid"; const MOCK_PROFILE = setThing( createSolidDataset(), setStringNoLocale( createThing({ url: MOCK_WEBID }), "https://example.org/ns#somePredicate", "Some value", ), ); describe("getAltProfileUrlAllFrom", () => { it("returns no alt profiles if the WebID profile contains no triples with the rdfs:seeAlso or foaf:primaryTopic/foaf:isPrimaryTopicOf predicate", async () => { const webIdProfile = mockSolidDatasetFrom(MOCK_WEBID); await expect( getProfileAll(MOCK_WEBID, { webIdProfile }), ).resolves.toStrictEqual({ webIdProfile, altProfileAll: [], }); }); it("returns an array of the IRI of subject of triples of the WebID doc with the foaf:primaryTopic predicate not matching the WebID", () => { const profileContent = buildThing({ url: "https://some.profile" }) .addIri(foaf.primaryTopic, MOCK_WEBID) .build(); const otherProfileContent = buildThing({ url: "https://some.other.profile", }) .addIri(foaf.primaryTopic, MOCK_WEBID) .build(); let webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); webIdProfile = setThing(webIdProfile, otherProfileContent); const result = getAltProfileUrlAllFrom(MOCK_WEBID, webIdProfile); expect(result).toHaveLength(2); expect(result).toContain("https://some.profile"); expect(result).toContain("https://some.other.profile"); }); it("returns an array of the IRI of objects of triples of the WebID doc such as <webid, rdfs:seeAlso, ?object>", () => { const profileContent = buildThing({ url: MOCK_WEBID }) .addIri(rdfs.seeAlso, "https://some.profile") .addIri(rdfs.seeAlso, "https://some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); const result = getAltProfileUrlAllFrom(MOCK_WEBID, webIdProfile); expect(result).toHaveLength(2); expect(result).toContain("https://some.profile"); expect(result).toContain("https://some.other.profile"); }); it("returns an array of the IRI of objects of triples of the WebID doc such as <webid, foaf:isPrimaryTopicOf, ?object>", () => { const profileContent = buildThing({ url: MOCK_WEBID }) .addIri(foaf.isPrimaryTopicOf, "https://some.profile") .addIri(foaf.isPrimaryTopicOf, "https://some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); const result = getAltProfileUrlAllFrom(MOCK_WEBID, webIdProfile); expect(result).toHaveLength(2); expect(result).toContain("https://some.profile"); expect(result).toContain("https://some.other.profile"); }); it("returns an array of the IRI of objects of triples of the WebID doc such as <webid, foaf:isPrimaryTopicOf, ?object>, <webid, foaf:isPrimaryTopicOf, ?object>, in the correct order prioritizing rdfs:seeAlso", () => { const profileContent = buildThing({ url: MOCK_WEBID }) .addIri(foaf.isPrimaryTopicOf, "https://some.profile") .addIri(rdfs.seeAlso, "https://some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); const result = getAltProfileUrlAllFrom(MOCK_WEBID, webIdProfile); expect(result).toHaveLength(2); expect(result[0]).toBe("https://some.other.profile"); expect(result[1]).toBe("https://some.profile"); }); it("deduplicates profile values", () => { // The profile document will have two triples <profile, foaf:primaryTopic, webid>... const profileContent = buildThing({ url: "https://some.profile" }) .addIri(foaf.primaryTopic, MOCK_WEBID) .build(); // and <webid, foaf:isPrimaryTopicOf, profile>. const webidData = buildThing({ url: MOCK_WEBID }) .addIri(foaf.isPrimaryTopicOf, "https://some.profile") .build(); let webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); webIdProfile = setThing(webIdProfile, webidData); const result = getAltProfileUrlAllFrom(MOCK_WEBID, webIdProfile); // 'profile' should appear only once in the result set. expect(result).toHaveLength(1); expect(result).toContain("https://some.profile"); }); }); const profileFn: typeof fetch = async (url) => { const profileContent = buildThing({ url: "https://example.org/some.profile" }) .addIri(foaf.primaryTopic, MOCK_WEBID) .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); const mapping = { "https://example.org/some.profile": MOCK_PROFILE, [MOCK_WEBID]: webIdProfile, }; if (!Object.keys(mapping).includes(url.toString())) { throw new Error(`Unexpected URL: ${url.toString()}`); } return new Response( await triplesToTurtle( toRdfJsQuads(mapping[url.toString() as keyof typeof mapping]), ), { headers: { "Content-Type": "text/turtle", }, }, ); }; describe("getProfileAll", () => { let fetchSpy: jest.SpiedFunction<typeof fetch>; beforeEach(() => { fetchSpy = jest.spyOn(globalThis, "fetch").mockImplementation(profileFn); }); afterEach(() => { jest.resetAllMocks(); }); it("defaults to the embeded fetch if available to fetch alt profiles", async () => { await getProfileAll(MOCK_WEBID); // The embedded fetch should have been used to fetch the WebID Profile first, // and then the alt profile. expect(fetchSpy).toHaveBeenCalledTimes(2); expect(fetchSpy).toHaveBeenNthCalledWith(1, MOCK_WEBID, expect.anything()); expect(fetchSpy).toHaveBeenNthCalledWith( 2, "https://example.org/some.profile", expect.anything(), ); }); it("uses the provided fetch to fetch alt profiles, but not the WebID", async () => { // Mock the alt profile authenticated fetch const mockedAuthFetcher = jest.fn<typeof fetch>(profileFn); await getProfileAll(MOCK_WEBID, { fetch: mockedAuthFetcher }); // The embedded fetch should have been used. expect(mockedAuthFetcher).toHaveBeenCalledTimes(1); expect(fetchSpy).toHaveBeenCalledTimes(1); }); it("does not fetch the WebID profile document if provided", async () => { const mockedFetch = jest.fn<typeof fetch>(); const webIdProfile = mockSolidDatasetFrom(MOCK_WEBID); await expect( getProfileAll(MOCK_WEBID, { fetch: mockedFetch, webIdProfile }), ).resolves.toStrictEqual({ webIdProfile, altProfileAll: [], }); expect(mockedFetch).not.toHaveBeenCalled(); }); it("returns an array of the subject of triples of the WebID doc with the foaf:primaryTopic predicate not matching the WebID", async () => { const mockedFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.profile", ), ) .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.other.profile", ), ); const profileContent = buildThing({ url: "https://some.profile" }) .addIri(foaf.primaryTopic, MOCK_WEBID) .build(); const otherProfileContent = buildThing({ url: "https://some.other.profile", }) .addIri(foaf.primaryTopic, MOCK_WEBID) .build(); let webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); webIdProfile = setThing(webIdProfile, otherProfileContent); const result = await getProfileAll(MOCK_WEBID, { fetch: mockedFetch, webIdProfile, }); // The provided fetch should have been used. expect(mockedFetch).toHaveBeenCalled(); expect(result.altProfileAll).toHaveLength(2); expect(getThingAll(result.altProfileAll[0])).toStrictEqual( getThingAll(MOCK_PROFILE), ); expect(getThingAll(result.altProfileAll[1])).toStrictEqual( getThingAll(MOCK_PROFILE), ); }); it("returns an array of the objects of triples of the WebID doc such as <webid, rdfs:seeAlso, ?object>", async () => { const mockedFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.profile", ), ) .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.other.profile", ), ); const profileContent = buildThing({ url: MOCK_WEBID }) .addIri(rdfs.seeAlso, "https://some.profile") .addIri(rdfs.seeAlso, "https://some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); const result = await getProfileAll(MOCK_WEBID, { fetch: mockedFetch, webIdProfile, }); expect(result.altProfileAll).toHaveLength(2); expect(getThingAll(result.altProfileAll[0])).toStrictEqual( getThingAll(MOCK_PROFILE), ); expect(getThingAll(result.altProfileAll[1])).toStrictEqual( getThingAll(MOCK_PROFILE), ); }); it("returns an array of the objects of triples of the WebID doc such as <webid, foaf:isPrimaryTopicOf, ?object>", async () => { const mockedFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.profile", ), ) .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.other.profile", ), ); const profileContent = buildThing({ url: MOCK_WEBID }) .addIri(foaf.isPrimaryTopicOf, "https://some.profile") .addIri(foaf.isPrimaryTopicOf, "https://some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); const result = await getProfileAll(MOCK_WEBID, { fetch: mockedFetch, webIdProfile, }); expect(result.altProfileAll).toHaveLength(2); expect(getThingAll(result.altProfileAll[0])).toStrictEqual( getThingAll(MOCK_PROFILE), ); expect(getThingAll(result.altProfileAll[1])).toStrictEqual( getThingAll(MOCK_PROFILE), ); }); it("deduplicates profile values", async () => { const mockedFetch = jest.fn<typeof fetch>().mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.profile", ), ); // The profile document will have two triples <profile, foaf:primaryTopic, webid>... const profileContent = buildThing({ url: "https://some.profile" }) .addIri(foaf.primaryTopic, MOCK_WEBID) .build(); // and <webid, foaf:isPrimaryTopicOf, profile>. const webidData = buildThing({ url: MOCK_WEBID }) .addIri(foaf.isPrimaryTopicOf, "https://some.profile") .addIri(rdfs.seeAlso, "https://some.profile") .build(); let webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); webIdProfile = setThing(webIdProfile, webidData); const result = await getProfileAll(MOCK_WEBID, { fetch: mockedFetch, webIdProfile, }); // 'profile' should appear only once in the result set. expect(result.altProfileAll).toHaveLength(1); expect(mockedFetch).toHaveBeenCalledTimes(1); }); it("handles gracefully fetch errors on alternative profiles", async () => { const mockedFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.profile", ), ) // Fetching one of the alt profiles fails. .mockResolvedValueOnce( mockResponse(undefined, { status: 401, }), ); const profileContent = buildThing({ url: MOCK_WEBID }) .addIri(foaf.isPrimaryTopicOf, "https://some.profile") .addIri(foaf.isPrimaryTopicOf, "https://some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); const result = await getProfileAll(MOCK_WEBID, { fetch: mockedFetch, webIdProfile, }); expect(result.altProfileAll).toHaveLength(1); expect(getThingAll(result.altProfileAll[0])).toStrictEqual( getThingAll(MOCK_PROFILE), ); }); }); const mockProfileDoc = ( iri: string, webId: string, content: Partial<{ altProfiles: string[]; pods: string[] }>, ): SolidDataset & WithServerResourceInfo => { const profileContent = buildThing({ url: webId }).addIri( rdf.type, foaf.Agent, ); content.altProfiles?.forEach((altProfileIri) => { profileContent.addIri(foaf.isPrimaryTopicOf, altProfileIri); }); content.pods?.forEach((podIri) => { profileContent.addIri(pim.storage, podIri); }); return setThing(mockSolidDatasetFrom(iri), profileContent.build()); }; describe("getPodUrlAll", () => { let fetchSpy: jest.SpiedFunction<typeof fetch>; beforeEach(() => { fetchSpy = jest.spyOn(globalThis, "fetch").mockImplementation(profileFn); }); afterEach(() => { jest.resetAllMocks(); }); it("does not use the provided fetch to dereference the WebID", async () => { const mockedFetch = jest.fn<typeof fetch>(); await getPodUrlAll("https://example.org/some.profile", { fetch: mockedFetch, }); expect(mockedFetch).not.toHaveBeenCalled(); expect(fetchSpy).toHaveBeenCalled(); }); it("uses the embedded fetch if solid-client-authn-browser is in the dependencies", async () => { const profileContent = buildThing({ url: MOCK_WEBID }) // This will point to an alt profile, prompting the authenticated fetch. .addIri(rdfs.seeAlso, "https://some.profile") .addIri(foaf.isPrimaryTopicOf, "https://some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); // The WebID is explicitly fetched using the unauthenticated fetch. fetchSpy.mockImplementation( async () => new Response(await triplesToTurtle(toRdfJsQuads(webIdProfile)), { headers: { "Content-Type": "text/turtle", }, }), ); await getPodUrlAll(MOCK_WEBID); // The embedded fetch should have been used. expect(fetchSpy).toHaveBeenCalled(); }); it("uses the provided fetch to fetch alt profiles, but not the WebID", async () => { const mockedAuthFetch = jest.fn<typeof fetch>(); mockedAuthFetch.mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(MOCK_PROFILE)), { headers: { "Content-Type": "text/turtle", }, }, "https://some.profile", ), ); const profileContent = buildThing({ url: MOCK_WEBID }) // This will point to an alt profile, prompting the authenticated fetch. .addIri(rdfs.seeAlso, "https://example.org/some.profile") .addIri(foaf.isPrimaryTopicOf, "https://example.org/some.other.profile") .build(); const webIdProfile = setThing( mockSolidDatasetFrom(MOCK_WEBID), profileContent, ); fetchSpy.mockResolvedValueOnce( new Response(await triplesToTurtle(toRdfJsQuads(webIdProfile)), { headers: { "Content-Type": "text/turtle", }, url: MOCK_WEBID, } as ResponseInit), ); await getPodUrlAll(MOCK_WEBID, { fetch: mockedAuthFetch }); // The provided authenticated fetch should have been used to fetch the alt profile. expect(mockedAuthFetch).toHaveBeenCalledTimes(2); expect(mockedAuthFetch).toHaveBeenCalledWith( "https://example.org/some.profile", expect.anything(), ); expect(mockedAuthFetch).toHaveBeenCalledWith( "https://example.org/some.other.profile", expect.anything(), ); // The unauthenticated fetch should have been used to fetch the webid profile. expect(fetchSpy).toHaveBeenCalledTimes(1); expect(fetchSpy).toHaveBeenCalledWith(MOCK_WEBID, expect.anything()); }); it("returns Pod URLs found in the fetched WebId profile", async () => { const MOCK_STORAGE = "https://some.storage"; const webIdProfile = mockProfileDoc("https://some.profile", MOCK_WEBID, { pods: [MOCK_STORAGE], }); fetchSpy.mockResolvedValueOnce( mockResponse(await triplesToTurtle(toRdfJsQuads(webIdProfile)), { headers: { "Content-Type": "text/turtle", }, }), ); await expect(getPodUrlAll(MOCK_WEBID)).resolves.toStrictEqual([ MOCK_STORAGE, ]); }); it("returns all Pod URLs found in fetched alternative profiles", async () => { const ALT_MOCK_STORAGE_1 = "https://some.storage"; const ALT_MOCK_STORAGE_2 = "https://some-other.storage"; const altProfileAll = [ mockProfileDoc("https://some.alt-profile", MOCK_WEBID, { pods: [ALT_MOCK_STORAGE_1], }), mockProfileDoc("https://some.other.alt-profile", MOCK_WEBID, { pods: [ALT_MOCK_STORAGE_2], }), ]; const webIdProfile = mockProfileDoc("https://some.profile", MOCK_WEBID, { altProfiles: altProfileAll.map(getSourceIri) as string[], }); fetchSpy.mockResolvedValueOnce( new Response(await triplesToTurtle(toRdfJsQuads(webIdProfile)), { headers: { "Content-Type": "text/turtle", }, }), ); // The alternative profiles are Solid resources, and require authentication. const mockedFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(altProfileAll[0])), { headers: { "Content-Type": "text/turtle", }, }, "https://some.alt-profile", ), ) .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(altProfileAll[1])), { headers: { "Content-Type": "text/turtle", }, }, "https://some.other.alt-profile", ), ); await expect( getPodUrlAll(MOCK_WEBID, { fetch: mockedFetch }), ).resolves.toStrictEqual([ALT_MOCK_STORAGE_1, ALT_MOCK_STORAGE_2]); }); it("returns Pod URLs from both the fetched WebID profile and fetched alternative profiles when applicable", async () => { const MOCK_STORAGE = "https://some.storage"; const ALT_MOCK_STORAGE = "https://some-other.storage"; const altProfileAll = [ mockProfileDoc("https://some.alt-profile", MOCK_WEBID, { pods: [ALT_MOCK_STORAGE], }), ]; const webIdProfile = mockProfileDoc("https://some.profile", MOCK_WEBID, { altProfiles: altProfileAll.map(getSourceIri) as string[], pods: [MOCK_STORAGE], }); fetchSpy.mockResolvedValueOnce( new Response(await triplesToTurtle(toRdfJsQuads(webIdProfile)), { headers: { "Content-Type": "text/turtle", }, }), ); const mockedFetch = jest.fn<typeof fetch>().mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(altProfileAll[0])), { headers: { "Content-Type": "text/turtle", }, }, "https://some.alt-profile", ), ); await expect( getPodUrlAll(MOCK_WEBID, { fetch: mockedFetch }), ).resolves.toStrictEqual([MOCK_STORAGE, ALT_MOCK_STORAGE]); }); }); describe("getPodUrlAllFrom", () => { it("returns an empty result if the given resources doesn't have the WebID as a subject", () => { const MOCK_STORAGE = "https://some.storage"; const webIdProfile = mockProfileDoc( "https://some.profile", "https://some.different.webid", { pods: [MOCK_STORAGE], }, ); expect( getPodUrlAllFrom({ webIdProfile, altProfileAll: [] }, MOCK_WEBID), ).toStrictEqual([]); }); it("returns Pod URLs found in the WebId profile", () => { const MOCK_STORAGE = "https://some.storage"; const webIdProfile = mockProfileDoc("https://some.profile", MOCK_WEBID, { pods: [MOCK_STORAGE], }); expect( getPodUrlAllFrom({ webIdProfile, altProfileAll: [] }, MOCK_WEBID), ).toStrictEqual([MOCK_STORAGE]); }); it("returns all Pod URLs found in alternative profiles", () => { const ALT_MOCK_STORAGE_1 = "https://some.storage"; const ALT_MOCK_STORAGE_2 = "https://some-other.storage"; const altProfileAll = [ mockProfileDoc("https://some.alt-profile", MOCK_WEBID, { pods: [ALT_MOCK_STORAGE_1], }), mockProfileDoc("https://some.other.alt-profile", MOCK_WEBID, { pods: [ALT_MOCK_STORAGE_2], }), ]; const webIdProfile = mockProfileDoc("https://some.profile", MOCK_WEBID, { altProfiles: altProfileAll.map(getSourceIri) as string[], }); expect( getPodUrlAllFrom({ webIdProfile, altProfileAll }, MOCK_WEBID), ).toStrictEqual([ALT_MOCK_STORAGE_1, ALT_MOCK_STORAGE_2]); }); it("returns Pod URLs from both the WebID profile and alternative profiles when applicable", () => { const MOCK_STORAGE = "https://some.storage"; const ALT_MOCK_STORAGE = "https://some-other.storage"; const altProfileAll = [ mockProfileDoc("https://some.alt-profile", MOCK_WEBID, { pods: [ALT_MOCK_STORAGE], }), ]; const webIdProfile = mockProfileDoc("https://some.profile", MOCK_WEBID, { altProfiles: altProfileAll.map(getSourceIri) as string[], pods: [MOCK_STORAGE], }); expect( getPodUrlAllFrom({ webIdProfile, altProfileAll }, MOCK_WEBID), ).toStrictEqual([MOCK_STORAGE, ALT_MOCK_STORAGE]); }); }); describe("getWebIdDataset", () => { afterEach(() => { jest.restoreAllMocks(); }); it("returns a Solid Dataset for a given WebID", async () => { const webIdProfile = mockProfileDoc("https://some.profile", MOCK_WEBID, {}); jest.spyOn(globalThis, "fetch").mockResolvedValueOnce( mockResponse(await triplesToTurtle(toRdfJsQuads(webIdProfile)), { headers: { "Content-Type": "text/turtle", }, }), ); const result = await getWebIdDataset(MOCK_WEBID); expect(result?.graphs).toEqual(webIdProfile.graphs); }); it("throws an error if fetching fails", async () => { jest.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("error")); await expect(getWebIdDataset(MOCK_WEBID)).rejects.toThrow("error"); }); });