From e5b7d99da4f60fb84ce0111fd4bf405b41d4f170 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 15 Feb 2021 12:09:18 +0100 Subject: [PATCH] test: Add subdomain integration tests --- test/assets/templates/.acl | 25 ++- test/integration/PodCreation.test.ts | 5 +- test/integration/Subdomains.test.ts | 165 ++++++++++++++++++ .../config/server-subdomains-unsafe.json | 87 +++++++++ 4 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 test/integration/Subdomains.test.ts create mode 100644 test/integration/config/server-subdomains-unsafe.json diff --git a/test/assets/templates/.acl b/test/assets/templates/.acl index f5072ada9..86d27703c 100644 --- a/test/assets/templates/.acl +++ b/test/assets/templates/.acl @@ -1,3 +1,26 @@ +# Root ACL resource for the agent account @prefix acl: . +@prefix foaf: . -<#owner> acl:agent <{{webId}}>. +# The homepage is readable by the public +<#public> + a acl:Authorization; + acl:agentClass foaf:Agent; + acl:accessTo ; + acl:mode acl:Read. + +# The owner has full access to every resource in their pod. +# Other agents have no access rights, +# unless specifically authorized in other .acl resources. +<#owner> + a acl:Authorization; + acl:agent <{{webId}}>; + # Optional owner email, to be used for account recovery: + {{#if email}}acl:agent ;{{/if}} + # Set the access to the root storage folder itself + acl:accessTo ; + # All resources will inherit this authorization, by default + acl:default ; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. diff --git a/test/integration/PodCreation.test.ts b/test/integration/PodCreation.test.ts index 69461dd92..520f69f6c 100644 --- a/test/integration/PodCreation.test.ts +++ b/test/integration/PodCreation.test.ts @@ -51,10 +51,7 @@ describe('A server with a pod handler', (): void => { res = await fetch(`${pod}.acl`); expect(res.status).toBe(200); let body = await readableToString(res.body as any); - expect(body).toBe(`@prefix acl: . - -<#owner> acl:agent <${agent.webId}>. -`); + expect(body).toContain(`acl:agent <${agent.webId}>`); res = await fetch(`${pod}profile/card`); expect(res.status).toBe(200); diff --git a/test/integration/Subdomains.test.ts b/test/integration/Subdomains.test.ts new file mode 100644 index 000000000..528e02f7d --- /dev/null +++ b/test/integration/Subdomains.test.ts @@ -0,0 +1,165 @@ +import type { Server } from 'http'; +import fetch from 'cross-fetch'; +import type { Initializer } from '../../src/init/Initializer'; +import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; +import type { ResourceStore } from '../../src/storage/ResourceStore'; +import { joinFilePath } from '../../src/util/PathUtil'; +import { getTestFolder, instantiateFromConfig, removeFolder } from './Config'; + +const port = 6005; +const baseUrl = `http://localhost:${port}/`; + +const rootFilePath = getTestFolder('subdomains'); +const stores: [string, any][] = [ + [ 'in-memory storage', { + storeUrn: 'urn:solid-server:default:MemoryResourceStore', + teardown: jest.fn(), + }], + [ 'on-disk storage', { + storeUrn: 'urn:solid-server:default:FileResourceStore', + teardown: (): void => removeFolder(rootFilePath), + }], +]; + +// Simulating subdomains using the forwarded header so no DNS changes are required +describe.each(stores)('A subdomain server with %s', (name, { storeUrn, teardown }): void => { + let server: Server; + let initializer: Initializer; + let factory: HttpServerFactory; + const agent = { login: 'alice', webId: 'http://test.com/#alice', name: 'Alice Bob' }; + const podHost = `alice.localhost:${port}`; + const podUrl = `http://${podHost}/`; + + beforeAll(async(): Promise => { + const variables: Record = { + 'urn:solid-server:default:variable:baseUrl': baseUrl, + 'urn:solid-server:default:variable:port': port, + 'urn:solid-server:default:variable:rootFilePath': rootFilePath, + 'urn:solid-server:default:variable:podTemplateFolder': joinFilePath(__dirname, '../assets/templates'), + }; + const internalStore = await instantiateFromConfig( + storeUrn, + 'server-subdomains-unsafe.json', + variables, + ) as ResourceStore; + variables['urn:solid-server:default:variable:store'] = internalStore; + + // Create and initialize the HTTP handler and related components + const instances = await instantiateFromConfig( + 'urn:solid-server:test:Instances', + 'server-subdomains-unsafe.json', + variables, + ) as Record; + ({ factory, initializer } = instances); + + // Set up the internal store + await initializer.handleSafe(); + + server = factory.startServer(port); + }); + + afterAll(async(): Promise => { + await new Promise((resolve, reject): void => { + server.close((error): void => error ? reject(error) : resolve()); + }); + await teardown(); + }); + + describe('handling resources', (): void => { + it('can read the root container.', async(): Promise => { + const res = await fetch(baseUrl); + expect(res.status).toBe(200); + }); + + it('can write resources.', async(): Promise => { + let res = await fetch(`${baseUrl}alice`, { + method: 'PUT', + headers: { + authorization: `WebID ${agent.webId}`, + 'content-type': 'text/plain', + }, + body: 'this is new data!', + }); + expect(res.status).toBe(205); + + res = await fetch(`${baseUrl}alice`); + expect(res.status).toBe(200); + await expect(res.text()).resolves.toBe('this is new data!'); + }); + }); + + describe('handling pods', (): void => { + it('creates pods in a subdomain.', async(): Promise => { + const res = await fetch(`${baseUrl}pods`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(agent), + }); + expect(res.status).toBe(201); + expect(res.headers.get('location')).toBe(podUrl); + }); + + it('can fetch the created pod in a subdomain.', async(): Promise => { + const res = await fetch(baseUrl, { headers: { forwarded: `host=${podHost}` }}); + expect(res.status).toBe(200); + }); + + it('should not be able to read the acl file.', async(): Promise => { + const res = await fetch(`${baseUrl}.acl`, { headers: { forwarded: `host=${podHost}` }}); + expect(res.status).toBe(401); + }); + + it('should be able to read acl file with the correct credentials.', async(): Promise => { + const res = await fetch(`${baseUrl}.acl`, { + headers: { + forwarded: `host=${podHost}`, + authorization: `WebID ${agent.webId}`, + }, + }); + expect(res.status).toBe(200); + }); + + it('should be able to write to the pod now as the owner.', async(): Promise => { + let res = await fetch(`${baseUrl}alice`, { + headers: { + forwarded: `host=${podHost}`, + authorization: `WebID ${agent.webId}`, + }, + }); + expect(res.status).toBe(404); + + res = await fetch(`${baseUrl}alice`, { + method: 'PUT', + headers: { + forwarded: `host=${podHost}`, + authorization: `WebID ${agent.webId}`, + 'content-type': 'text/plain', + }, + body: 'this is new data!', + }); + expect(res.status).toBe(205); + + res = await fetch(`${baseUrl}alice`, { + headers: { + forwarded: `host=${podHost}`, + authorization: `WebID ${agent.webId}`, + }, + }); + expect(res.status).toBe(200); + await expect(res.text()).resolves.toBe('this is new data!'); + }); + + it('should not be able to create a pod with the same name.', async(): Promise => { + const res = await fetch(`${baseUrl}pods`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(agent), + }); + expect(res.status).toBe(409); + }); + }); +}); diff --git a/test/integration/config/server-subdomains-unsafe.json b/test/integration/config/server-subdomains-unsafe.json new file mode 100644 index 000000000..3c41c74dc --- /dev/null +++ b/test/integration/config/server-subdomains-unsafe.json @@ -0,0 +1,87 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^0.0.0/components/context.jsonld", + "import": [ + "files-scs:config/presets/acl.json", + "files-scs:config/presets/http.json", + "files-scs:config/presets/identifiers/subdomain-identifiers.json", + "files-scs:config/presets/init.json", + "files-scs:config/presets/ldp/metadata-handler.json", + "files-scs:config/presets/ldp/operation-handler.json", + "files-scs:config/presets/ldp/permissions-extractor.json", + "files-scs:config/presets/ldp/response-writer.json", + "files-scs:config/presets/ldp/request-parser.json", + "files-scs:config/presets/ldp/websockets.json", + "files-scs:config/presets/middleware.json", + "files-scs:config/presets/pod-management.json", + "files-scs:config/presets/representation-conversion.json", + "files-scs:config/presets/static.json", + "files-scs:config/presets/storage/backend/storage-memory.json", + "files-scs:config/presets/storage/backend/storage-filesystem-subdomains.json", + "files-scs:config/presets/storage-wrapper.json", + "files-scs:config/presets/cli-params.json" + ], + "@graph": [ + { + "comment": "Main changes from default are the subdomain parts and the unsafe UnsecureWebIdExtractor", + "@id": "urn:solid-server:test:Instances", + "@type": "RecordObject", + "RecordObject:_record": [ + { + "RecordObject:_record_key": "initializer", + "RecordObject:_record_value": { + "@type": "SequenceHandler", + "SequenceHandler:_handlers": [ + { "@id": "urn:solid-server:default:RootContainerInitializer" }, + { "@id": "urn:solid-server:default:AclInitializer" } + ] + } + }, + { + "RecordObject:_record_key": "factory", + "RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" } + } + ] + }, + { + "@id": "urn:solid-server:default:LdpHandler", + "@type": "AuthenticatedLdpHandler", + "AuthenticatedLdpHandler:_args_requestParser": { + "@id": "urn:solid-server:default:RequestParser" + }, + "AuthenticatedLdpHandler:_args_credentialsExtractor": { + "@type": "WaterfallHandler", + "WaterfallHandler:_handlers": [ + { + "@type": "UnsecureWebIdExtractor" + }, + { + "@type": "EmptyCredentialsExtractor" + } + ] + }, + "AuthenticatedLdpHandler:_args_permissionsExtractor": { + "@id": "urn:solid-server:default:PermissionsExtractor" + }, + "AuthenticatedLdpHandler:_args_authorizer": { + "@id": "urn:solid-server:default:AclBasedAuthorizer" + }, + "AuthenticatedLdpHandler:_args_operationHandler": { + "@id": "urn:solid-server:default:OperationHandler" + }, + "AuthenticatedLdpHandler:_args_responseWriter": { + "@id": "urn:solid-server:default:ResponseWriter" + } + }, + { + "@id": "urn:solid-server:default:RoutingResourceStore", + "@type": "PassthroughStore", + "PassthroughStore:_source": { + "@id": "urn:solid-server:default:variable:store" + } + }, + { + "@id": "urn:solid-server:default:variable:store", + "@type": "Variable" + } + ] +}