UNPKG

@inrupt/solid-client

Version:
942 lines (843 loc) • 33.9 kB
/** * Copyright 2020 Inrupt Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the * Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { jest, describe, it, expect } from "@jest/globals"; 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, }); }); }); } );