test: Update LdpHandlerWitAuth to use fetch for tests

The AclHelper class has been extended to be more flexible for future tests.
This commit is contained in:
Joachim Van Herwegen 2021-04-29 16:55:59 +02:00
parent dd3fb63d18
commit 4083d24e4a
6 changed files with 207 additions and 230 deletions

View File

@ -1,8 +1,15 @@
import { createReadStream } from 'fs'; import type { Server } from 'http';
import type { HttpHandler, Initializer, ResourceStore } from '../../src/'; import fetch from 'cross-fetch';
import { LDP, BasicRepresentation, joinFilePath } from '../../src/'; import type { Initializer, ResourceStore } from '../../src/';
import { AclHelper, ResourceHelper } from '../util/TestHelpers'; import { BasicRepresentation } from '../../src/';
import { BASE, getTestFolder, removeFolder, instantiateFromConfig } from './Config'; import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
import { deleteResource, getResource, postResource, putResource } from '../util/FetchUtil';
import { AclHelper } from '../util/TestHelpers';
import { getPort } from '../util/Util';
import { getTestFolder, instantiateFromConfig, removeFolder } from './Config';
const port = getPort('LpdHandlerWithAuth');
const baseUrl = `http://localhost:${port}/`;
const rootFilePath = getTestFolder('full-config-acl'); const rootFilePath = getTestFolder('full-config-acl');
const stores: [string, any][] = [ const stores: [string, any][] = [
@ -17,13 +24,17 @@ const stores: [string, any][] = [
]; ];
describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, teardown }): void => { describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, teardown }): void => {
let handler: HttpHandler; let server: Server;
let initializer: Initializer;
let factory: HttpServerFactory;
let store: ResourceStore;
let aclHelper: AclHelper; let aclHelper: AclHelper;
let resourceHelper: ResourceHelper; const permanent = `${baseUrl}document.txt`;
beforeAll(async(): Promise<void> => { beforeAll(async(): Promise<void> => {
const variables: Record<string, any> = { const variables: Record<string, any> = {
'urn:solid-server:default:variable:baseUrl': BASE, 'urn:solid-server:default:variable:port': port,
'urn:solid-server:default:variable:baseUrl': baseUrl,
'urn:solid-server:default:variable:rootFilePath': rootFilePath, 'urn:solid-server:default:variable:rootFilePath': rootFilePath,
}; };
const internalStore = await instantiateFromConfig( const internalStore = await instantiateFromConfig(
@ -33,128 +44,145 @@ describe.each(stores)('An LDP handler with auth using %s', (name, { storeUrn, te
) as ResourceStore; ) as ResourceStore;
variables['urn:solid-server:default:variable:store'] = internalStore; variables['urn:solid-server:default:variable:store'] = internalStore;
// Create and initialize the HTTP handler and related components // Create and initialize the server
let initializer: Initializer;
let store: ResourceStore;
const instances = await instantiateFromConfig( const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances', 'urn:solid-server:test:Instances',
'ldp-with-auth.json', 'ldp-with-auth.json',
variables, variables,
) as Record<string, any>; ) as Record<string, any>;
({ handler, store, initializer } = instances); ({ factory, initializer, store } = instances);
// Set up the internal store
await initializer.handleSafe(); await initializer.handleSafe();
server = factory.startServer(port);
// Create test helpers for manipulating the components // Create test helper for manipulating acl
aclHelper = new AclHelper(store, BASE); aclHelper = new AclHelper(store);
resourceHelper = new ResourceHelper(handler, BASE); });
// Write test resource beforeEach(async(): Promise<void> => {
await store.setRepresentation({ path: `${BASE}/permanent.txt` }, // Set the root acl file to allow everything and create a single document
new BasicRepresentation(createReadStream(joinFilePath(__dirname, '../assets/permanent.txt')), 'text/plain')); await store.setRepresentation({ path: permanent }, new BasicRepresentation('PERMANENT', 'text/plain'));
await aclHelper.setSimpleAcl(baseUrl, {
permissions: { read: true, write: true, append: true, control: true },
agentClass: 'agent',
accessTo: true,
default: true,
});
}); });
afterAll(async(): Promise<void> => { afterAll(async(): Promise<void> => {
await teardown(); await teardown();
await new Promise((resolve, reject): void => {
server.close((error): void => error ? reject(error) : resolve());
});
}); });
it('can add a file to the store, read it and delete it if allowed.', async(): Promise<void> => { it('can add a document, read it and delete it if allowed.', async(): Promise<void> => {
// Set acl await aclHelper.setSimpleAcl(baseUrl, {
await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent'); permissions: { read: true, write: true, append: true },
agentClass: 'agent',
accessTo: true,
default: true,
});
// Create file // PUT
const filePath = 'testfile2.txt'; const document = `${baseUrl}test.txt`;
const fileUrl = `${BASE}/${filePath}`; await putResource(document, { contentType: 'text/plain', body: 'TESTDATA' });
let response = await resourceHelper.createResource(
'../assets/testfile2.txt', filePath, 'text/plain',
);
// Get file // GET
response = await resourceHelper.getResource(fileUrl); const response = await getResource(document);
expect(response.statusCode).toBe(200); await expect(response.text()).resolves.toBe('TESTDATA');
expect(response._getBuffer().toString()).toContain('TESTFILE2'); expect(response.headers.get('wac-allow')).toBe('user="read write append",public="read write append"');
expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`);
expect(response.getHeaders().link).toContain(`<${fileUrl}.acl>; rel="acl"`);
expect(response.getHeaders()['wac-allow']).toBe('user="read write append",public="read write append"');
// DELETE file // DELETE
await resourceHelper.deleteResource(fileUrl); await deleteResource(document);
await resourceHelper.shouldNotExist(fileUrl);
}); });
it('can not add a file to the store if not allowed.', async(): Promise<void> => { it('can not add a file to the store if not allowed.', async(): Promise<void> => {
// Set acl await aclHelper.setSimpleAcl(baseUrl, {
await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'authenticated'); permissions: { read: true, write: true, append: true },
agentClass: 'authenticated',
accessTo: true,
default: true,
});
// Try to create file // PUT fail
const filePath = 'testfile2.txt'; const documentUrl = `${baseUrl}test.txt`;
const response = await resourceHelper.createResource( const response = await fetch(documentUrl, { method: 'PUT' });
'../assets/testfile2.txt', filePath, 'text/plain', true, expect(response.status).toBe(401);
);
expect(response.statusCode).toBe(401);
}); });
it('can not add/delete, but only read files if allowed.', async(): Promise<void> => { it('can not add/delete if only read is allowed.', async(): Promise<void> => {
// Set acl await aclHelper.setSimpleAcl(baseUrl, {
await aclHelper.setSimpleAcl({ read: true, write: false, append: false, control: false }, 'agent'); permissions: { read: true },
agentClass: 'agent',
accessTo: true,
default: true,
});
// Try to create file // PUT fail
const filePath = 'testfile2.txt'; const document = `${baseUrl}test.txt`;
let response = await resourceHelper.createResource( let response = await fetch(document, { method: 'PUT' });
'../assets/testfile2.txt', filePath, 'text/plain', true, expect(response.status).toBe(401);
);
expect(response.statusCode).toBe(401);
// GET permanent file // GET permanent file
response = await resourceHelper.getResource('http://test.com/permanent.txt'); response = await getResource(permanent);
expect(response._getBuffer().toString()).toContain('TEST'); await expect(response.text()).resolves.toBe('PERMANENT');
expect(response.getHeaders().link).toContain(`<${LDP.Resource}>; rel="type"`); expect(response.headers.get('wac-allow')).toBe('user="read",public="read"');
expect(response.getHeaders().link).toContain(`<http://test.com/permanent.txt.acl>; rel="acl"`);
expect(response.getHeaders()['wac-allow']).toBe('user="read",public="read"');
// Try to delete permanent file // DELETE fail
response = await resourceHelper.deleteResource('http://test.com/permanent.txt', true); response = await fetch(permanent, { method: 'DELETE' });
expect(response.statusCode).toBe(401); expect(response.status).toBe(401);
}); });
it('can add files but not write to them if append is allowed.', async(): Promise<void> => { it('can add files but not write to them if append is allowed.', async(): Promise<void> => {
// Set acl await aclHelper.setSimpleAcl(baseUrl, {
await aclHelper.setSimpleAcl({ read: true, write: false, append: true, control: false }, 'agent'); permissions: { append: true },
agentClass: 'agent',
accessTo: true,
default: true,
});
// Add a file // POST
const filePath = 'testfile2.txt'; const slug = 'slug';
let response = await resourceHelper.performRequestWithBody( let response = await postResource(baseUrl, { contentType: 'text/plain', slug, body: 'SLUGDATA' });
new URL(`${BASE}/`), const document = response.headers.get('location')!;
'POST',
{
'content-type': 'text/plain',
'transfer-encoding': 'chunked',
slug: filePath,
},
Buffer.from('data'),
);
expect(response.statusCode).toBe(201);
response = await resourceHelper.createResource( // PUT fail
'../assets/testfile2.txt', filePath, 'text/plain', true, response = await fetch(document, { method: 'PUT' });
); expect(response.status).toBe(401);
expect(response.statusCode).toBe(401);
// DELETE fail
response = await fetch(document, { method: 'DELETE' });
expect(response.status).toBe(401);
// Clean up resource
await store.deleteResource({ path: document });
}); });
it('can not access an acl file if no control rights are provided.', async(): Promise<void> => { it('can not access an acl file if no control rights are provided.', async(): Promise<void> => {
// Set acl await aclHelper.setSimpleAcl(baseUrl, {
await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent'); permissions: { read: true, write: true, append: true },
agentClass: 'agent',
accessTo: true,
});
const response = await resourceHelper.performRequest(new URL('http://test.com/.acl'), 'GET', { accept: '*/*' }); const response = await fetch(`${baseUrl}.acl`);
expect(response.statusCode).toBe(401); expect(response.status).toBe(401);
}); });
it('can only access an acl file if control rights are provided.', async(): Promise<void> => { it('can only access an acl file if control rights are provided.', async(): Promise<void> => {
// Set acl await aclHelper.setSimpleAcl(baseUrl, {
await aclHelper.setSimpleAcl({ read: false, write: false, append: false, control: true }, 'agent'); permissions: { control: true },
agentClass: 'agent',
accessTo: true,
});
const response = await resourceHelper.performRequest(new URL('http://test.com/.acl'), 'GET', { accept: '*/*' }); const response = await fetch(`${baseUrl}.acl`);
expect(response.statusCode).toBe(200); expect(response.status).toBe(200);
expect(response.getHeaders()['wac-allow']).toBe('user="control",public="control"'); expect(response.headers.get('wac-allow')).toBe('user="control",public="control"');
// Close response
await response.text();
}); });
}); });

View File

@ -253,4 +253,22 @@ describe.each(stores)('An LDP handler allowing all request %s', (name, { storeUr
// DELETE // DELETE
expect(await deleteResource(containerUrl)).toBeUndefined(); expect(await deleteResource(containerUrl)).toBeUndefined();
}); });
// https://github.com/solid/community-server/issues/498
it('accepts a GET with Content-Length: 0.', async(): Promise<void> => {
// PUT
const documentUrl = `${baseUrl}foo/bar`;
const response = await fetch(documentUrl, {
method: 'PUT',
headers: { 'content-length': '0', 'content-type': 'text/turtle' },
body: '',
});
expect(response.status).toBe(205);
// GET
await getResource(documentUrl);
// DELETE
expect(await deleteResource(documentUrl)).toBeUndefined();
});
}); });

View File

@ -1,117 +0,0 @@
import type { MockResponse } from 'node-mocks-http';
import type { HttpHandler, Initializer, ResourceStore } from '../../src/';
import { AclHelper } from '../util/TestHelpers';
import { performRequest } from '../util/Util';
import { BASE, instantiateFromConfig } from './Config';
describe('A server with authorization', (): void => {
let handler: HttpHandler;
let aclHelper: AclHelper;
beforeAll(async(): Promise<void> => {
// Set up the internal store
const variables: Record<string, any> = {
'urn:solid-server:default:variable:baseUrl': BASE,
};
const internalStore = await instantiateFromConfig(
'urn:solid-server:default:MemoryResourceStore',
'ldp-with-auth.json',
variables,
) as ResourceStore;
variables['urn:solid-server:default:variable:store'] = internalStore;
// Create and initialize the HTTP handler and related components
let initializer: Initializer;
let store: ResourceStore;
const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances',
'ldp-with-auth.json',
variables,
) as Record<string, any>;
({ handler, store, initializer } = instances);
await initializer.handleSafe();
// Create test helpers for manipulating the components
aclHelper = new AclHelper(store, BASE);
});
it('can create new entries.', async(): Promise<void> => {
await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent');
// POST
let requestUrl = new URL('http://test.com/');
let response: MockResponse<any> = await performRequest(
handler,
requestUrl,
'POST',
{ 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(201);
// PUT
requestUrl = new URL('http://test.com/foo/bar');
response = await performRequest(
handler,
requestUrl,
'PUT',
{ 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(205);
});
it('cannot create new entries if not allowed.', async(): Promise<void> => {
await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'authenticated');
// POST
let requestUrl = new URL('http://test.com/');
let response: MockResponse<any> = await performRequest(
handler,
requestUrl,
'POST',
{ 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(401);
// PUT
requestUrl = new URL('http://test.com/foo/bar');
response = await performRequest(
handler,
requestUrl,
'PUT',
{ 'content-type': 'text/turtle', 'transfer-encoding': 'chunked' },
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
expect(response.statusCode).toBe(401);
});
// https://github.com/solid/community-server/issues/498
it('accepts a GET with Content-Length: 0.', async(): Promise<void> => {
await aclHelper.setSimpleAcl({ read: true, write: true, append: true, control: false }, 'agent');
// PUT
let requestUrl = new URL('http://test.com/foo/bar');
let response: MockResponse<any> = await performRequest(
handler,
requestUrl,
'PUT',
{ 'content-length': '0', 'content-type': 'text/turtle' },
[],
);
expect(response.statusCode).toBe(205);
// GET
requestUrl = new URL('http://test.com/foo/bar');
response = await performRequest(
handler,
requestUrl,
'GET',
{ 'content-length': '0' },
[],
);
expect(response.statusCode).toBe(200);
expect(response.getHeaders()['wac-allow']).toBe('user="read write append",public="read write append"');
});
});

View File

@ -4,7 +4,6 @@
"files-scs:config/presets/acl.json", "files-scs:config/presets/acl.json",
"files-scs:config/presets/identifiers/suffix-identifiers.json", "files-scs:config/presets/identifiers/suffix-identifiers.json",
"files-scs:config/presets/init.json", "files-scs:config/presets/init.json",
"files-scs:config/presets/ldp.json",
"files-scs:config/presets/ldp/credentials-extractor.json", "files-scs:config/presets/ldp/credentials-extractor.json",
"files-scs:config/presets/ldp/metadata-handler.json", "files-scs:config/presets/ldp/metadata-handler.json",
"files-scs:config/presets/ldp/operation-handler.json", "files-scs:config/presets/ldp/operation-handler.json",
@ -24,7 +23,7 @@
"comment": "Sets up an HTTP server with only the LDP handler as HttpHandler.", "comment": "Sets up an HTTP server with only the LDP handler as HttpHandler.",
"@id": "urn:solid-server:test:Instances", "@id": "urn:solid-server:test:Instances",
"@type": "RecordObject", "@type": "RecordObject",
"RecordObject:_record": [ "record": [
{ {
"RecordObject:_record_key": "initializer", "RecordObject:_record_key": "initializer",
"RecordObject:_record_value": { "RecordObject:_record_value": {
@ -35,10 +34,6 @@
] ]
} }
}, },
{
"RecordObject:_record_key": "handler",
"RecordObject:_record_value": { "@id": "urn:solid-server:default:LdpHandler" }
},
{ {
"RecordObject:_record_key": "store", "RecordObject:_record_key": "store",
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore" } "RecordObject:_record_value": { "@id": "urn:solid-server:default:ResourceStore" }
@ -50,6 +45,38 @@
] ]
}, },
{
"comment": "The default LDP handler, but with an unsecure webId extractor.",
"@id": "urn:solid-server:default:LdpHandler",
"@type": "AuthenticatedLdpHandler",
"args_requestParser": {
"@id": "urn:solid-server:default:RequestParser"
},
"args_credentialsExtractor": {
"@type": "WaterfallHandler",
"WaterfallHandler:_handlers": [
{
"@type": "UnsecureWebIdExtractor"
},
{
"@type": "EmptyCredentialsExtractor"
}
]
},
"args_permissionsExtractor": {
"@id": "urn:solid-server:default:PermissionsExtractor"
},
"args_authorizer": {
"@id": "urn:solid-server:default:AclBasedAuthorizer"
},
"args_operationHandler": {
"@id": "urn:solid-server:default:OperationHandler"
},
"args_responseWriter": {
"@id": "urn:solid-server:default:ResponseWriter"
}
},
{ {
"@id": "urn:solid-server:default:ServerFactory", "@id": "urn:solid-server:default:ServerFactory",
"@type": "BaseHttpServerFactory", "@type": "BaseHttpServerFactory",
@ -60,7 +87,7 @@
{ {
"@id": "urn:solid-server:default:RoutingResourceStore", "@id": "urn:solid-server:default:RoutingResourceStore",
"@type": "PassthroughStore", "@type": "PassthroughStore",
"PassthroughStore:_source": { "source": {
"@id": "urn:solid-server:default:variable:store" "@id": "urn:solid-server:default:variable:store"
} }
}, },

View File

@ -5,23 +5,34 @@ import { Readable } from 'stream';
import type { MockResponse } from 'node-mocks-http'; import type { MockResponse } from 'node-mocks-http';
import { createResponse } from 'node-mocks-http'; import { createResponse } from 'node-mocks-http';
import type { ResourceStore, PermissionSet, HttpHandler, HttpRequest } from '../../src/'; import type { ResourceStore, PermissionSet, HttpHandler, HttpRequest } from '../../src/';
import { BasicRepresentation, joinFilePath, ensureTrailingSlash } from '../../src/'; import { BasicRepresentation, joinFilePath } from '../../src/';
import { performRequest } from './Util'; import { performRequest } from './Util';
/* eslint-disable jest/no-standalone-expect */ /* eslint-disable jest/no-standalone-expect */
export class AclHelper { export class AclHelper {
public readonly store: ResourceStore; public readonly store: ResourceStore;
public id: string;
public constructor(store: ResourceStore, id: string) { public constructor(store: ResourceStore) {
this.store = store; this.store = store;
this.id = ensureTrailingSlash(id);
} }
public async setSimpleAcl( public async setSimpleAcl(
permissions: PermissionSet, resource: string,
agentClass: 'agent' | 'authenticated', options: {
permissions: Partial<PermissionSet>;
agentClass?: 'agent' | 'authenticated';
agent?: string;
accessTo?: boolean;
default?: boolean;
},
): Promise<void> { ): Promise<void> {
if (!options.agentClass && !options.agent) {
throw new Error('At least one of agentClass or agent have to be provided for this to make sense.');
}
if (!options.accessTo && !options.default) {
throw new Error('At least one of accessTo or default have to be true for this to make sense.');
}
const acl: string[] = [ const acl: string[] = [
'@prefix acl: <http://www.w3.org/ns/auth/acl#>.\n', '@prefix acl: <http://www.w3.org/ns/auth/acl#>.\n',
'@prefix foaf: <http://xmlns.com/foaf/0.1/>.\n', '@prefix foaf: <http://xmlns.com/foaf/0.1/>.\n',
@ -29,21 +40,30 @@ export class AclHelper {
]; ];
for (const perm of [ 'Read', 'Append', 'Write', 'Control' ]) { for (const perm of [ 'Read', 'Append', 'Write', 'Control' ]) {
if (permissions[perm.toLowerCase() as keyof PermissionSet]) { if (options.permissions[perm.toLowerCase() as keyof PermissionSet]) {
acl.push(`;\n acl:mode acl:${perm}`); acl.push(`;\n acl:mode acl:${perm}`);
} }
} }
acl.push(`;\n acl:accessTo <${this.id}>`); if (options.accessTo) {
acl.push(`;\n acl:default <${this.id}>`); acl.push(`;\n acl:accessTo <${resource}>`);
acl.push( }
`;\n acl:agentClass ${ if (options.default) {
agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent' acl.push(`;\n acl:default <${resource}>`);
}`, }
); if (options.agentClass) {
acl.push(
`;\n acl:agentClass ${
options.agentClass === 'agent' ? 'foaf:Agent' : 'foaf:AuthenticatedAgent'
}`,
);
}
if (options.agent) {
acl.push(`;\n acl:agent ${options.agent}`);
}
acl.push('.'); acl.push('.');
await this.store.setRepresentation({ path: `${this.id}.acl` }, new BasicRepresentation(acl, 'text/turtle')); await this.store.setRepresentation({ path: `${resource}.acl` }, new BasicRepresentation(acl, 'text/turtle'));
} }
} }

View File

@ -12,6 +12,7 @@ import type { SystemError } from '../../src/util/errors/SystemError';
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
const portNames = [ const portNames = [
'DynamicPods', 'DynamicPods',
'LpdHandlerWithAuth',
'LpdHandlerWithoutAuth', 'LpdHandlerWithoutAuth',
'Middleware', 'Middleware',
'PodCreation', 'PodCreation',