2022-01-25 11:30:42 +01:00

399 lines
16 KiB
TypeScript

import 'jest-rdf';
import { fetch } from 'cross-fetch';
import { Parser } from 'n3';
import type { AclPermission } from '../../src/authorization/permissions/AclPermission';
import { BasicRepresentation } from '../../src/http/representation/BasicRepresentation';
import type { App } from '../../src/init/App';
import type { ResourceStore } from '../../src/storage/ResourceStore';
import { joinUrl } from '../../src/util/PathUtil';
import { AclHelper } from '../util/AclHelper';
import { getPort } from '../util/Util';
import {
getDefaultVariables,
getPresetConfigPath,
getTestConfigPath,
instantiateFromConfig,
} from './Config';
const port = getPort('N3Patch');
const baseUrl = `http://localhost:${port}/`;
let store: ResourceStore;
let aclHelper: AclHelper;
async function expectPatch(
input: { path: string; contentType?: string; body: string },
expected: { status: number; message?: string; turtle?: string },
): Promise<void> {
const message = expected.message ?? '';
const contentType = input.contentType ?? 'text/n3';
const body = `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
${input.body}`;
const url = joinUrl(baseUrl, input.path);
const res = await fetch(url, {
method: 'PATCH',
headers: { 'content-type': contentType },
body,
});
await expect(res.text()).resolves.toContain(message);
expect(res.status).toBe(expected.status);
// Verify if the resource has the expected RDF data
if (expected.turtle) {
// Might not have read permissions so need to update
await aclHelper.setSimpleAcl(url, { permissions: { read: true }, agentClass: 'agent', accessTo: true });
const get = await fetch(url, {
method: 'GET',
headers: { accept: 'text/turtle' },
});
const expectedTurtle = `@prefix solid: <http://www.w3.org/ns/solid/terms#>.
${expected.turtle}`;
expect(get.status).toBe(200);
const parser = new Parser({ format: 'text/turtle', baseIRI: url });
const actualTriples = parser.parse(await get.text());
expect(actualTriples).toBeRdfIsomorphic(parser.parse(expectedTurtle));
}
}
// Creates/updates a resource with the given data and permissions
async function setResource(path: string, turtle: string, permissions: AclPermission): Promise<void> {
const url = joinUrl(baseUrl, path);
await store.setRepresentation({ path: url }, new BasicRepresentation(turtle, 'text/turtle'));
await aclHelper.setSimpleAcl(url, { permissions, agentClass: 'agent', accessTo: true });
}
describe('A Server supporting N3 Patch', (): void => {
let app: App;
beforeAll(async(): Promise<void> => {
// Create and start the server
const instances = await instantiateFromConfig(
'urn:solid-server:test:Instances',
[
getPresetConfigPath('storage/backend/memory.json'),
getTestConfigPath('ldp-with-auth.json'),
],
getDefaultVariables(port, baseUrl),
) as Record<string, any>;
({ app, store } = instances);
await app.start();
// Create test helper for manipulating acl
aclHelper = new AclHelper(store);
});
afterAll(async(): Promise<void> => {
await app.stop();
});
describe('with an invalid patch document', (): void => {
it('requires text/n3 content-type.', async(): Promise<void> => {
await expectPatch(
{ path: '/invalid', contentType: 'text/other', body: '' },
{ status: 415 },
);
});
it('requires valid syntax.', async(): Promise<void> => {
await expectPatch(
{ path: '/invalid', body: 'invalid syntax' },
{ status: 400, message: 'Invalid N3' },
);
});
it('requires a solid:InsertDeletePatch.', async(): Promise<void> => {
await expectPatch(
{ path: '/invalid', body: '<> a solid:Patch.' },
{
status: 422,
message: 'This patcher only supports N3 Patch documents with exactly 1 solid:InsertDeletePatch entry',
},
);
});
});
describe('inserting data', (): void => {
it('succeeds if there is no resource.', async(): Promise<void> => {
await expectPatch(
{ path: '/new-insert', body: `<> a solid:InsertDeletePatch; solid:inserts { <x> <y> <z>. }.` },
{ status: 201, turtle: '<x> <y> <z>.' },
);
});
it('fails if there is only read access.', async(): Promise<void> => {
await setResource('/read-only', '<a> <b> <c>.', { read: true });
await expectPatch(
{ path: '/read-only', body: `<> a solid:InsertDeletePatch; solid:inserts { <x> <y> <z>. }.` },
{ status: 401 },
);
});
it('succeeds if there is only read access.', async(): Promise<void> => {
await setResource('/append-only', '<a> <b> <c>.', { append: true });
await expectPatch(
{ path: '/append-only', body: `<> a solid:InsertDeletePatch; solid:inserts { <x> <y> <z>. }.` },
{ status: 205, turtle: '<a> <b> <c>. <x> <y> <z>.' },
);
});
it('succeeds if there is only write access.', async(): Promise<void> => {
await setResource('/write-only', '<a> <b> <c>.', { write: true });
await expectPatch(
{ path: '/write-only', body: `<> a solid:InsertDeletePatch; solid:inserts { <x> <y> <z>. }.` },
{ status: 205, turtle: '<a> <b> <c>. <x> <y> <z>.' },
);
});
});
describe('inserting conditional data', (): void => {
it('fails if there is no resource.', async(): Promise<void> => {
await expectPatch(
{ path: '/new-insert-where', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <b> <c>. }.` },
{ status: 409, message: 'The document does not contain any matches for the N3 Patch solid:where condition.' },
);
});
it('fails if there is only read access.', async(): Promise<void> => {
await setResource('/read-only', '<a> <b> <c>.', { read: true });
await expectPatch(
{ path: '/read-only', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <b> <c>. }.` },
{ status: 401 },
);
});
it('fails if there is only append access.', async(): Promise<void> => {
await setResource('/append-only', '<a> <b> <c>.', { append: true });
await expectPatch(
{ path: '/append-only', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <b> <c>. }.` },
{ status: 401 },
);
});
it('fails if there is only write access.', async(): Promise<void> => {
await setResource('/write-only', '<a> <b> <c>.', { write: true });
await expectPatch(
{ path: '/write-only', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <b> <c>. }.` },
{ status: 401 },
);
});
describe('with read/append access', (): void => {
it('succeeds if the conditions match.', async(): Promise<void> => {
await setResource('/read-append', '<a> <b> <c>.', { read: true, append: true });
await expectPatch(
{ path: '/read-append', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <b> <c>. }.` },
{ status: 205, turtle: '<a> <b> <c>. <a> <y> <z>.' },
);
});
it('rejects if there is no match.', async(): Promise<void> => {
await setResource('/read-append', '<a> <b> <c>.', { read: true, append: true });
await expectPatch(
{ path: '/read-append', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <y> <z>. }.` },
{ status: 409, message: 'The document does not contain any matches for the N3 Patch solid:where condition.' },
);
});
it('rejects if there are multiple matches.', async(): Promise<void> => {
await setResource('/read-append', '<a> <b> <c>. <c> <b> <c>.', { read: true, append: true });
await expectPatch(
{ path: '/read-append', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <b> <c>. }.` },
{ status: 409, message: 'The document contains multiple matches for the N3 Patch solid:where condition' },
);
});
});
describe('with read/write access', (): void => {
it('succeeds if the conditions match.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:inserts { ?a <y> <z>. };
solid:where { ?a <b> <c>. }.` },
{ status: 205, turtle: '<a> <b> <c>. <a> <y> <z>.' },
);
});
});
});
describe('deleting data', (): void => {
it('fails if there is no resource.', async(): Promise<void> => {
await expectPatch(
{ path: '/new-delete', body: `<> a solid:InsertDeletePatch;
solid:deletes { <x> <y> <z>. }.` },
{ status: 409, message: 'The document does not contain all triples the N3 Patch requests to delete' },
);
});
it('fails if there is only append access.', async(): Promise<void> => {
await setResource('/append-only', '<a> <b> <c>.', { append: true });
await expectPatch(
{ path: '/append-only', body: `<> a solid:InsertDeletePatch;
solid:deletes { <x> <y> <z>. }.` },
{ status: 401 },
);
});
it('fails if there is only write access.', async(): Promise<void> => {
await setResource('/write-only', '<a> <b> <c>.', { write: true });
await expectPatch(
{ path: '/write-only', body: `<> a solid:InsertDeletePatch;
solid:deletes { <x> <y> <z>. }.` },
{ status: 401 },
);
});
it('fails if there is only read/append access.', async(): Promise<void> => {
await setResource('/read-append', '<a> <b> <c>.', { read: true, append: true });
await expectPatch(
{ path: '/read-append', body: `<> a solid:InsertDeletePatch;
solid:deletes { <x> <y> <z>. }.` },
{ status: 401 },
);
});
describe('with read/write access', (): void => {
it('succeeds if the delete triples exist.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>. <x> <y> <z>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:deletes { <x> <y> <z>. }.` },
{ status: 205, turtle: '<a> <b> <c>.' },
);
});
it('fails if the delete triples do not exist.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>. <x> <y> <z>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:deletes { <a> <y> <z>. }.` },
{ status: 409, message: 'The document does not contain all triples the N3 Patch requests to delete' },
);
});
it('succeeds if the conditions match.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>. <x> <y> <z>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:where { ?a <y> <z>. };
solid:deletes { ?a <y> <z>. }.` },
{ status: 205, turtle: '<a> <b> <c>.' },
);
});
it('fails if the conditions do not match.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:where { ?a <y> <z>. };
solid:deletes { ?a <b> <c>. }.` },
{ status: 409, message: 'The document does not contain any matches for the N3 Patch solid:where condition.' },
);
});
});
});
describe('deleting and inserting data', (): void => {
it('fails if there is no resource.', async(): Promise<void> => {
await expectPatch(
{ path: '/new-delete-insert', body: `<> a solid:InsertDeletePatch;
solid:inserts { <x> <y> <z>. };
solid:deletes { <a> <b> <c>. }.` },
{ status: 409, message: 'The document does not contain all triples the N3 Patch requests to delete' },
);
});
it('fails if there is only append access.', async(): Promise<void> => {
await setResource('/append-only', '<a> <b> <c>.', { append: true });
await expectPatch(
{ path: '/append-only', body: `<> a solid:InsertDeletePatch;
solid:inserts { <x> <y> <z>. };
solid:deletes { <a> <b> <c>. }.` },
{ status: 401 },
);
});
it('fails if there is only write access.', async(): Promise<void> => {
await setResource('/write-only', '<a> <b> <c>.', { write: true });
await expectPatch(
{ path: '/write-only', body: `<> a solid:InsertDeletePatch;
solid:inserts { <x> <y> <z>. };
solid:deletes { <a> <b> <c>. }.` },
{ status: 401 },
);
});
it('fails if there is only read/append access.', async(): Promise<void> => {
await setResource('/read-append', '<a> <b> <c>.', { read: true, append: true });
await expectPatch(
{ path: '/read-append', body: `<> a solid:InsertDeletePatch;
solid:inserts { <x> <y> <z>. };
solid:deletes { <a> <b> <c>. }.` },
{ status: 401 },
);
});
describe('with read/write access', (): void => {
it('executes deletes before inserts.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:inserts { <x> <y> <z>. };
solid:deletes { <x> <y> <z>. }.` },
{ status: 409, message: 'The document does not contain all triples the N3 Patch requests to delete' },
);
});
it('succeeds if the delete triples exist.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:inserts { <x> <y> <z>. };
solid:deletes { <a> <b> <c>. }.` },
{ status: 205, turtle: '<x> <y> <z>.' },
);
});
it('succeeds if the conditions match.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:where { ?a <b> <c>. };
solid:inserts { ?a <y> <z>. };
solid:deletes { ?a <b> <c>. }.` },
{ status: 205, turtle: '<a> <y> <z>.' },
);
});
it('fails if the conditions do not match.', async(): Promise<void> => {
await setResource('/read-write', '<a> <b> <c>.', { read: true, write: true });
await expectPatch(
{ path: '/read-write', body: `<> a solid:InsertDeletePatch;
solid:where { ?a <y> <z>. };
solid:inserts { ?a <y> <z>. };
solid:deletes { ?a <b> <c>. }.` },
{ status: 409, message: 'The document does not contain any matches for the N3 Patch solid:where condition.' },
);
});
});
});
});