UNPKG

@inrupt/solid-client

Version:

Make your web apps work with Solid Pods.

1,882 lines (1,711 loc) • 99.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 type { IriString, SolidDataset, WithServerResourceInfo, } from "../interfaces"; import type { AgentAccess } from "../acl/agent"; import { getAgentResourceAccess } from "../acl/agent"; import type { WacAccess } from "./wac"; import { getAgentAccess, getAgentAccessAll, getGroupAccess, getGroupAccessAll, getPublicAccess, setAgentResourceAccess, setGroupResourceAccess, setPublicResourceAccess, } from "./wac"; import { triplesToTurtle } from "../formats/turtle"; import { addMockAclRuleQuads, setMockAclUrl } from "../acl/mock.internal"; import { acl, foaf } from "../constants"; import { mockSolidDatasetFrom } from "../resource/mock"; import { internal_getResourceAcl } from "../acl/acl.internal"; import type { AclDataset } from "../acl/acl"; import { getGroupResourceAccess } from "../acl/group"; import { getPublicResourceAccess } from "../acl/class"; import { toRdfJsQuads } from "../rdfjs.internal"; import { mockResponse } from "../tests.internal"; jest.spyOn(globalThis, "fetch").mockImplementation( async () => new Response(undefined, { headers: { Location: "https://arbitrary.pod/resource" }, }), ); function getMockDataset( sourceIri: IriString, aclIri?: IriString, ): SolidDataset & WithServerResourceInfo { const result = mockSolidDatasetFrom(sourceIri); if (aclIri === undefined) { return result; } return setMockAclUrl(result, aclIri); } const mockedResource = getMockDataset( "https://some.pod/resource", "https://some.pod/resource.acl", ); describe("getAgentAccess", () => { it("calls the included fetcher by default", async () => { await getAgentAccess(mockedResource, "https://some.pod/profile#agent"); expect(fetch).toHaveBeenCalledWith( "https://some.pod/resource.acl", expect.anything(), ); }); it("returns null if no ACL is accessible", async () => { const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: async (url) => { switch (url.toString()) { case "https://some.pod/resource.acl": case "https://some.pod/.acl": return new Response("", { status: 404 }); case "https://some.pod/": return new Response('<.acl>; rel="acl"', { status: 200 }); default: throw new Error("Unepxected URL"); } }, }, ); await expect(result).resolves.toBeNull(); }); it("returns null if no ACL is advertised by the target resource", async () => { const resource = getMockDataset("https://some.pod/resource"); const result = getAgentAccess(resource, "https://some.pod/profile#agent", { fetch: async (url) => { if (url.toString() === "https://some.pod/resource.acl") { return new Response("ACL not found", { status: 404 }); } throw new Error("Unepxected URL"); }, }); await expect(result).resolves.toBeNull(); }); it("fetches the resource ACL if available", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: async (url) => { if (url.toString() === "https://some.pod/resource.acl") { return Object.defineProperty( new Response(await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }), "url", { value: url.toString() }, ); } throw new Error("Unepxected URL"); }, }, ); await expect(result).resolves.toStrictEqual({ read: true, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("fetches the fallback ACL if no resource ACL is available", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/.acl"), "https://some.pod/profile#agent", "https://some.pod/", { read: true, append: false, write: false, control: false }, "default", ); const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/.acl", ), ); const resource = getMockDataset( "https://some.pod/resource", "https://some.pod/resource.acl", ); const result = getAgentAccess(resource, "https://some.pod/profile#agent", { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ read: true, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("ignores the fallback ACL if the resource ACL is available", async () => { const fallbackAclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/.acl"), "https://some.pod/profile#agent", "https://some.pod/", { read: false, append: true, write: false, control: false }, "default", ); const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", ); const resource = getMockDataset( "https://some.pod/resource", "https://some.pod/resource.acl", ); const result = getAgentAccess(resource, "https://some.pod/profile#agent", { fetch: async (url) => { let response: Response | undefined; switch (url.toString()) { case "https://some.pod/resource.acl": response = new Response( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" } }, ); break; case "https://some.pod/.acl": response = new Response( await triplesToTurtle(toRdfJsQuads(fallbackAclResource)), { status: 200, headers: { "Content-Type": "text/turtle" } }, ); break; case "https://some.pod/": response = new Response('<.acl>; rel="acl"', { status: 200 }); break; default: throw new Error("Unepxected URL"); } if (!response) { throw new Error("Unepxected URL"); } return Object.defineProperty(response, "url", { value: url.toString(), }); }, }); await expect(result).resolves.toStrictEqual({ append: false, read: true, write: false, controlRead: false, controlWrite: false, }); }); it("returns true for both controlRead and controlWrite if the Agent has control access", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: false, append: false, write: false, control: true }, "resource", ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: true, controlWrite: true, }); }); it("correctly reads the Agent append access", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: false, append: true, write: false, control: false }, "resource", ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: true, write: false, controlRead: false, controlWrite: false, }); }); it("correctly reads the Agent write access, which implies append", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: false, append: false, write: true, control: false }, "resource", ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: true, write: true, controlRead: false, controlWrite: false, }); }); it("returns false for all modes the Agent isn't present", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#another-agent", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for groups", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentGroup, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for everyone", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), foaf.Agent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for authenticated agents", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), acl.AuthenticatedAgent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccess( mockedResource, "https://some.pod/profile#agent", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); }); describe("getGroupAccess", () => { it("calls the included fetcher by default", async () => { await getGroupAccess(mockedResource, "https://some.pod/groups#group"); expect(fetch).toHaveBeenCalledWith( "https://some.pod/resource.acl", expect.anything(), ); }); it("returns null if no ACL is accessible", async () => { const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toBeNull(); }); it("returns null if no ACL is advertised by the target resource", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "ACL not found", { status: 404, }, "https://some.pod/resource.acl", ), ); const resource = getMockDataset("https://some.pod/resource"); const result = getAgentAccess(resource, "https://some.pod/groups#group", { fetch: mockFetch, }); await expect(result).resolves.toBeNull(); }); it("fetches the resource ACL if available", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/groups#group", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentGroup, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: true, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("fetches the fallback ACL if no resource ACL is available", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/.acl"), "https://some.pod/groups#group", "https://some.pod/", { read: true, append: false, write: false, control: false }, "default", "https://some.pod/resource.acl#some-rule", acl.agentGroup, ); const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: true, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("ignores the fallback ACL if the resource ACL is available", async () => { const fallbackAclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/.acl"), "https://some.pod/groups#group", "https://some.pod/", { read: true, append: false, write: false, control: false }, "default", "https://some.pod/resource.acl#some-rule", acl.agentGroup, ); const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/groups#group", "https://some.pod/resource", { read: false, append: true, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentGroup, ); const mockFetch = jest .fn<typeof fetch>() // The resource ACL is available... .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(fallbackAclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ append: true, read: false, write: false, controlRead: false, controlWrite: false, }); }); it("returns an empty object if the group isn't present", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/groups#another-group", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentGroup, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for agents", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/groups#group", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agent, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for everyone", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), foaf.Agent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for authenticated agents", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), acl.AuthenticatedAgent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getGroupAccess( mockedResource, "https://some.pod/groups#group", { fetch: mockFetch, }, ); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); }); describe("getPublicAccess", () => { it("calls the included fetcher by default", async () => { await getPublicAccess(mockedResource); expect(fetch).toHaveBeenCalledWith( "https://some.pod/resource.acl", expect.anything(), ); }); it("returns null if no ACL is accessible", async () => { const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toBeNull(); }); it("returns null if no ACL is advertised by the target resource", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "ACL not found", { status: 404, }, "https://some.pod/resource.acl", ), ); const resource = getMockDataset("https://some.pod/resource"); const result = getPublicAccess(resource, { fetch: mockFetch, }); await expect(result).resolves.toBeNull(); }); it("fetches the resource ACL if available", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), foaf.Agent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ read: true, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("fetches the fallback ACL if no resource ACL is available", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/.acl"), foaf.Agent, "https://some.pod/", { read: true, append: false, write: false, control: false }, "default", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ read: true, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("ignores the fallback ACL if the resource ACL is available", async () => { const fallbackAclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/.acl"), foaf.Agent, "https://some.pod/", { read: true, append: false, write: false, control: false }, "default", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), foaf.Agent, "https://some.pod/resource", { read: false, append: true, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest .fn<typeof fetch>() // The resource ACL is available... .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( await triplesToTurtle(toRdfJsQuads(fallbackAclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ append: true, read: false, write: false, controlRead: false, controlWrite: false, }); }); it("returns an empty object if no public access is specified", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), foaf.Agent, "https://some.pod/resource", { read: false, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for agents", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), foaf.Agent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agent, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for groups", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), foaf.Agent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentGroup, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); it("does not return access for authenticated agents", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), acl.AuthenticatedAgent, "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#some-rule", acl.agentClass, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getPublicAccess(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ read: false, append: false, write: false, controlRead: false, controlWrite: false, }); }); }); describe("getAgentAccessAll", () => { it("uses the default fetcher if none is provided", async () => { await getAgentAccessAll(mockedResource); expect(fetch).toHaveBeenCalledWith( "https://some.pod/resource.acl", expect.anything(), ); }); it("returns null if the advertized ACL isn't accessible", async () => { const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/.acl", ), ); const result = getAgentAccessAll(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toBeNull(); }); it("returns null if no ACL is advertised by the resource", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "ACL not found", { status: 404, }, "https://some.pod/resource.acl", ), ); const resource = getMockDataset("https://some.pod/resource"); const result = getAgentAccessAll(resource, { fetch: mockFetch, }); await expect(result).resolves.toBeNull(); }); it("calls the underlying getAgentAccessAll", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "", { status: 200, }, "https://some.pod/resource.acl", ), ); const wacModule = jest.requireActual("../acl/agent") as { getAgentAccessAll: () => Promise<AgentAccess>; }; const getAgentAccessAllWac = jest.spyOn(wacModule, "getAgentAccessAll"); await getAgentAccessAll(mockedResource, { fetch: mockFetch, }); expect(getAgentAccessAllWac).toHaveBeenCalled(); }); it("returns an empty list if the ACL defines no access", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "", { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); await expect( getAgentAccessAll(mockedResource, { fetch: mockFetch, }), ).resolves.toStrictEqual({}); }); it("returns the access set for all the actors present", async () => { let aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent-a", "https://some.pod/resource", { read: false, append: false, write: true, control: false }, "resource", ); aclResource = addMockAclRuleQuads( aclResource, "https://some.pod/profile#agent-b", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); await expect( getAgentAccessAll(mockedResource, { fetch: mockFetch, }), ).resolves.toStrictEqual({ "https://some.pod/profile#agent-a": { read: false, append: true, write: true, controlRead: false, controlWrite: false, }, "https://some.pod/profile#agent-b": { read: true, append: false, write: false, controlRead: false, controlWrite: false, }, }); }); it("returns true for both controlRead and controlWrite if an Agent has control access", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: false, append: false, write: false, control: true }, "resource", ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getAgentAccessAll(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ "https://some.pod/profile#agent": { read: false, append: false, write: false, controlRead: true, controlWrite: true, }, }); }); }); describe("getGroupAccessAll", () => { it("uses the default fetcher if none is provided", async () => { await getGroupAccessAll(mockedResource); expect(fetch).toHaveBeenCalledWith( "https://some.pod/resource.acl", expect.anything(), ); }); it("returns null if the advertized ACL isn't accessible", async () => { const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/.acl", ), ); const result = getGroupAccessAll(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toBeNull(); }); it("returns null if no ACL is advertised by the resource", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "ACL not found", { status: 404, }, "https://some.pod/resource.acl", ), ); const resource = getMockDataset("https://some.pod/resource"); const result = getGroupAccessAll(resource, { fetch: mockFetch, }); await expect(result).resolves.toBeNull(); }); it("calls the underlying getGroupAccessAll", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "", { status: 200, }, "https://some.pod/resource.acl", ), ); const wacModule = jest.requireActual("../acl/group") as { getGroupAccessAll: () => Promise<AgentAccess>; }; const getGroupAccessAllWac = jest.spyOn(wacModule, "getGroupAccessAll"); await getGroupAccessAll(mockedResource, { fetch: mockFetch, }); expect(getGroupAccessAllWac).toHaveBeenCalled(); }); it("returns an empty list if the ACL defines no access", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "", { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); await expect( getGroupAccessAll(mockedResource, { fetch: mockFetch, }), ).resolves.toStrictEqual({}); }); it("returns the access set for all the actors present", async () => { let aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/groups#group-a", "https://some.pod/resource", { read: false, append: false, write: true, control: false }, "resource", "https://some.pod/resource.acl#rule-a", acl.agentGroup, ); aclResource = addMockAclRuleQuads( aclResource, "https://some.pod/groups#group-b", "https://some.pod/resource", { read: true, append: false, write: false, control: false }, "resource", "https://some.pod/resource.acl#rule-b", acl.agentGroup, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); await expect( getGroupAccessAll(mockedResource, { fetch: mockFetch, }), ).resolves.toStrictEqual({ "https://some.pod/groups#group-a": { read: false, append: true, write: true, controlRead: false, controlWrite: false, }, "https://some.pod/groups#group-b": { read: true, append: false, write: false, controlRead: false, controlWrite: false, }, }); }); it("returns true for both controlRead and controlWrite if a Group has control access", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/groups#group", "https://some.pod/resource", { read: false, append: false, write: false, control: true }, "resource", "https://some.pod/resource.acl#rule", acl.agentGroup, ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = getGroupAccessAll(mockedResource, { fetch: mockFetch, }); await expect(result).resolves.toStrictEqual({ "https://some.pod/groups#group": { read: false, append: false, write: false, controlRead: true, controlWrite: true, }, }); }); }); describe("setAgentAccess", () => { it("calls the included fetcher by default", async () => { await setAgentResourceAccess( mockedResource, "https://some.pod/profile#agent", { read: true, append: undefined, write: undefined, controlRead: undefined, controlWrite: undefined, }, ); expect(fetch).toHaveBeenCalledWith( "https://some.pod/resource.acl", expect.anything(), ); }); it("returns null if no ACL is accessible", async () => { const mockFetch = jest .fn<typeof fetch>() // No resource ACL available... .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/resource.acl", ), ) // Link to the fallback ACL... .mockResolvedValueOnce( mockResponse( "", { status: 200, headers: { Link: '<.acl>; rel="acl"', }, }, "https://some.pod/", ), ) // Get the fallback ACL .mockResolvedValueOnce( mockResponse( "", { status: 404, }, "https://some.pod/.acl", ), ); const result = setAgentResourceAccess( mockedResource, "https://some.pod/profile#agent", { read: true, append: undefined, write: undefined, controlRead: undefined, controlWrite: undefined, }, { fetch: mockFetch, }, ); await expect(result).resolves.toBeNull(); }); it("returns null if no ACL is advertised by the target resource", async () => { const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( "ACL not found", { status: 404, }, "https://some.pod/resource.acl", ), ); const resource = getMockDataset("https://some.pod/resource"); const result = setAgentResourceAccess( resource, "https://some.pod/profile#agent", { read: true, append: undefined, write: undefined, controlRead: undefined, controlWrite: undefined, }, { fetch: mockFetch, }, ); await expect(result).resolves.toBeNull(); }); it("sets read access in the resource ACL if available", async () => { const aclResource = addMockAclRuleQuads( getMockDataset("https://some.pod/resource.acl"), "https://some.pod/profile#agent", "https://some.pod/resource", { read: false, append: false, write: false, control: false }, "resource", ); const mockFetch = jest.fn<typeof fetch>().mockResolvedValue( mockResponse( await triplesToTurtle(toRdfJsQuads(aclResource)), { status: 200, headers: { "Content-Type": "text/turtle" }, }, "https://some.pod/resource.acl", ), ); const result = await setAgentResourceAccess( mockedResource, "https://some.pod/profile#agent", { read: true, }, { fetch: mockFetch, }, ); const newAccess = getAgentResourceAccess( internal_getResourceAcl(result!), "https://some.p