@inrupt/solid-client
Version:
Make your web apps work with Solid Pods.
1,160 lines (1,025 loc) • 39.7 kB
text/typescript
// 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();
});