UNPKG

@inrupt/solid-client

Version:
1,588 lines (1,426 loc) • 94.4 kB
/** * Copyright 2020 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"; jest.mock("../fetcher.ts", () => ({ fetch: jest.fn().mockImplementation(() => Promise.resolve( new Response(undefined, { headers: { Location: "https://arbitrary.pod/resource" }, }) ) ), })); import { Response } from "cross-fetch"; import { dataset } from "@rdfjs/dataset"; import { DataFactory } from "n3"; import { getResourceAcl, getFallbackAcl, createAclFromFallbackAcl, saveAclFor, deleteAclFor, createAcl, hasAcl, getSolidDatasetWithAcl, getFileWithAcl, getResourceInfoWithAcl, WithAccessibleAcl, AclRule, AclDataset, Access, WithAcl, } from "./acl"; import { internal_getAccess, internal_getAclRules, internal_getResourceAclRules, internal_getDefaultAclRules, internal_getResourceAclRulesForResource, internal_getDefaultAclRulesForResource, internal_combineAccessModes, internal_removeEmptyAclRules, internal_fetchResourceAcl, internal_fetchFallbackAcl, internal_getContainerPath, internal_fetchAcl, internal_setAcl, } from "./acl.internal"; import { WithServerResourceInfo, ThingPersisted } from "../interfaces"; import { getFile } from "../resource/file"; import { mockSolidDatasetFrom } from "../resource/mock"; import { getMatchingQuads } from "../rdfjs.test"; function mockResponse( body?: BodyInit | null, init?: ResponseInit & { url: string } ): Response { return new Response(body, init); } describe("fetchAcl", () => { it("calls the included fetcher by default", async () => { const mockedFetcher = jest.requireMock("../fetcher.ts") as { fetch: jest.Mock< ReturnType<typeof window.fetch>, [RequestInfo, RequestInit?] >; }; const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"], }, }, }; await internal_fetchAcl(mockResourceInfo); expect(mockedFetcher.fetch.mock.calls[0][0]).toEqual( "https://some.pod/resource.acl" ); }); it("does not attempt to fetch ACLs if the fetched Resource does not include a pointer to an ACL file, and sets an appropriate default value.", async () => { const mockFetch = jest.fn(window.fetch); const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, linkedResources: {}, }, }; const fetchedAcl = await internal_fetchAcl(mockResourceInfo, { fetch: mockFetch, }); expect(mockFetch.mock.calls).toHaveLength(0); expect(fetchedAcl.resourceAcl).toBeNull(); expect(fetchedAcl.fallbackAcl).toBeNull(); }); it("returns null for the Container ACL if the Container's ACL file could not be fetched", async () => { const mockFetch = jest.fn((url) => { const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: "" } : undefined; return Promise.resolve( mockResponse(undefined, { headers: headers, url: url as string, }) ); }); const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"], }, }, }; const fetchedAcl = await internal_fetchAcl(mockResourceInfo, { fetch: mockFetch, }); expect(fetchedAcl).not.toBeNull(); expect(fetchedAcl.fallbackAcl).toBeNull(); expect(fetchedAcl.resourceAcl?.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource.acl" ); }); it("returns the fallback ACL even if the Resource's own ACL could not be found", async () => { const mockFetch = jest.fn((url) => { if (url === "https://some.pod/resource.acl") { return Promise.resolve( mockResponse("ACL not found", { status: 404, url: "https://some.pod/resource.acl", }) ); } const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: '<.acl>; rel="acl"' } : undefined; return Promise.resolve( mockResponse(undefined, { headers: headers, url: url as string, }) ); }); const mockResourceInfo: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"], }, }, }; const fetchedAcl = await internal_fetchAcl(mockResourceInfo, { fetch: mockFetch, }); expect(fetchedAcl.resourceAcl).toBeNull(); expect(fetchedAcl.fallbackAcl?.internal_resourceInfo.sourceIri).toBe( "https://some.pod/.acl" ); }); }); describe("fetchResourceAcl", () => { it("returns the fetched ACL SolidDataset", async () => { const sourceDataset: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"], }, }, }; const mockFetch = jest .fn(window.fetch) .mockReturnValueOnce( Promise.resolve( mockResponse(undefined, { url: "https://some.pod/resource.acl" }) ) ); const fetchedAcl = await internal_fetchResourceAcl(sourceDataset, { fetch: mockFetch, }); expect(fetchedAcl?.internal_accessTo).toBe("https://some.pod/resource"); expect(fetchedAcl?.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource.acl" ); expect(mockFetch.mock.calls).toHaveLength(1); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource.acl"); }); it("calls the included fetcher by default", async () => { const sourceDataset: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"], }, }, }; const mockedFetcher = jest.requireMock("../fetcher.ts") as { fetch: jest.Mock< ReturnType<typeof window.fetch>, [RequestInfo, RequestInit?] >; }; await internal_fetchResourceAcl(sourceDataset); expect(mockedFetcher.fetch.mock.calls[0][0]).toEqual( "https://some.pod/resource.acl" ); }); it("returns null if the source SolidDataset has no known ACL IRI", async () => { const sourceDataset: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: false, linkedResources: {}, }, }; const fetchedAcl = await internal_fetchResourceAcl(sourceDataset); expect(fetchedAcl).toBeNull(); }); it("returns null if the ACL was not found", async () => { const sourceDataset: WithServerResourceInfo = { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: false, aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://some.pod/resource.acl"], }, }, }; const mockFetch = jest.fn(window.fetch).mockReturnValueOnce( Promise.resolve( mockResponse("ACL not found", { status: 404, url: "https://some.pod/resource.acl", }) ) ); const fetchedAcl = await internal_fetchResourceAcl(sourceDataset, { fetch: mockFetch, }); expect(fetchedAcl).toBeNull(); expect(mockFetch.mock.calls).toHaveLength(1); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource.acl"); }); }); describe("fetchFallbackAcl", () => { it("returns the parent Container's ACL SolidDataset, if present", async () => { const sourceDataset = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, // If no ACL IRI is given, the user does not have Control Access, // in which case we wouldn't be able to reliably determine the effective ACL. // Hence, the function requires the given SolidDataset to have one known: aclUrl: "https://arbitrary.pod/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/resource.acl"], }, }, }; const mockFetch = jest.fn(window.fetch).mockReturnValueOnce( Promise.resolve( mockResponse("", { headers: { Link: '<.acl>; rel="acl"', }, url: "https://some.pod/", }) ) ); mockFetch.mockReturnValueOnce( Promise.resolve(mockResponse(undefined, { url: "https://some.pod/.acl" })) ); const fetchedAcl = await internal_fetchFallbackAcl(sourceDataset, { fetch: mockFetch, }); expect(fetchedAcl?.internal_accessTo).toBe("https://some.pod/"); expect(fetchedAcl?.internal_resourceInfo.sourceIri).toBe( "https://some.pod/.acl" ); expect(mockFetch.mock.calls).toHaveLength(2); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/.acl"); }); it("calls the included fetcher by default", async () => { const sourceDataset = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, aclUrl: "https://some.pod/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/resource.acl"], }, }, }; const mockedFetcher = jest.requireMock("../fetcher.ts") as { fetch: jest.Mock< ReturnType<typeof window.fetch>, [RequestInfo, RequestInit?] >; }; await internal_fetchFallbackAcl(sourceDataset); expect(mockedFetcher.fetch.mock.calls).toHaveLength(1); expect(mockedFetcher.fetch.mock.calls[0][0]).toBe("https://some.pod/"); }); it("travels up multiple levels if no ACL was found on the levels in between", async () => { const sourceDataset = { internal_resourceInfo: { sourceIri: "https://some.pod/with-acl/without-acl/resource", isRawData: false, // If no ACL IRI is given, the user does not have Control Access, // in which case we wouldn't be able to reliably determine the effective ACL. // Hence, the function requires the given SolidDataset to have one known: aclUrl: "https://arbitrary.pod/with-acl/without-acl/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/resource.acl"], }, }, }; const mockFetch = jest.fn(window.fetch).mockReturnValueOnce( Promise.resolve( mockResponse("", { headers: { Link: '<.acl>; rel="acl"', }, url: "https://some.pod/with-acl/without-acl/", }) ) ); mockFetch.mockReturnValueOnce( Promise.resolve( mockResponse("ACL not found", { status: 404, url: "https://some.pod/with-acl/without-acl/.acl", }) ) ); mockFetch.mockReturnValueOnce( Promise.resolve( mockResponse("", { headers: { Link: '<.acl>; rel="acl"', }, url: "https://some.pod/with-acl/", }) ) ); mockFetch.mockReturnValueOnce( Promise.resolve( mockResponse(undefined, { url: "https://some.pod/with-acl/.acl" }) ) ); const fetchedAcl = await internal_fetchFallbackAcl(sourceDataset, { fetch: mockFetch, }); expect(fetchedAcl?.internal_accessTo).toBe("https://some.pod/with-acl/"); expect(fetchedAcl?.internal_resourceInfo.sourceIri).toBe( "https://some.pod/with-acl/.acl" ); expect(mockFetch.mock.calls).toHaveLength(4); expect(mockFetch.mock.calls[0][0]).toBe( "https://some.pod/with-acl/without-acl/" ); expect(mockFetch.mock.calls[1][0]).toBe( "https://some.pod/with-acl/without-acl/.acl" ); expect(mockFetch.mock.calls[2][0]).toBe("https://some.pod/with-acl/"); expect(mockFetch.mock.calls[3][0]).toBe("https://some.pod/with-acl/.acl"); }); // This happens if the user does not have Control access to that Container, in which case we will // not be able to determine the effective ACL: it("returns null if one of the Containers on the way up does not advertise an ACL", async () => { const sourceDataset = { internal_resourceInfo: { sourceIri: "https://some.pod/arbitrary-parent/no-control-access/resource", isRawData: false, // If no ACL IRI is given, the user does not have Control Access, // in which case we wouldn't be able to reliably determine the effective ACL. // Hence, the function requires the given SolidDataset to have one known: aclUrl: "https://arbitrary.pod/arbitrary-parent/no-control-access/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/resource.acl"], }, }, }; const mockFetch = jest.fn(window.fetch).mockReturnValueOnce( Promise.resolve( mockResponse(undefined, { url: "https://some.pod/arbitrary-parent/no-control-access/", }) ) ); const fetchedAcl = await internal_fetchFallbackAcl(sourceDataset, { fetch: mockFetch, }); expect(fetchedAcl).toBeNull(); expect(mockFetch.mock.calls).toHaveLength(1); expect(mockFetch.mock.calls[0][0]).toBe( "https://some.pod/arbitrary-parent/no-control-access/" ); }); it("returns null if no ACL could be found for the Containers up to the root of the Pod", async () => { const sourceDataset = { internal_resourceInfo: { sourceIri: "https://some.pod/resource", isRawData: false, // If no ACL IRI is given, the user does not have Control Access, // in which case we wouldn't be able to reliably determine the effective ACL. // Hence, the function requires the given SolidDataset to have one known: aclUrl: "https://arbitrary.pod/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/resource.acl"], }, }, }; const mockFetch = jest.fn(window.fetch).mockReturnValueOnce( Promise.resolve( mockResponse("", { headers: { Link: '<.acl>; rel="acl"', }, url: "https://some.pod", }) ) ); mockFetch.mockReturnValueOnce( Promise.resolve( mockResponse("ACL not found", { status: 404, url: "https://some.pod/.acl", }) ) ); const fetchedAcl = await internal_fetchFallbackAcl(sourceDataset, { fetch: mockFetch, }); expect(fetchedAcl).toBeNull(); expect(mockFetch.mock.calls).toHaveLength(2); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/.acl"); }); }); describe("getContainerPath", () => { it("returns the parent if the input is a Resource path", () => { expect(internal_getContainerPath("/container/resource")).toBe( "/container/" ); }); it("returns the parent if the input is a Container path", () => { expect(internal_getContainerPath("/container/child-container/")).toBe( "/container/" ); }); it("returns the root if the input is a child of the root", () => { expect(internal_getContainerPath("/resource")).toBe("/"); }); it("does not prefix a slash if the input did not do so either", () => { expect(internal_getContainerPath("container/resource")).toBe("container/"); }); }); describe("hasAcl", () => { it("returns true if a Resource was fetched with its ACL Resources attached", () => { const withAcl: WithAcl = { internal_acl: { resourceAcl: null, fallbackAcl: null, }, }; expect(hasAcl(withAcl)).toBe(true); }); it("returns false if a Resource was fetched without its ACL Resources attached", () => { const withoutAcl = {}; expect(hasAcl(withoutAcl)).toBe(false); }); }); describe("getSolidDatasetWithAcl", () => { it("returns the Resource's own ACL and not its Container's if available", async () => { const mockFetch = jest.fn((url) => { const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: '<.acl>; rel="acl"' } : undefined; return Promise.resolve( mockResponse(undefined, { headers: headers, url: url as string, }) ); }); const fetchedSolidDataset = await getSolidDatasetWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(fetchedSolidDataset.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource" ); expect( fetchedSolidDataset.internal_acl?.resourceAcl?.internal_resourceInfo .sourceIri ).toBe("https://some.pod/resource.acl"); expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull(); expect(mockFetch.mock.calls).toHaveLength(2); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); }); it("returns the Resource's Container's ACL if its own ACL is not available", async () => { const mockFetch = jest.fn((url) => { if (url === "https://some.pod/resource.acl") { return Promise.resolve(new Response("Not found", { status: 404 })); } const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: '<.acl>; rel="acl"' } : undefined; return Promise.resolve( mockResponse(undefined, { headers: headers, url: url as string, }) ); }); const fetchedSolidDataset = await getSolidDatasetWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(fetchedSolidDataset.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource" ); expect(fetchedSolidDataset.internal_acl?.resourceAcl).toBeNull(); expect( fetchedSolidDataset.internal_acl?.fallbackAcl?.internal_resourceInfo .sourceIri ).toBe("https://some.pod/.acl"); expect(mockFetch.mock.calls).toHaveLength(4); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); expect(mockFetch.mock.calls[2][0]).toBe("https://some.pod/"); expect(mockFetch.mock.calls[3][0]).toBe("https://some.pod/.acl"); }); it("calls the included fetcher by default", async () => { const mockedFetcher = jest.requireMock("../fetcher.ts") as { fetch: jest.Mock< ReturnType<typeof window.fetch>, [RequestInfo, RequestInit?] >; }; await getSolidDatasetWithAcl("https://some.pod/resource"); expect(mockedFetcher.fetch.mock.calls[0][0]).toEqual( "https://some.pod/resource" ); }); it("does not attempt to fetch ACLs if the fetched Resource does not include a pointer to an ACL file, and sets an appropriate default value.", async () => { const mockFetch = jest.fn(window.fetch); mockFetch.mockReturnValueOnce( Promise.resolve( mockResponse(undefined, { headers: { Link: "", }, url: "https://some.pod/resource", }) ) ); const fetchedSolidDataset = await getSolidDatasetWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(mockFetch.mock.calls).toHaveLength(1); expect(fetchedSolidDataset.internal_acl.resourceAcl).toBeNull(); expect(fetchedSolidDataset.internal_acl.fallbackAcl).toBeNull(); }); it("returns a meaningful error when the server returns a 403", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve(new Response("Not allowed", { status: 403 })) ); const fetchPromise = getSolidDatasetWithAcl("https://some.pod/resource", { fetch: mockFetch, }); await expect(fetchPromise).rejects.toThrow( new Error( "Fetching the Resource at [https://some.pod/resource] failed: [403] [Forbidden]." ) ); }); it("returns a meaningful error when the server returns a 404", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve(new Response("Not found", { status: 404 })) ); const fetchPromise = getSolidDatasetWithAcl("https://some.pod/resource", { fetch: mockFetch, }); await expect(fetchPromise).rejects.toThrow( new Error( "Fetching the Resource at [https://some.pod/resource] failed: [404] [Not Found]." ) ); }); it("includes the status code and status message when a request failed", async () => { const mockFetch = jest.fn(window.fetch).mockReturnValue( Promise.resolve( new Response("I'm a teapot!", { status: 418, statusText: "I'm a teapot!", }) ) ); const fetchPromise = getSolidDatasetWithAcl( "https://arbitrary.pod/resource", { fetch: mockFetch, } ); await expect(fetchPromise).rejects.toMatchObject({ statusCode: 418, statusText: "I'm a teapot!", }); }); }); describe("getFileWithAcl", () => { it("should GET a remote resource using the included fetcher if no other fetcher is available", async () => { const fetcher = jest.requireMock("../fetcher") as { fetch: jest.Mock< ReturnType<typeof window.fetch>, [RequestInfo, RequestInit?] >; }; fetcher.fetch.mockReturnValue( Promise.resolve( new Response("Some data", { status: 200, statusText: "OK" }) ) ); await getFileWithAcl("https://some.url"); expect(fetcher.fetch.mock.calls).toEqual([["https://some.url", undefined]]); }); it("should GET a remote resource using the provided fetcher", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve( new Response("Some data", { status: 200, statusText: "OK" }) ) ); const response = await getFileWithAcl("https://some.url", { fetch: mockFetch, }); expect(mockFetch.mock.calls).toEqual([["https://some.url", undefined]]); }); it("should return the fetched data as a blob, along with its ACL", async () => { const init: ResponseInit & { url: string } = { status: 200, statusText: "OK", url: "https://some.url", }; const mockFetch = jest .fn(window.fetch) .mockReturnValue(Promise.resolve(new Response("Some data", init))); const file = await getFileWithAcl("https://some.url", { fetch: mockFetch, }); expect(file.internal_resourceInfo.sourceIri).toEqual("https://some.url"); expect(file.internal_resourceInfo.contentType).toContain("text/plain"); expect(file.internal_resourceInfo.isRawData).toBe(true); const fileData = await file.text(); expect(fileData).toEqual("Some data"); }); it("returns the Resource's own ACL and not its Container's if available", async () => { const mockFetch = jest.fn((url) => { const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: '<.acl>; rel="acl"' } : undefined; const init: ResponseInit & { url: string } = { headers: headers, url: url as string, }; return Promise.resolve(new Response(undefined, init)); }); const fetchedSolidDataset = await getFileWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(fetchedSolidDataset.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource" ); expect( fetchedSolidDataset.internal_acl?.resourceAcl?.internal_resourceInfo .sourceIri ).toBe("https://some.pod/resource.acl"); expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull(); expect(mockFetch.mock.calls).toHaveLength(2); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); }); it("returns the Resource's Container's ACL if its own ACL is not available", async () => { const mockFetch = jest.fn((url) => { if (url === "https://some.pod/resource.acl") { return Promise.resolve(new Response("Not found", { status: 404 })); } const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: '<.acl>; rel="acl"' } : undefined; const init: ResponseInit & { url: string } = { headers: headers, url: url as string, }; return Promise.resolve(new Response(undefined, init)); }); const fetchedSolidDataset = await getFileWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(fetchedSolidDataset.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource" ); expect(fetchedSolidDataset.internal_acl?.resourceAcl).toBeNull(); expect( fetchedSolidDataset.internal_acl?.fallbackAcl?.internal_resourceInfo .sourceIri ).toBe("https://some.pod/.acl"); expect(mockFetch.mock.calls).toHaveLength(4); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); expect(mockFetch.mock.calls[2][0]).toBe("https://some.pod/"); expect(mockFetch.mock.calls[3][0]).toBe("https://some.pod/.acl"); }); it("does not attempt to fetch ACLs if the fetched Resource does not include a pointer to an ACL file, and sets an appropriate default value.", async () => { const mockFetch = jest.fn(window.fetch); const init: ResponseInit & { url: string } = { headers: { Link: "", }, url: "https://some.pod/resource", }; mockFetch.mockReturnValueOnce( Promise.resolve(new Response(undefined, init)) ); const fetchedSolidDataset = await getFileWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(mockFetch.mock.calls).toHaveLength(1); expect(fetchedSolidDataset.internal_acl.resourceAcl).toBeNull(); expect(fetchedSolidDataset.internal_acl.fallbackAcl).toBeNull(); }); it("returns a meaningful error when the server returns a 403", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve(new Response("Not allowed", { status: 403 })) ); const fetchPromise = getFileWithAcl("https://arbitrary.pod/resource", { fetch: mockFetch, }); await expect(fetchPromise).rejects.toThrow( new Error("Fetching the File failed: [403] [Forbidden].") ); }); it("returns a meaningful error when the server returns a 404", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve(new Response("Not found", { status: 404 })) ); const fetchPromise = getFileWithAcl("https://arbitrary.pod/resource", { fetch: mockFetch, }); await expect(fetchPromise).rejects.toThrow( new Error("Fetching the File failed: [404] [Not Found].") ); }); it("includes the status code and status message when a request failed", async () => { const mockFetch = jest.fn(window.fetch).mockReturnValue( Promise.resolve( new Response("I'm a teapot!", { status: 418, statusText: "I'm a teapot!", }) ) ); const fetchPromise = getFileWithAcl("https://arbitrary.pod/resource", { fetch: mockFetch, }); await expect(fetchPromise).rejects.toMatchObject({ statusCode: 418, statusText: "I'm a teapot!", }); }); it("should pass the request headers through", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve( new Response("Some data", { status: 200, statusText: "OK" }) ) ); const response = await getFile("https://some.url", { init: { headers: new Headers({ Accept: "text/turtle" }), }, fetch: mockFetch, }); expect(mockFetch.mock.calls).toEqual([ [ "https://some.url", { headers: new Headers({ Accept: "text/turtle" }), }, ], ]); }); it("should throw on failure", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve( new Response(undefined, { status: 400, statusText: "Bad request" }) ) ); const response = getFile("https://some.url", { fetch: mockFetch, }); await expect(response).rejects.toThrow( "Fetching the File failed: [400] [Bad request]" ); }); }); describe("getResourceInfoWithAcl", () => { it("returns the Resource's own ACL and not its Container's if available", async () => { const mockFetch = jest.fn((url) => { const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: '<.acl>; rel="acl"' } : undefined; return Promise.resolve( mockResponse(undefined, { headers: headers, url: url as string, }) ); }); const fetchedSolidDataset = await getResourceInfoWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(fetchedSolidDataset.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource" ); expect( fetchedSolidDataset.internal_acl?.resourceAcl?.internal_resourceInfo .sourceIri ).toBe("https://some.pod/resource.acl"); expect(fetchedSolidDataset.internal_acl?.fallbackAcl).toBeNull(); expect(mockFetch.mock.calls).toHaveLength(2); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); }); it("returns the Resource's Container's ACL if its own ACL is not available", async () => { const mockFetch = jest.fn((url) => { if (url === "https://some.pod/resource.acl") { return Promise.resolve(new Response("Not found", { status: 404 })); } const headers = url === "https://some.pod/resource" ? { Link: '<resource.acl>; rel="acl"' } : url === "https://some.pod/" ? { Link: '<.acl>; rel="acl"' } : undefined; return Promise.resolve( mockResponse(undefined, { headers: headers, url: url as string, }) ); }); const fetchedSolidDataset = await getResourceInfoWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(fetchedSolidDataset.internal_resourceInfo.sourceIri).toBe( "https://some.pod/resource" ); expect(fetchedSolidDataset.internal_acl?.resourceAcl).toBeNull(); expect( fetchedSolidDataset.internal_acl?.fallbackAcl?.internal_resourceInfo .sourceIri ).toBe("https://some.pod/.acl"); expect(mockFetch.mock.calls).toHaveLength(4); expect(mockFetch.mock.calls[0][0]).toBe("https://some.pod/resource"); expect(mockFetch.mock.calls[1][0]).toBe("https://some.pod/resource.acl"); expect(mockFetch.mock.calls[2][0]).toBe("https://some.pod/"); expect(mockFetch.mock.calls[3][0]).toBe("https://some.pod/.acl"); }); it("calls the included fetcher by default", async () => { const mockedFetcher = jest.requireMock("../fetcher.ts") as { fetch: jest.Mock< ReturnType<typeof window.fetch>, [RequestInfo, RequestInit?] >; }; await getResourceInfoWithAcl("https://some.pod/resource"); expect(mockedFetcher.fetch.mock.calls).toEqual([ [ "https://some.pod/resource", { method: "HEAD", }, ], ]); }); it("does not attempt to fetch ACLs if the fetched Resource does not include a pointer to an ACL file, and sets an appropriate default value.", async () => { const mockFetch = jest.fn(window.fetch); mockFetch.mockReturnValueOnce( Promise.resolve( mockResponse(undefined, { headers: { Link: "", }, url: "https://some.pod/resource", }) ) ); const fetchedSolidDataset = await getResourceInfoWithAcl( "https://some.pod/resource", { fetch: mockFetch } ); expect(mockFetch.mock.calls).toHaveLength(1); expect(fetchedSolidDataset.internal_acl.resourceAcl).toBeNull(); expect(fetchedSolidDataset.internal_acl.fallbackAcl).toBeNull(); }); it("returns a meaningful error when the server returns a 403", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve(new Response("Not allowed", { status: 403 })) ); const fetchPromise = getResourceInfoWithAcl( "https://arbitrary.pod/resource", { fetch: mockFetch, } ); await expect(fetchPromise).rejects.toThrow( new Error( "Fetching the metadata of the Resource at [https://arbitrary.pod/resource] failed: [403] [Forbidden]." ) ); }); it("returns a meaningful error when the server returns a 404", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve(new Response("Not found", { status: 404 })) ); const fetchPromise = getResourceInfoWithAcl( "https://arbitrary.pod/resource", { fetch: mockFetch, } ); await expect(fetchPromise).rejects.toThrow( new Error( "Fetching the metadata of the Resource at [https://arbitrary.pod/resource] failed: [404] [Not Found]." ) ); }); it("includes the status code and status message when a request failed", async () => { const mockFetch = jest.fn(window.fetch).mockReturnValue( Promise.resolve( new Response("I'm a teapot!", { status: 418, statusText: "I'm a teapot!", }) ) ); const fetchPromise = getResourceInfoWithAcl( "https://arbitrary.pod/resource", { fetch: mockFetch, } ); await expect(fetchPromise).rejects.toMatchObject({ statusCode: 418, statusText: "I'm a teapot!", }); }); it("does not request the actual data from the server", async () => { const mockFetch = jest .fn(window.fetch) .mockReturnValue( Promise.resolve( mockResponse(undefined, { url: "https://some.pod/resource" }) ) ); await getResourceInfoWithAcl("https://some.pod/resource", { fetch: mockFetch, }); expect(mockFetch.mock.calls).toEqual([ ["https://some.pod/resource", { method: "HEAD" }], ]); }); }); describe("getResourceAcl", () => { it("returns the attached Resource ACL Dataset", () => { const aclDataset: AclDataset = Object.assign(dataset(), { internal_accessTo: "https://arbitrary.pod/resource", internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource.acl", isRawData: false, linkedResources: {}, }, }); const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: aclDataset, fallbackAcl: null, }), { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: false, aclUrl: "https://arbitrary.pod/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/resource.acl"], }, }, } ); expect(getResourceAcl(solidDataset)).toEqual(aclDataset); }); it("returns null if the given Resource does not consider the attached ACL to pertain to it", () => { const aclDataset: AclDataset = Object.assign(dataset(), { internal_accessTo: "https://arbitrary.pod/resource", internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource.acl", isRawData: false, linkedResources: {}, }, }); const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: aclDataset, fallbackAcl: null, }), { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: false, aclUrl: "https://arbitrary.pod/other-resource.acl", linkedResources: { acl: ["https://arbitrary.pod/other-resource.acl"], }, }, } ); expect(getResourceAcl(solidDataset)).toBeNull(); }); it("returns null if the attached ACL does not pertain to the given Resource", () => { const aclDataset: AclDataset = Object.assign(dataset(), { internal_accessTo: "https://arbitrary.pod/other-resource", internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource.acl", isRawData: false, linkedResources: {}, }, }); const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: aclDataset, fallbackAcl: null, }), { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: false, aclUrl: "https://arbitrary.pod/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/resource.acl"], }, }, } ); expect(getResourceAcl(solidDataset)).toBeNull(); }); it("returns null if the given SolidDataset does not have a Resource ACL attached", () => { const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: null, fallbackAcl: null, }), { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/resource", isRawData: false, linkedResources: {}, }, } ); expect(getResourceAcl(solidDataset)).toBeNull(); }); }); describe("getFallbackAcl", () => { it("returns the attached Fallback ACL Dataset", () => { const aclDataset: AclDataset = Object.assign(dataset(), { internal_accessTo: "https://arbitrary.pod/", internal_resourceInfo: { sourceIri: "https://arbitrary.pod/.acl", isRawData: false, linkedResources: {}, }, }); const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: null, fallbackAcl: aclDataset, }), {} ); expect(getFallbackAcl(solidDataset)).toEqual(aclDataset); }); it("returns null if the given SolidDataset does not have a Fallback ACL attached", () => { const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: null, fallbackAcl: null, }), {} ); expect(getFallbackAcl(solidDataset)).toBeNull(); }); }); describe("createAcl", () => { it("creates a new empty ACL", () => { const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: null, fallbackAcl: null, }), { internal_resourceInfo: { sourceIri: "https://some.pod/container/resource", isRawData: false, aclUrl: "https://some.pod/container/resource.acl", linkedResources: { acl: ["https://some.pod/container/resource.acl"], }, }, } ); const resourceAcl = createAcl(solidDataset); expect(resourceAcl.size).toBe(0); expect(resourceAcl.internal_accessTo).toBe( "https://some.pod/container/resource" ); expect(resourceAcl.internal_resourceInfo.sourceIri).toBe( "https://some.pod/container/resource.acl" ); }); }); describe("createAclFromFallbackAcl", () => { it("creates a new ACL including existing default rules as Resource and default rules", () => { const aclDataset: AclDataset = Object.assign(dataset(), { internal_accessTo: "https://arbitrary.pod/container/", internal_resourceInfo: { sourceIri: "https://arbitrary.pod/container/.acl", isRawData: false, linkedResources: {}, }, }); const subjectIri = "https://arbitrary.pod/container/.acl#" + Math.random(); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode( "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" ), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#Authorization") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#default"), DataFactory.namedNode("https://arbitrary.pod/container/") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agent"), DataFactory.namedNode("https://arbitrary.pod/profileDoc#webId") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#mode"), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#Read") ) ); const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: null, fallbackAcl: aclDataset, }), { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/container/resource", isRawData: false, aclUrl: "https://arbitrary.pod/container/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/container/resource.acl"], }, }, } ); const resourceAcl = createAclFromFallbackAcl(solidDataset); expect(resourceAcl.size).toBe(5); expect( getMatchingQuads(resourceAcl, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/container/resource", }) ).toHaveLength(1); expect( getMatchingQuads(resourceAcl, { predicate: "http://www.w3.org/ns/auth/acl#default", object: "https://arbitrary.pod/container/resource", }) ).toHaveLength(1); expect(resourceAcl.internal_accessTo).toBe( "https://arbitrary.pod/container/resource" ); expect(resourceAcl.internal_resourceInfo.sourceIri).toBe( "https://arbitrary.pod/container/resource.acl" ); }); it("supports the legacy acl:defaultForNew predicate", () => { const aclDataset: AclDataset = Object.assign(dataset(), { internal_accessTo: "https://arbitrary.pod/container/", internal_resourceInfo: { sourceIri: "https://arbitrary.pod/container/.acl", isRawData: false, linkedResources: {}, }, }); const subjectIri = "https://arbitrary.pod/container/.acl#" + Math.random(); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode( "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" ), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#Authorization") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#defaultForNew"), DataFactory.namedNode("https://arbitrary.pod/container/") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agent"), DataFactory.namedNode("https://arbitrary.pod/profileDoc#webId") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#mode"), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#Read") ) ); const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: null, fallbackAcl: aclDataset, }), { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/container/resource", isRawData: false, aclUrl: "https://arbitrary.pod/container/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/container/resource.acl"], }, }, } ); const resourceAcl = createAclFromFallbackAcl(solidDataset); expect(resourceAcl.size).toBe(5); expect( getMatchingQuads(resourceAcl, { predicate: "http://www.w3.org/ns/auth/acl#accessTo", object: "https://arbitrary.pod/container/resource", }) ).toHaveLength(1); expect( getMatchingQuads(resourceAcl, { predicate: "http://www.w3.org/ns/auth/acl#default", object: "https://arbitrary.pod/container/resource", }) ).toHaveLength(1); expect(resourceAcl.internal_accessTo).toBe( "https://arbitrary.pod/container/resource" ); expect(resourceAcl.internal_resourceInfo.sourceIri).toBe( "https://arbitrary.pod/container/resource.acl" ); }); it("does not copy over Resource rules from the fallback ACL", () => { const aclDataset: AclDataset = Object.assign(dataset(), { internal_accessTo: "https://arbitrary.pod/container/", internal_resourceInfo: { sourceIri: "https://arbitrary.pod/container/.acl", isRawData: false, linkedResources: {}, }, }); const subjectIri = "https://arbitrary.pod/container/.acl#" + Math.random(); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode( "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" ), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#Authorization") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#accessTo"), DataFactory.namedNode("https://arbitrary.pod/container/") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#agent"), DataFactory.namedNode("https://arbitrary.pod/profileDoc#webId") ) ); aclDataset.add( DataFactory.quad( DataFactory.namedNode(subjectIri), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#mode"), DataFactory.namedNode("http://www.w3.org/ns/auth/acl#Read") ) ); const solidDataset = Object.assign( internal_setAcl(mockSolidDatasetFrom("https://arbitrary.pod/resource"), { resourceAcl: null, fallbackAcl: aclDataset, }), { internal_resourceInfo: { sourceIri: "https://arbitrary.pod/container/resource", isRawData: false, aclUrl: "https://arbitrary.pod/container/resource.acl", linkedResources: { acl: ["https://arbitrary.pod/container/resource.acl"], }, }, } ); const resourceAcl = createAclFromFallbackAcl(solidDataset); expect(resourceAcl.size).toBe(0); }); }); describe("getAclRules", () => { it("only r