UNPKG

@inrupt/solid-client

Version:

Make your web apps work with Solid Pods.

1,160 lines (1,025 loc) • 39.7 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 } from "@jest/globals"; import { acp, rdf } from "../constants"; import * as SolidDatasetModule from "../resource/solidDataset"; import * as FileModule from "../resource/file"; import * as ResourceModule from "../resource/resource"; import { getFileWithAccessDatasets, getFileWithAcr, getLinkedAcrUrl, getReferencedPolicyUrlAll, getResourceInfoWithAccessDatasets, getResourceInfoWithAcr, getSolidDatasetWithAccessDatasets, getSolidDatasetWithAcr, isAcpControlled, saveAcrFor, getVcAccess, setVcAccess, } from "./acp"; import type { UrlString, WithServerResourceInfo, File } from "../interfaces"; import { createThing, setThing } from "../thing/thing"; import { addIri } from "../thing/add"; import type { AccessControlResource } from "./control"; import { mockSolidDatasetFrom } from "../resource/mock"; import { addMockAcrTo } from "./mock"; import { mockResponse } from "../tests.internal"; const spyFetch = jest.spyOn(globalThis, "fetch").mockImplementation(() => Promise.resolve( new Response(undefined, { headers: { Location: "https://arbitrary.pod/resource" }, }), ), ); const defaultMockPolicies = { policies: ["https://some.pod/policies#policy"], memberPolicies: ["https://some.pod/policies#memberPolicy"], acrPolicies: [] as string[], memberAcrPolicies: [] as string[], }; function mockAcr(accessTo: UrlString, policies = defaultMockPolicies) { let control = createThing({ name: "access-control" }); control = addIri(control, rdf.type, acp.AccessControl); policies.policies.forEach((policyUrl) => { control = addIri(control, acp.apply, policyUrl); }); policies.memberPolicies.forEach((policyUrl) => { control = addIri(control, acp.applyMembers, policyUrl); }); const acrUrl = `${accessTo}?ext=acr`; let acrThing = createThing({ url: acrUrl }); policies.acrPolicies.forEach((policyUrl) => { acrThing = addIri(acrThing, acp.access, policyUrl); }); policies.memberAcrPolicies.forEach((policyUrl) => { acrThing = addIri(acrThing, acp.accessMembers, policyUrl); }); let acr: AccessControlResource & WithServerResourceInfo = { ...mockSolidDatasetFrom(acrUrl), accessTo, }; acr = setThing(acr, control); acr = setThing(acr, acrThing); return acr; } describe("getSolidDatasetWithAcr", () => { it("calls the included fetcher by default", async () => { getSolidDatasetWithAcr("https://some.pod/resource").catch(() => { // We're just checking that this is called, // so we can ignore the error about not being able to parse // the mock Response. }); expect(fetch).toHaveBeenCalledWith("https://some.pod/resource", { headers: { Accept: "text/turtle" }, }); }); it("uses the given fetcher if provided", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValue( new Response(undefined, { headers: { "Content-Type": "text/turtle" } }), ); await getSolidDatasetWithAcr("https://some.pod/resource", { fetch: mockFetch, }); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); }); it("returns null for the ACR if it is not accessible to the current user", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( undefined, { headers: { Link: `<https://some.pod/acr.ttl>; rel="${acp.accessControl}"`, "Content-Type": "text/turtle", }, }, "https://some.pod/resource", ), ) .mockResolvedValueOnce(new Response("Not allowed", { status: 401 })); const fetchedDataset = await getSolidDatasetWithAcr( "https://some.pod/resource", { fetch: mockFetch }, ); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/acr.ttl"); expect(fetchedDataset.internal_acp.acr).toBeNull(); }); it("attaches the fetched ACR to the returned SolidDataset", async () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); mockedSolidDataset.internal_resourceInfo.linkedResources = { [acp.accessControl]: ["https://arbitrary.pod/resource?ext=acr"], }; const mockedAcr = mockAcr("https://arbitrary.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset .mockResolvedValueOnce(mockedSolidDataset) .mockResolvedValueOnce(mockedAcr); const fetchedDataset = await getSolidDatasetWithAcr( "https://some.pod/resource", ); expect(fetchedDataset.internal_acp.acr).toStrictEqual(mockedAcr); }); it('returns the ACR even if it is exposed via a rel="acl" Link header on the given Resource', async () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); const acrUrl = "https://arbitrary.pod/resource?ext=acr"; mockedSolidDataset.internal_resourceInfo.linkedResources = { acl: [acrUrl], }; mockedSolidDataset.internal_resourceInfo.aclUrl = acrUrl; const mockedAcr = mockAcr("https://arbitrary.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); mockedAcr.internal_resourceInfo.linkedResources = { type: ["http://www.w3.org/ns/solid/acp#AccessControlResource"], }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset .mockResolvedValueOnce(mockedSolidDataset) .mockResolvedValueOnce(mockedAcr); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockedAcr); const fetchedDataset = await getSolidDatasetWithAcr( "https://some.pod/resource", ); expect(fetchedDataset.internal_acp.acr).toStrictEqual(mockedAcr); }); it("returns nothing if the linked ACL is not an ACP ACR", async () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); const acrUrl = "https://arbitrary.pod/resource?ext=acr"; mockedSolidDataset.internal_resourceInfo.linkedResources = { acl: [acrUrl], }; mockedSolidDataset.internal_resourceInfo.aclUrl = acrUrl; // This is not an ACL, because it is linked with rel="acl", // but does not have a type of // http://www.w3.org/ns/solid/acp#AccessControlResource. const mockedNonAcr = mockSolidDatasetFrom("https://arbitrary.pod/resource"); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockedSolidDataset); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockedNonAcr); const fetchedDataset = await getSolidDatasetWithAcr( "https://some.pod/resource", ); expect(fetchedDataset.internal_acp.acr).toBeNull(); }); }); describe("getFileWithAcr", () => { it("calls the included fetcher by default", async () => { await getFileWithAcr("https://some.pod/resource"); expect(fetch).toHaveBeenCalledWith("https://some.pod/resource", undefined); }); it("uses the given fetcher if provided", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue(new Response()); await getFileWithAcr("https://some.pod/resource", { fetch: mockFetch, }); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); }); it("returns null for the ACR if it is not accessible to the current user", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( undefined, { headers: { Link: `<https://some.pod/acr.ttl>; rel="${acp.accessControl}"`, }, }, "https://some.pod/resource", ), ) .mockResolvedValueOnce(new Response("Not allowed", { status: 401 })); const fetchedFile = await getFileWithAcr("https://some.pod/resource", { fetch: mockFetch, }); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/acr.ttl"); expect(fetchedFile.internal_acp.acr).toBeNull(); }); it("attaches the fetched ACR to the returned File", async () => { const mockedFile: File & WithServerResourceInfo = Object.assign( new Blob(), { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: true, linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, }, }, ); const mockedAcr = mockAcr("https://some.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); const mockedGetFile = jest.spyOn(FileModule, "getFile"); mockedGetFile.mockResolvedValueOnce(mockedFile); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockedAcr); const fetchedFile = await getFileWithAcr("https://some.pod/resource"); expect(fetchedFile.internal_acp.acr).toStrictEqual(mockedAcr); }); }); describe("getResourceInfoWithAcr", () => { it("calls the included fetcher by default", async () => { await getResourceInfoWithAcr("https://some.pod/resource"); expect(fetch).toHaveBeenCalledWith("https://some.pod/resource", { method: "HEAD", }); }); it("uses the given fetcher if provided", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue(new Response()); await getResourceInfoWithAcr("https://some.pod/resource", { fetch: mockFetch, }); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); }); it("attaches the fetched ACR to the returned ResourceInfo", async () => { const mockedResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: true, linkedResources: { [acp.accessControl]: ["https://arbitrary.pod/resource?ext=acr"], }, }, }; const mockedAcr = mockAcr("https://arbitrary.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockedResourceInfo); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockedAcr); const fetchedResourceInfo = await getResourceInfoWithAcr( "https://some.pod/resource", ); expect(fetchedResourceInfo.internal_acp.acr).toStrictEqual(mockedAcr); }); it("returns null for the ACR if it is not accessible to the current user", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValueOnce( mockResponse( undefined, { headers: { Link: `<https://some.pod/acr.ttl>; rel="${acp.accessControl}"`, }, }, "https://some.pod/resource", ), ) .mockResolvedValueOnce(mockResponse("Not allowed", { status: 401 })); const fetchedResourceInfo = await getResourceInfoWithAcr( "https://some.pod/resource", { fetch: mockFetch }, ); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/acr.ttl"); expect(fetchedResourceInfo.internal_acp.acr).toBeNull(); }); }); describe("getReferencedPolicyUrlAll", () => { it("returns an empty Object if no APRs were referenced", async () => { const mockedResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: true, linkedResources: { [acp.accessControl]: ["https://arbitrary.pod/resource?ext=acr"], }, }, }; const mockedAcr = mockAcr("https://arbitrary.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr); const policyUrls = getReferencedPolicyUrlAll(withMockedAcr); expect(policyUrls).toHaveLength(0); }); it("only includes one mention of a Resource that was referenced multiple times", async () => { const mockedResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: true, linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, }, }; const mockedAcr = mockAcr("https://some.pod/resource", { policies: ["https://some.pod/policy-resource#a-policy"], memberPolicies: [ "https://some.pod/policy-resource#a-member-policy", "https://some.pod/policy-resource#another-member-policy", ], acrPolicies: [], memberAcrPolicies: [], }); const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr); const policyUrls = getReferencedPolicyUrlAll(withMockedAcr); expect(policyUrls).toEqual(["https://some.pod/policy-resource"]); }); it("includes all referenced Policy Resources", async () => { const mockedResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: true, linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, }, }; const mockedAcr = mockAcr("https://some.pod/resource", { policies: ["https://some.pod/policy-resource#a-policy"], memberPolicies: [ "https://some.pod/policy-resource#a-member-policy", "https://some.pod/other-policy-resource#another-member-policy", ], acrPolicies: [], memberAcrPolicies: [], }); const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr); const policyUrls = getReferencedPolicyUrlAll(withMockedAcr); expect(policyUrls).toEqual([ "https://some.pod/policy-resource", "https://some.pod/other-policy-resource", ]); }); it("includes referenced ACR Policy Resources", async () => { const mockedResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: true, linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, }, }; const mockedAcr = mockAcr("https://some.pod/resource", { policies: [], memberPolicies: [], acrPolicies: ["https://some.pod/policy-resource#an-acr-policy"], memberAcrPolicies: [ "https://some.pod/other-policy-resource#another-acr-policy", ], }); const withMockedAcr = addMockAcrTo(mockedResourceInfo, mockedAcr); const policyUrls = getReferencedPolicyUrlAll(withMockedAcr); expect(policyUrls).toEqual([ "https://some.pod/policy-resource", "https://some.pod/other-policy-resource", ]); }); }); describe("getSolidDatasetWithAccessDatasets", () => { it("fetches the Resource at the given URL", async () => { const mockedGetSolidDataset = jest .spyOn(SolidDatasetModule, "getSolidDataset") .mockResolvedValueOnce(mockSolidDatasetFrom("https://some.pod/resource")); await getSolidDatasetWithAccessDatasets("https://some.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource", undefined, ); }); it("fetches the ACL when the SolidDataset at the given URL exposes one", async () => { const mockDataset = mockSolidDatasetFrom("https://arbitrary.pod/resource"); mockDataset.internal_resourceInfo.aclUrl = "https://some.pod/resource.acl"; mockDataset.internal_resourceInfo.linkedResources = { acl: ["https://some.pod/resource.acl"], }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockDataset); await getSolidDatasetWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource.acl", undefined, ); }); it("fetches the ACR when the SolidDataset at the given URL exposes one", async () => { const mockDataset = mockSolidDatasetFrom("https://arbitrary.pod/resource"); mockDataset.internal_resourceInfo.linkedResources = { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockDataset); await getSolidDatasetWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource?ext=acr", undefined, ); }); it("does not fetch any Access Dataset if none is exposed", async () => { const mockDataset = mockSolidDatasetFrom("https://arbitrary.pod/resource"); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockDataset); await getSolidDatasetWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); }); it("passes on the given fetcher to the Resource and ACL fetcher", async () => { const mockDataset = mockSolidDatasetFrom("https://some.pod/resource"); mockDataset.internal_resourceInfo.aclUrl = "https://some.pod/resource.acl"; mockDataset.internal_resourceInfo.linkedResources = { acl: ["https://some.pod/resource.acl"], }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockDataset); const mockedFetcher = jest .fn<typeof fetch>() .mockResolvedValue(new Response()); await getSolidDatasetWithAccessDatasets("https://some.pod/resource", { fetch: mockedFetcher, }); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2); expect(mockedGetSolidDataset).toHaveBeenNthCalledWith( 1, "https://some.pod/resource", expect.objectContaining({ fetch: mockedFetcher }), ); expect(mockedGetSolidDataset).toHaveBeenNthCalledWith( 2, "https://some.pod/resource.acl", expect.objectContaining({ fetch: mockedFetcher }), ); }); it("passes on the given fetcher to the Resource and ACR fetcher", async () => { const mockDataset = mockSolidDatasetFrom("https://some.pod/resource"); mockDataset.internal_resourceInfo.linkedResources = { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockDataset); const mockedFetcher = jest .fn<typeof fetch>() .mockResolvedValue(new Response()); await getSolidDatasetWithAccessDatasets("https://some.pod/resource", { fetch: mockedFetcher, }); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(2); expect(mockedGetSolidDataset).toHaveBeenNthCalledWith( 1, "https://some.pod/resource", expect.objectContaining({ fetch: mockedFetcher }), ); expect(mockedGetSolidDataset).toHaveBeenNthCalledWith( 2, "https://some.pod/resource?ext=acr", expect.objectContaining({ fetch: mockedFetcher }), ); }); }); describe("getFileWithAccessDatasets", () => { it("fetches the Resource at the given URL", async () => { const mockedGetFile = jest.spyOn(FileModule, "getFile"); await getFileWithAccessDatasets("https://some.pod/resource"); expect(mockedGetFile).toHaveBeenCalledTimes(1); expect(mockedGetFile).toHaveBeenLastCalledWith( "https://some.pod/resource", undefined, ); }); it("fetches the ACL when the File at the given URL exposes one", async () => { const mockFile = Object.assign(new Blob(), { internal_resourceInfo: { aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"] }, sourceIri: "https://arbitrary.pod/resource", isRawData: true, url: "https://arbitrary.pod/resource", }, }); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetFile = jest.spyOn(FileModule, "getFile"); mockedGetFile.mockResolvedValueOnce(mockFile); await getFileWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource.acl", undefined, ); }); it("fetches the ACR when the File at the given URL exposes one", async () => { const mockFile = Object.assign(new Blob(), { internal_resourceInfo: { linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, sourceIri: "https://arbitrary.pod/resource", isRawData: true, }, }); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetFile = jest.spyOn(FileModule, "getFile"); mockedGetFile.mockResolvedValueOnce(mockFile); await getFileWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource?ext=acr", undefined, ); }); it("does not fetch any Access Dataset if none is exposed", async () => { const mockFile = Object.assign(new Blob(), { internal_resourceInfo: { linkedResources: {}, sourceIri: "https://arbitrary.pod/resource", isRawData: true, }, }); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetFile = jest.spyOn(FileModule, "getFile"); mockedGetFile.mockResolvedValueOnce(mockFile); await getFileWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).not.toHaveBeenCalled(); }); it("passes on the given fetcher to the Resource and ACL fetcher", async () => { const mockFile = Object.assign(new Blob(), { internal_resourceInfo: { aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"] }, sourceIri: "https://arbitrary.pod/resource", isRawData: true, }, }); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetFile = jest.spyOn(FileModule, "getFile"); mockedGetFile.mockResolvedValueOnce(mockFile); const mockedFetcher = jest .fn<typeof fetch>() .mockResolvedValue(new Response()); await getFileWithAccessDatasets("https://some.pod/resource", { fetch: mockedFetcher, }); expect(mockedGetFile).toHaveBeenCalledTimes(1); expect(mockedGetFile).toHaveBeenLastCalledWith( "https://some.pod/resource", expect.objectContaining({ fetch: mockedFetcher }), ); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource.acl", expect.objectContaining({ fetch: mockedFetcher }), ); }); it("passes on the given fetcher to the Resource and ACR fetcher", async () => { const mockFile = Object.assign(new Blob(), { internal_resourceInfo: { linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, sourceIri: "https://arbitrary.pod/resource", isRawData: true, }, }); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetFile = jest.spyOn(FileModule, "getFile"); mockedGetFile.mockResolvedValueOnce(mockFile); const mockedFetcher = jest .fn<typeof fetch>() .mockResolvedValue(new Response()); await getFileWithAccessDatasets("https://some.pod/resource", { fetch: mockedFetcher, }); expect(mockedGetFile).toHaveBeenCalledTimes(1); expect(mockedGetFile).toHaveBeenLastCalledWith( "https://some.pod/resource", expect.objectContaining({ fetch: mockedFetcher }), ); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource?ext=acr", expect.objectContaining({ fetch: mockedFetcher }), ); }); }); describe("getResourceInfoWithAccessDatasets", () => { it("fetches the ResourceInfo for the given URL", async () => { const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); await getResourceInfoWithAccessDatasets("https://some.pod/resource"); expect(mockedGetResourceInfo).toHaveBeenCalledTimes(1); expect(mockedGetResourceInfo).toHaveBeenLastCalledWith( "https://some.pod/resource", undefined, ); }); it("fetches the ACL when the Resource at the given URL exposes one", async () => { const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"] }, sourceIri: "https://arbitrary.pod/resource", isRawData: true, }, }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo); await getResourceInfoWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource.acl", undefined, ); }); it("fetches the ACR when the Resource at the given URL exposes one", async () => { const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, sourceIri: "https://arbitrary.pod/resource", isRawData: true, }, }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo); await getResourceInfoWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource?ext=acr", undefined, ); }); it("does not fetch any Access Dataset if none is exposed", async () => { const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { linkedResources: {}, sourceIri: "https://arbitrary.pod/resource", isRawData: true, }, }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo); await getResourceInfoWithAccessDatasets("https://arbitrary.pod/resource"); expect(mockedGetSolidDataset).not.toHaveBeenCalled(); }); it("passes on the given fetcher to the ResourceInfo and ACL fetcher", async () => { const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { aclUrl: "https://some.pod/.acl", linkedResources: { acl: ["https://some.pod/.acl"] }, sourceIri: "https://some.pod/", isRawData: true, }, }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo); const mockedFetcher = jest .fn<typeof fetch>() .mockResolvedValue(new Response()); // We specifically use the root Resource here, // because otherwise `getResourceInfo` would be called on the parent Resource // to find a fallback ACL: await getResourceInfoWithAccessDatasets("https://some.pod/", { fetch: mockedFetcher, }); expect(mockedGetResourceInfo).toHaveBeenCalledTimes(1); expect(mockedGetResourceInfo).toHaveBeenLastCalledWith( "https://some.pod/", expect.objectContaining({ fetch: mockedFetcher }), ); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/.acl", expect.objectContaining({ fetch: mockedFetcher }), ); }); it("passes on the given fetcher to the ResourceInfo and ACR fetcher", async () => { const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { linkedResources: { [acp.accessControl]: ["https://some.pod/resource?ext=acr"], }, sourceIri: "https://some.pod/resource", isRawData: true, }, }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo.mockResolvedValueOnce(mockResourceInfo); const mockedFetcher = jest .fn<typeof fetch>() .mockResolvedValue(new Response()); await getResourceInfoWithAccessDatasets("https://some.pod/resource", { fetch: mockedFetcher, }); expect(mockedGetResourceInfo).toHaveBeenCalledTimes(1); expect(mockedGetResourceInfo).toHaveBeenLastCalledWith( "https://some.pod/resource", expect.objectContaining({ fetch: mockedFetcher }), ); expect(mockedGetSolidDataset).toHaveBeenCalledTimes(1); expect(mockedGetSolidDataset).toHaveBeenLastCalledWith( "https://some.pod/resource?ext=acr", expect.objectContaining({ fetch: mockedFetcher }), ); }); }); describe("saveAcrFor", () => { it("calls the included fetcher by default", async () => { spyFetch.mockResolvedValue( mockResponse(undefined, undefined, "https://arbitrary.pod/resource"), ); const mockedAcr = mockAcr("https://arbitrary.pod/resource"); const mockedResource = addMockAcrTo( mockSolidDatasetFrom("https://arbitrary.pod/resource"), mockedAcr, ); await saveAcrFor(mockedResource); expect(fetch).toHaveBeenCalledTimes(1); }); it("uses the given fetcher if provided", async () => { const mockFetch = jest .fn<typeof fetch>() .mockResolvedValue( mockResponse(undefined, undefined, "https://arbitrary.pod/resource"), ); const mockedAcr = mockAcr("https://arbitrary.pod/resource"); const mockedResource = addMockAcrTo( mockSolidDatasetFrom("https://arbitrary.pod/resource"), mockedAcr, ); await saveAcrFor(mockedResource, { fetch: mockFetch, }); expect(mockFetch).toHaveBeenCalledTimes(1); }); it("sends the ACR to the Pod", async () => { const mockedResponse = new Response(); jest .spyOn(mockedResponse, "url", "get") .mockReturnValue("https://arbitrary.pod/resource"); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue(mockedResponse); const mockedSaveSolidDatasetAt = jest.spyOn( SolidDatasetModule, "saveSolidDatasetAt", ); const mockedAcr = mockAcr("https://some.pod/resource"); const mockedResource = addMockAcrTo( mockSolidDatasetFrom("https://some.pod/resource"), mockedAcr, ); await saveAcrFor(mockedResource, { fetch: mockFetch, }); expect(mockedSaveSolidDatasetAt).toHaveBeenCalledTimes(1); expect(mockedSaveSolidDatasetAt).toHaveBeenCalledWith( "https://some.pod/resource?ext=acr", mockedAcr, expect.objectContaining({ fetch: mockFetch }), ); }); it("attaches the saved ACR to the returned Resource", async () => { const mockedSaveSolidDatasetAt = jest.spyOn( SolidDatasetModule, "saveSolidDatasetAt", ); const mockedAcr = mockAcr("https://some.pod/resource"); const mockedResource = addMockAcrTo( mockSolidDatasetFrom("https://some.pod/resource"), mockedAcr, ); const fakeReturnedAcr = { fake: "ACR" } as any; mockedSaveSolidDatasetAt.mockResolvedValueOnce(fakeReturnedAcr); const savedResource = await saveAcrFor(mockedResource); expect(savedResource.internal_acp.acr).toEqual(fakeReturnedAcr); }); }); describe("isAcpControlled", () => { it("returns true if a resource advertizes its linked ACP using an acp:accessControl Link header", async () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); mockedSolidDataset.internal_resourceInfo.linkedResources = { [acp.accessControl]: ["https://arbitrary.pod/resource?ext=acr"], }; const mockedAcr = mockAcr("https://arbitrary.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetResourceInfo.mockResolvedValueOnce(mockedSolidDataset); mockedGetSolidDataset.mockResolvedValueOnce(mockedAcr); await expect(isAcpControlled("https://some.pod/resource")).resolves.toBe( true, ); }); it("returns true if a resource advertizes its linked ACP using an 'acl' Link header", async () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); const acrUrl = "https://arbitrary.pod/resource?ext=acr"; mockedSolidDataset.internal_resourceInfo.linkedResources = { acl: [acrUrl], }; mockedSolidDataset.internal_resourceInfo.aclUrl = acrUrl; const mockedAcr = mockAcr("https://arbitrary.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); mockedAcr.internal_resourceInfo.linkedResources = { type: ["http://www.w3.org/ns/solid/acp#AccessControlResource"], }; const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockedAcr); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo .mockResolvedValueOnce(mockedSolidDataset) .mockResolvedValueOnce(mockedAcr); await expect(isAcpControlled("https://some.pod/resource")).resolves.toBe( true, ); }); it("returns false if a resource advertizes a linked ACL using an 'acl' Link header", async () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); const aclUrl = "https://arbitrary.pod/resource?ext=acl"; mockedSolidDataset.internal_resourceInfo.linkedResources = { acl: [aclUrl], }; mockedSolidDataset.internal_resourceInfo.aclUrl = aclUrl; const mockedAcr = mockAcr("https://arbitrary.pod/resource", { policies: [], memberPolicies: [], acrPolicies: [], memberAcrPolicies: [], }); const mockedGetSolidDataset = jest.spyOn( SolidDatasetModule, "getSolidDataset", ); mockedGetSolidDataset.mockResolvedValueOnce(mockedAcr); const mockedGetResourceInfo = jest.spyOn(ResourceModule, "getResourceInfo"); mockedGetResourceInfo .mockResolvedValueOnce(mockedSolidDataset) .mockResolvedValueOnce(mockedAcr); await expect(isAcpControlled("https://some.pod/resource")).resolves.toBe( false, ); }); }); describe("getLinkedAcrUrl", () => { it("returns the IRI of an ACR linked with the ACP vocab predicate", () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); const acrUrl = "https://arbitrary.pod/resource?ext=acl"; mockedSolidDataset.internal_resourceInfo.linkedResources = { [acp.accessControl]: [acrUrl], }; expect(getLinkedAcrUrl(mockedSolidDataset)).toBe(acrUrl); }); it("returns the IRI of an ACR linked with the 'acl' link rel", () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); const acrUrl = "https://arbitrary.pod/resource?ext=acl"; mockedSolidDataset.internal_resourceInfo.linkedResources = { acl: [acrUrl], }; expect(getLinkedAcrUrl(mockedSolidDataset)).toBe(acrUrl); }); it("returns undefined if no ACR is linked", () => { const mockedSolidDataset = mockSolidDatasetFrom( "https://arbitrary.pod/resource", ); expect(getLinkedAcrUrl(mockedSolidDataset)).toBeUndefined(); }); it("returns undefined if the given resource has no server information", () => { expect( getLinkedAcrUrl(undefined as unknown as WithServerResourceInfo), ).toBeUndefined(); }); }); it("re-exports util functions", () => { expect(setVcAccess).toBeDefined(); expect(getVcAccess).toBeDefined(); });