@inrupt/solid-client
Version:
Make your web apps work with Solid Pods.
942 lines (843 loc) • 33.9 kB
text/typescript
/**
* 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";
import { foaf, schema } from "rdf-namespaces";
import { Session } from "@inrupt/solid-client-authn-node";
import { config } from "dotenv-flow";
import {
getSolidDataset,
setThing,
getThing,
getStringNoLocale,
setDatetime,
setStringNoLocale,
setTerm,
saveSolidDatasetAt,
overwriteFile,
isRawData,
getContentType,
getResourceInfoWithAcl,
getSolidDatasetWithAcl,
hasResourceAcl,
getPublicAccess,
getAgentAccess,
getFallbackAcl,
getResourceAcl,
getAgentResourceAccess,
setAgentResourceAccess,
saveAclFor,
hasFallbackAcl,
hasAccessibleAcl,
createAclFromFallbackAcl,
getPublicDefaultAccess,
getPublicResourceAccess,
getFile,
getSourceUrl,
deleteFile,
createContainerAt,
createContainerInContainer,
getBoolean,
setBoolean,
createThing,
createSolidDataset,
deleteSolidDataset,
UrlString,
acp_v3 as acp,
FetchError,
} from "../index";
// Functions from this module have to be imported from the module directly,
// because their names overlap with access system-specific versions,
// and therefore aren't exported from the package root:
import {
getAgentAccess as getAgentAccessUniversal,
getPublicAccess as getPublicAccessUniversal,
setPublicAccess as setPublicAccessUniversal,
} from "../access/universal";
import openidClient from "openid-client";
import { blankNode } from "@rdfjs/dataset";
// This block of end-to-end tests should be removed once solid-client-authn-node works against NSS,
// and the other `describe` block has credentials for an NSS server:
describe.each([
["https://lit-e2e-test.inrupt.net/public/"],
// Since ESS switched to ACPs we no longer have a convenient way to prepare the tests data
// with the proper permissions (i.e. public-read-write).
// Therefore, end-to-end tests against ESS have been disabled for now.
// We can re-enable them once we have a Node library with which we can authenticate,
// after which we can set the relevant permissions in the tests themselves:
// ["https://ldp.demo-ess.inrupt.com/105177326598249077653/test-data/"],
])(
"End-to-end tests with pre-existing data against resources in [%s]:",
(rootContainer) => {
it("should be able to read and update data in a Pod", async () => {
const randomNick = "Random nick " + Math.random();
const dataset = await getSolidDataset(`${rootContainer}lit-pod-test.ttl`);
const existingThing = getThing(
dataset,
`${rootContainer}lit-pod-test.ttl#thing1`
);
if (existingThing === null) {
throw new Error(
`The test data did not look like we expected it to. Check whether [${rootContainer}lit-pod-test.ttl#thing1] exists.`
);
}
expect(getStringNoLocale(existingThing, foaf.name)).toBe(
"Thing for first end-to-end test"
);
let updatedThing = setDatetime(
existingThing,
schema.dateModified,
new Date()
);
updatedThing = setStringNoLocale(updatedThing, foaf.nick, randomNick);
const updatedDataset = setThing(dataset, updatedThing);
const savedDataset = await saveSolidDatasetAt(
`${rootContainer}lit-pod-test.ttl`,
updatedDataset
);
const savedThing = getThing(
savedDataset,
`${rootContainer}lit-pod-test.ttl#thing1`
);
expect(savedThing).not.toBeNull();
expect(getStringNoLocale(savedThing!, foaf.name)).toBe(
"Thing for first end-to-end test"
);
expect(getStringNoLocale(savedThing!, foaf.nick)).toBe(randomNick);
});
it("can read and write booleans", async () => {
const dataset = await getSolidDataset(`${rootContainer}lit-pod-test.ttl`);
const existingThing = getThing(
dataset,
`${rootContainer}lit-pod-test.ttl#thing2`
);
if (existingThing === null) {
throw new Error(
`The test data did not look like we expected it to. Check whether [${rootContainer}lit-pod-test.ttl#thing2] exists.`
);
}
const currentValue = getBoolean(
existingThing,
"https://example.com/boolean"
);
const updatedThing = setBoolean(
existingThing,
"https://example.com/boolean",
!currentValue
);
const updatedDataset = setThing(dataset, updatedThing);
const savedDataset = await saveSolidDatasetAt(
`${rootContainer}lit-pod-test.ttl`,
updatedDataset
);
const savedThing = getThing(
savedDataset,
`${rootContainer}lit-pod-test.ttl#thing2`
);
expect(savedThing).not.toBeNull();
expect(getBoolean(savedThing!, "https://example.com/boolean")).toBe(
!currentValue
);
});
it("can differentiate between RDF and non-RDF Resources", async () => {
const rdfResourceInfo = await getResourceInfoWithAcl(
`${rootContainer}lit-pod-resource-info-test/litdataset.ttl`
);
const nonRdfResourceInfo = await getResourceInfoWithAcl(
`${rootContainer}lit-pod-resource-info-test/not-a-litdataset.png`
);
expect(isRawData(rdfResourceInfo)).toBe(false);
expect(isRawData(nonRdfResourceInfo)).toBe(true);
});
it("can create and remove empty Containers", async () => {
const newContainer1 = await createContainerAt(
`${rootContainer}container-test/some-container/`
);
const newContainer2 = await createContainerInContainer(
"https://lit-e2e-test.inrupt.net/public/container-test/",
{ slugSuggestion: "some-other-container" }
);
expect(getSourceUrl(newContainer1)).toBe(
`${rootContainer}container-test/some-container/`
);
await deleteFile(`${rootContainer}container-test/some-container/`);
await deleteFile(getSourceUrl(newContainer2));
});
it("should be able to read and update ACLs", async () => {
const fakeWebId =
"https://example.com/fake-webid#" +
Date.now().toString() +
Math.random().toString();
const datasetWithAcl = await getSolidDatasetWithAcl(
`${rootContainer}lit-pod-acl-test/passthrough-container/resource-with-acl.ttl`
);
const datasetWithoutAcl = await getSolidDatasetWithAcl(
`${rootContainer}lit-pod-acl-test/passthrough-container/resource-without-acl.ttl`
);
expect(hasResourceAcl(datasetWithAcl)).toBe(true);
expect(hasResourceAcl(datasetWithoutAcl)).toBe(false);
expect(getPublicAccess(datasetWithAcl)).toEqual({
read: true,
append: true,
write: true,
control: true,
});
expect(
getAgentAccess(
datasetWithAcl,
"https://vincentt.inrupt.net/profile/card#me"
)
).toEqual({
read: false,
append: true,
write: false,
control: false,
});
expect(
getAgentAccess(
datasetWithoutAcl,
"https://vincentt.inrupt.net/profile/card#me"
)
).toEqual({
read: true,
append: false,
write: false,
control: false,
});
const fallbackAclForDatasetWithoutAcl = getFallbackAcl(datasetWithoutAcl);
expect(fallbackAclForDatasetWithoutAcl?.internal_accessTo).toBe(
`${rootContainer}lit-pod-acl-test/`
);
if (!hasResourceAcl(datasetWithAcl)) {
throw new Error(
`The Resource at ${rootContainer}lit-pod-acl-test/passthrough-container/resource-with-acl.ttl does not seem to have an ACL. The end-to-end tests do expect it to have one.`
);
}
const acl = getResourceAcl(datasetWithAcl);
const updatedAcl = setAgentResourceAccess(acl, fakeWebId, {
read: true,
append: false,
write: false,
control: false,
});
const savedAcl = await saveAclFor(datasetWithAcl, updatedAcl);
const fakeWebIdAccess = getAgentResourceAccess(savedAcl, fakeWebId);
expect(fakeWebIdAccess).toEqual({
read: true,
append: false,
write: false,
control: false,
});
// Cleanup
const cleanedAcl = setAgentResourceAccess(savedAcl, fakeWebId, {
read: false,
append: false,
write: false,
control: false,
});
await saveAclFor(datasetWithAcl, cleanedAcl);
});
it("can copy default rules from the fallback ACL as Resource rules to a new ACL", async () => {
const dataset = await getSolidDatasetWithAcl(
`${rootContainer}lit-pod-acl-initialisation-test/resource.ttl`
);
if (
!hasFallbackAcl(dataset) ||
!hasAccessibleAcl(dataset) ||
hasResourceAcl(dataset)
) {
throw new Error(
`The Resource at ${rootContainer}lit-pod-acl-initialisation-test/resource.ttl appears to not have an accessible fallback ACL, or it already has an ACL, which the end-to-end tests do not expect.`
);
}
const newResourceAcl = createAclFromFallbackAcl(dataset);
const existingFallbackAcl = getFallbackAcl(dataset);
expect(getPublicDefaultAccess(existingFallbackAcl)).toEqual(
getPublicResourceAccess(newResourceAcl)
);
});
it("can fetch a non-RDF file and its metadata", async () => {
const jsonFile = await getFile(`${rootContainer}arbitrary.json`);
expect(getContentType(jsonFile)).toEqual("application/json");
const data = JSON.parse(await jsonFile.text());
expect(data).toEqual({ arbitrary: "json data" });
});
}
);
// Load environment variables from .env.test.local if available:
config({
path: __dirname,
// In CI, actual environment variables will overwrite values from .env files.
// We don't need warning messages in the logs for that:
silent: process.env.CI === "true",
});
type OidcIssuer = string;
type ClientId = string;
type ClientSecret = string;
type RefreshToken = string;
type Pod = string;
type AuthDetails = [Pod, OidcIssuer, ClientId, ClientSecret, RefreshToken];
// Instructions for obtaining these credentials can be found here:
// https://github.com/inrupt/solid-client-authn-js/blob/1a97ef79057941d8ac4dc328fff18333eaaeb5d1/packages/node/example/bootstrappedApp/README.md
const serversUnderTest: AuthDetails[] = [
// pod.inrupt.com:
[
// Cumbersome workaround, but:
// Trim `https://` from the start of these URLs,
// so that GitHub Actions doesn't replace them with *** in the logs.
process.env.E2E_TEST_ESS_POD!.replace(/^https:\/\//, ""),
process.env.E2E_TEST_ESS_IDP_URL!.replace(/^https:\/\//, ""),
process.env.E2E_TEST_ESS_CLIENT_ID!,
process.env.E2E_TEST_ESS_CLIENT_SECRET!,
process.env.E2E_TEST_ESS_REFRESH_TOKEN!,
],
// pod-compat.inrupt.com:
[
// Cumbersome workaround, but:
// Trim `https://` from the start of these URLs,
// so that GitHub Actions doesn't replace them with *** in the logs.
process.env.E2E_TEST_ESS_COMPAT_POD!.replace(/^https:\/\//, ""),
process.env.E2E_TEST_ESS_COMPAT_IDP_URL!.replace(/^https:\/\//, ""),
process.env.E2E_TEST_ESS_COMPAT_CLIENT_ID!,
process.env.E2E_TEST_ESS_COMPAT_CLIENT_SECRET!,
process.env.E2E_TEST_ESS_COMPAT_REFRESH_TOKEN!,
],
// inrupt.net
// Unfortunately we cannot authenticate against Node Solid Server yet, due to this issue:
// https://github.com/solid/node-solid-server/issues/1533
// Once that is fixed, credentials can be added here, and the other `describe()` can be removed.
];
describe.each(serversUnderTest)(
"Authenticated end-to-end tests against Pod [%s] and OIDC Issuer [%s]:",
(rootContainer, oidcIssuer, clientId, clientSecret, refreshToken) => {
// Re-add `https://` at the start of these URLs, which we trimmed above
// so that GitHub Actions doesn't replace them with *** in the logs.
rootContainer = "https://" + rootContainer;
oidcIssuer = "https://" + oidcIssuer;
function supportsWac() {
return (
rootContainer.includes("pod-compat.inrupt.com") ||
rootContainer.includes("inrupt.net") ||
rootContainer.includes("solidcommunity.net")
);
}
function supportsAcps() {
return rootContainer.includes("pod.inrupt.com");
}
async function getSession() {
const session = new Session();
await session.login({
oidcIssuer: oidcIssuer,
clientId: clientId,
clientName: "Solid Client End-2-End Test Client App - Node.js",
clientSecret: clientSecret,
refreshToken: refreshToken,
});
return session;
}
if (rootContainer.includes("pod-compat.inrupt.com")) {
// pod-compat.inrupt.com seems to be experiencing some slowdowns processing POST requests,
// so temporarily increase the timeouts for it:
jest.setTimeout(30000);
openidClient.custom.setHttpOptionsDefaults({ timeout: 5000 });
}
it("can create, read, update and delete data", async () => {
const session = await getSession();
const arbitraryPredicate = "https://arbitrary.vocab/predicate";
let newThing = createThing({ name: "e2e-test-thing" });
newThing = setBoolean(newThing, arbitraryPredicate, true);
let newDataset = createSolidDataset();
newDataset = setThing(newDataset, newThing);
const datasetUrl = `${rootContainer}solid-client-tests/node/crud-dataset-${session.info.sessionId}.ttl`;
await saveSolidDatasetAt(datasetUrl, newDataset, {
fetch: session.fetch,
});
const firstSavedDataset = await getSolidDataset(datasetUrl, {
fetch: session.fetch,
});
const firstSavedThing = getThing(
firstSavedDataset,
datasetUrl + "#e2e-test-thing"
)!;
expect(firstSavedThing).not.toBeNull();
expect(getBoolean(firstSavedThing, arbitraryPredicate)).toBe(true);
const updatedThing = setBoolean(
firstSavedThing,
arbitraryPredicate,
false
);
const updatedDataset = setThing(firstSavedDataset, updatedThing);
await saveSolidDatasetAt(datasetUrl, updatedDataset, {
fetch: session.fetch,
});
const secondSavedDataset = await getSolidDataset(datasetUrl, {
fetch: session.fetch,
});
const secondSavedThing = getThing(
secondSavedDataset,
datasetUrl + "#e2e-test-thing"
)!;
expect(secondSavedThing).not.toBeNull();
expect(getBoolean(secondSavedThing, arbitraryPredicate)).toBe(false);
await deleteSolidDataset(datasetUrl, { fetch: session.fetch });
await expect(() =>
getSolidDataset(datasetUrl, { fetch: session.fetch })
).rejects.toEqual(
expect.objectContaining({
statusCode: 404,
})
);
});
it("can create, delete, and differentiate between RDF and non-RDF Resources", async () => {
const session = await getSession();
const datasetUrl = `${rootContainer}solid-client-tests/node/dataset-${session.info.sessionId}.ttl`;
const fileUrl = `${rootContainer}solid-client-tests/node/file-${session.info.sessionId}.txt`;
const sentFile = await overwriteFile(fileUrl, Buffer.from("test"), {
fetch: session.fetch,
});
const sentDataset = await saveSolidDatasetAt(
datasetUrl,
createSolidDataset(),
{ fetch: session.fetch }
);
expect(isRawData(sentDataset)).toBe(false);
expect(isRawData(sentFile)).toBe(true);
await deleteSolidDataset(datasetUrl, { fetch: session.fetch });
await deleteFile(fileUrl, { fetch: session.fetch });
});
it("can create and remove Containers", async () => {
const session = await getSession();
const containerUrl = `${rootContainer}solid-client-tests/node/container-test/container1-${session.info.sessionId}/`;
const containerContainerUrl = `${rootContainer}solid-client-tests/node/container-test/`;
const containerName = `container2-${session.info.sessionId}`;
const newContainer1 = await createContainerAt(containerUrl, {
fetch: session.fetch,
});
const newContainer2 = await createContainerInContainer(
containerContainerUrl,
{ slugSuggestion: containerName, fetch: session.fetch }
);
expect(getSourceUrl(newContainer1)).toBe(containerUrl);
expect(getSourceUrl(newContainer2)).toBe(
`${containerContainerUrl}${containerName}/`
);
await deleteFile(containerUrl, { fetch: session.fetch });
await deleteFile(getSourceUrl(newContainer2), { fetch: session.fetch });
});
it("can read and update ACLs", async () => {
if (!supportsWac()) {
// pod.inrupt.com does not support WAC, so skip this test there.
return;
}
const session = await getSession();
const fakeWebId =
"https://example.com/fake-webid#" + session.info.sessionId;
const datasetWithoutAclUrl = `${rootContainer}solid-client-tests/node/acl-test-${session.info.sessionId}.ttl`;
await saveSolidDatasetAt(datasetWithoutAclUrl, createSolidDataset(), {
fetch: session.fetch,
});
const datasetWithAcl = await getSolidDatasetWithAcl(rootContainer, {
fetch: session.fetch,
});
const datasetWithoutAcl = await getSolidDatasetWithAcl(
datasetWithoutAclUrl,
{ fetch: session.fetch }
);
expect(hasResourceAcl(datasetWithAcl)).toBe(true);
expect(hasResourceAcl(datasetWithoutAcl)).toBe(false);
expect(getPublicAccess(datasetWithAcl)).toEqual({
read: false,
append: false,
write: false,
control: false,
});
expect(getAgentAccess(datasetWithAcl, session.info.webId!)).toEqual({
read: true,
append: true,
write: true,
control: true,
});
expect(getAgentAccess(datasetWithoutAcl, session.info.webId!)).toEqual({
read: true,
append: true,
write: true,
control: true,
});
const fallbackAclForDatasetWithoutAcl = getFallbackAcl(datasetWithoutAcl);
expect(fallbackAclForDatasetWithoutAcl?.internal_accessTo).toBe(
rootContainer
);
if (!hasResourceAcl(datasetWithAcl)) {
throw new Error(
`The Resource at [${rootContainer}] does not seem to have an ACL. The end-to-end tests do expect it to have one.`
);
}
const acl = getResourceAcl(datasetWithAcl);
const updatedAcl = setAgentResourceAccess(acl, fakeWebId, {
read: true,
append: false,
write: false,
control: false,
});
const sentAcl = await saveAclFor(datasetWithAcl, updatedAcl, {
fetch: session.fetch,
});
const fakeWebIdAccess = getAgentResourceAccess(sentAcl, fakeWebId);
expect(fakeWebIdAccess).toEqual({
read: true,
append: false,
write: false,
control: false,
});
// Cleanup
const cleanedAcl = setAgentResourceAccess(sentAcl, fakeWebId, {
read: false,
append: false,
write: false,
control: false,
});
await saveAclFor(datasetWithAcl, cleanedAcl, { fetch: session.fetch });
await deleteSolidDataset(datasetWithoutAclUrl, { fetch: session.fetch });
});
it("can update Things containing Blank Nodes in different instances of the same SolidDataset", async () => {
const session = await getSession();
const regularPredicate = "https://arbitrary.vocab/regular-predicate";
const blankNodePredicate = "https://arbitrary.vocab/blank-node-predicate";
// Prepare the Resource on the Pod
let newThing = createThing({ name: "e2e-test-thing-with-blank-node" });
newThing = setBoolean(newThing, regularPredicate, true);
newThing = setTerm(newThing, blankNodePredicate, blankNode());
let newDataset = createSolidDataset();
newDataset = setThing(newDataset, newThing);
const datasetUrl = `${rootContainer}solid-client-tests/node/blank-node-updates-${session.info.sessionId}`;
try {
await saveSolidDatasetAt(datasetUrl, newDataset, {
fetch: session.fetch,
});
// Fetch the initialised SolidDataset for the first time,
// and change the non-blank node value:
const initialisedDataset = await getSolidDataset(datasetUrl, {
fetch: session.fetch,
});
const initialisedThing = getThing(
initialisedDataset,
datasetUrl + "#e2e-test-thing-with-blank-node"
)!;
const updatedThing = setBoolean(
initialisedThing,
regularPredicate,
false
);
// Now fetch the Resource again, and try to insert the updated Thing into it:
const refetchedDataset = await getSolidDataset(datasetUrl, {
fetch: session.fetch,
});
const updatedDataset = setThing(refetchedDataset, updatedThing);
await expect(
saveSolidDatasetAt(datasetUrl, updatedDataset, {
fetch: session.fetch,
})
).resolves.not.toThrow();
} finally {
// Clean up after ourselves
await deleteSolidDataset(datasetUrl, { fetch: session.fetch });
}
});
describe("Access Control Policies", () => {
if (
rootContainer.includes("inrupt.net") ||
rootContainer.includes("pod-compat.inrupt.com")
) {
// These servers do not support Access Control Policies,
// so ACP tests can be skipped for them:
return;
}
async function initialisePolicyResource(
policyResourceUrl: UrlString,
session: Session
) {
let publicRule = acp.createRule(policyResourceUrl + "#rule-public");
publicRule = acp.setPublic(publicRule);
let publicReadPolicy = acp.createPolicy(
policyResourceUrl + "#policy-publicRead"
);
// Note: we should think of a better name for "optional", as this isn't really optional.
// At least one "optional" rule should apply, and since this is the only rule for this
// policy, it will in practice be required.
publicReadPolicy = acp.addAnyOfRuleUrl(publicReadPolicy, publicRule);
publicReadPolicy = acp.setAllowModes(publicReadPolicy, {
read: true,
append: false,
write: false,
});
let selfRule = acp.createRule(policyResourceUrl + "#rule-self");
selfRule = acp.addAgent(selfRule, session.info.webId!);
// This policy denies write access to the current user,
// but allows write access so the Resource can still be removed afterwards:
let selfWriteNoReadPolicy = acp.createPolicy(
policyResourceUrl + "#policy-selfWriteNoRead"
);
selfWriteNoReadPolicy = acp.addAllOfRuleUrl(
selfWriteNoReadPolicy,
selfRule
);
selfWriteNoReadPolicy = acp.setAllowModes(selfWriteNoReadPolicy, {
read: false,
append: true,
write: true,
});
selfWriteNoReadPolicy = acp.setDenyModes(selfWriteNoReadPolicy, {
read: true,
append: false,
write: false,
});
let policyResource = createSolidDataset();
policyResource = setThing(policyResource, publicRule);
policyResource = setThing(policyResource, publicReadPolicy);
policyResource = setThing(policyResource, selfRule);
policyResource = setThing(policyResource, selfWriteNoReadPolicy);
return saveSolidDatasetAt(policyResourceUrl, policyResource, {
fetch: session.fetch,
});
}
async function applyPolicyToPolicyResource(
resourceUrl: UrlString,
policyUrl: UrlString,
session: Session
) {
const resourceWithAcr = await acp.getSolidDatasetWithAcr(resourceUrl, {
fetch: session.fetch,
});
if (!acp.hasAccessibleAcr(resourceWithAcr)) {
throw new Error(
`The test Resource at [${getSourceUrl(
resourceWithAcr
)}] does not appear to have a readable Access Control Resource. Please check the Pod setup.`
);
}
const changedResourceWithAcr = acp.addPolicyUrl(
resourceWithAcr,
policyUrl
);
return acp.saveAcrFor(changedResourceWithAcr, {
fetch: session.fetch,
});
}
it("can deny Read access", async () => {
const session = await getSession();
const policyResourceUrl =
rootContainer +
`solid-client-tests/node/acp/policy-deny-agent-read-${session.info.sessionId}.ttl`;
// Create a Resource containing Access Policies and Rules:
await initialisePolicyResource(policyResourceUrl, session);
// Verify that we can fetch the Resource before Denying Read access:
await expect(
getSolidDataset(policyResourceUrl, { fetch: session.fetch })
).resolves.not.toBeNull();
// In the Resource's Access Control Resource, apply the Policy
// that just so happens to be defined in the Resource itself,
// and that denies Read access to the current user:
await applyPolicyToPolicyResource(
policyResourceUrl,
policyResourceUrl + "#policy-selfWriteNoRead",
session
);
// Verify that indeed, the current user can no longer read it:
await expect(
getSolidDataset(policyResourceUrl, { fetch: session.fetch })
).rejects.toThrow(
// Forbidden:
expect.objectContaining({ statusCode: 403 }) as FetchError
);
// Clean up:
await deleteSolidDataset(policyResourceUrl, { fetch: session.fetch });
});
it("can allow public Read access", async () => {
const session = await getSession();
const policyResourceUrl =
rootContainer +
`solid-client-tests/node/acp/policy-allow-public-read-${session.info.sessionId}.ttl`;
// Create a Resource containing Access Policies and Rules:
await initialisePolicyResource(policyResourceUrl, session);
// Verify that we cannot fetch the Resource before adding public Read access
// when not logged in (i.e. not passing the session's fetch):
await expect(getSolidDataset(policyResourceUrl)).rejects.toThrow(
// Unauthorised:
expect.objectContaining({ statusCode: 401 }) as FetchError
);
// In the Resource's Access Control Resource, apply the Policy
// that just so happens to be defined in the Resource itself,
// and provides Read access to the public:
await applyPolicyToPolicyResource(
policyResourceUrl,
policyResourceUrl + "#policy-publicRead",
session
);
// Verify that indeed, an unauthenticated user can now read it:
await expect(
getSolidDataset(policyResourceUrl)
).resolves.not.toBeNull();
// Clean up:
await deleteSolidDataset(policyResourceUrl, { fetch: session.fetch });
});
it("can set Access from a Resource's ACR", async () => {
const session = await getSession();
const resourceUrl =
rootContainer +
`solid-client-tests/node/acp/resource-policies-and-rules-${session.info.sessionId}.ttl`;
await overwriteFile(resourceUrl, Buffer.from("To-be-public Resource"), {
fetch: session.fetch,
});
const resourceInfoWithAcr = await acp.getResourceInfoWithAcr(
resourceUrl,
{ fetch: session.fetch }
);
if (!acp.hasAccessibleAcr(resourceInfoWithAcr)) {
throw new Error(
`The end-to-end tests expect the end-to-end test user to be able to access Access Control Resources, but the ACR of [${resourceUrl}] was not accessible.`
);
}
let publicRule = acp.createResourceRuleFor(
resourceInfoWithAcr,
"publicRule"
);
publicRule = acp.setPublic(publicRule);
let publicReadPolicy = acp.createResourcePolicyFor(
resourceInfoWithAcr,
"publicReadPolicy"
);
publicReadPolicy = acp.addAllOfRuleUrl(publicReadPolicy, publicRule);
publicReadPolicy = acp.setAllowModes(publicReadPolicy, {
read: true,
append: false,
write: false,
});
let updatedResourceInfoWithAcr = acp.setResourceRule(
resourceInfoWithAcr,
publicRule
);
updatedResourceInfoWithAcr = acp.setResourcePolicy(
updatedResourceInfoWithAcr,
publicReadPolicy
);
// Verify that we cannot fetch the Resource before adding public Read access
// when not logged in (i.e. not passing the session's fetch):
await expect(getFile(resourceUrl)).rejects.toThrow(
// Unauthorised:
expect.objectContaining({ statusCode: 401 }) as FetchError
);
await acp.saveAcrFor(updatedResourceInfoWithAcr, {
fetch: session.fetch,
});
// Verify that indeed, an unauthenticated user can now read it:
await expect(getFile(resourceUrl)).resolves.not.toBeNull();
// Clean up:
await deleteFile(resourceUrl, { fetch: session.fetch });
});
});
describe("Wrapper Access API's", () => {
it("can read the user's access to their profile with WAC", async () => {
if (!supportsWac()) {
return;
}
const session = await getSession();
const webId = session.info.webId!;
const agentAccess = await getAgentAccessUniversal(webId, webId, {
fetch: session.fetch,
});
expect(agentAccess).toStrictEqual({
read: true,
append: true,
write: true,
controlRead: true,
controlWrite: true,
});
});
it("can read and change access", async () => {
const session = await getSession();
const datasetUrl = `${rootContainer}solid-client-tests/node/access-wrapper/access-test-${session.info.sessionId}.ttl`;
await saveSolidDatasetAt(datasetUrl, createSolidDataset(), {
fetch: session.fetch,
});
// Fetching it unauthenticated (i.e. without passing session.fetch):
await expect(getSolidDataset(datasetUrl)).rejects.toThrow();
await expect(
getPublicAccessUniversal(datasetUrl, { fetch: session.fetch })
).resolves.toStrictEqual({
read: false,
append: false,
write: false,
controlRead: false,
controlWrite: false,
});
const publicAccess = await setPublicAccessUniversal(
datasetUrl,
{ read: true },
{ fetch: session.fetch }
);
expect(publicAccess).toStrictEqual({
read: true,
append: false,
write: false,
controlRead: false,
controlWrite: false,
});
// Fetching it unauthenticated again (i.e. without passing session.fetch):
const publicDataset = await getSolidDataset(datasetUrl);
expect(publicDataset).not.toBeNull();
await expect(
getPublicAccessUniversal(datasetUrl, { fetch: session.fetch })
).resolves.toStrictEqual({
read: true,
append: false,
write: false,
controlRead: false,
controlWrite: false,
});
await deleteSolidDataset(datasetUrl, {
fetch: session.fetch,
});
});
it("throws an error when trying to set different values for controlRead and controlWrite on a WAC-powered Pod", async () => {
if (!supportsWac()) {
return false;
}
const session = await getSession();
const datasetUrl = `${rootContainer}solid-client-tests/node/access-wrapper/different-control-values-${session.info.sessionId}.ttl`;
await saveSolidDatasetAt(datasetUrl, createSolidDataset(), {
fetch: session.fetch,
});
await expect(
setPublicAccessUniversal(
datasetUrl,
{ controlRead: true, controlWrite: false },
{ fetch: session.fetch }
)
).rejects.toThrow(
`When setting access for a Resource in a Pod implementing Web Access Control (i.e. [${datasetUrl}]), ` +
"`controlRead` and `controlWrite` should have the same value."
);
await deleteSolidDataset(datasetUrl, {
fetch: session.fetch,
});
});
});
}
);