feat: Dynamically generate Allow and Accept-* headers

This commit is contained in:
Joachim Van Herwegen
2022-03-25 11:34:40 +01:00
parent effc20a270
commit 6e98c6aae4
15 changed files with 326 additions and 18 deletions

View File

@@ -93,10 +93,12 @@ describe('An http server with middleware', (): void => {
expect(res.header).toEqual(expect.objectContaining({ 'access-control-allow-origin': 'test.com' }));
});
it('exposes the Accept-Patch header via CORS.', async(): Promise<void> => {
it('exposes the Accept-[Method] header via CORS.', async(): Promise<void> => {
const res = await request(server).get('/').expect(200);
const exposed = res.header['access-control-expose-headers'];
expect(exposed.split(/\s*,\s*/u)).toContain('Accept-Patch');
expect(exposed.split(/\s*,\s*/u)).toContain('Accept-Post');
expect(exposed.split(/\s*,\s*/u)).toContain('Accept-Put');
});
it('exposes the Last-Modified and ETag headers via CORS.', async(): Promise<void> => {

View File

@@ -0,0 +1,115 @@
import { createResponse } from 'node-mocks-http';
import { AllowAcceptHeaderWriter } from '../../../../../src/http/output/metadata/AllowAcceptHeaderWriter';
import type { MetadataRecord } from '../../../../../src/http/representation/RepresentationMetadata';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { MethodNotAllowedHttpError } from '../../../../../src/util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { LDP, PIM, RDF, SOLID_ERROR } from '../../../../../src/util/Vocabularies';
describe('An AllowAcceptHeaderWriter', (): void => {
const document = new RepresentationMetadata({ path: 'http://example.com/foo/bar' },
{ [RDF.type]: LDP.terms.Resource });
const emptyContainer = new RepresentationMetadata({ path: 'http://example.com/foo/' },
{ [RDF.type]: [ LDP.terms.Resource, LDP.terms.Container ]});
const fullContainer = new RepresentationMetadata({ path: 'http://example.com/foo/' },
{
[RDF.type]: [ LDP.terms.Resource, LDP.terms.Container ],
[LDP.contains]: [ document.identifier ],
// Typescript doesn't find the correct constructor without the cast
} as MetadataRecord);
const storageContainer = new RepresentationMetadata({ path: 'http://example.com/foo/' },
{ [RDF.type]: [ LDP.terms.Resource, LDP.terms.Container, PIM.terms.Storage ]});
const error404 = new RepresentationMetadata({ [SOLID_ERROR.errorResponse]: NotFoundHttpError.uri });
const error405 = new RepresentationMetadata(
{ [SOLID_ERROR.errorResponse]: MethodNotAllowedHttpError.uri, [SOLID_ERROR.disallowedMethod]: 'PUT' },
);
const error415 = new RepresentationMetadata({ [SOLID_ERROR.errorResponse]: UnsupportedMediaTypeHttpError.uri });
let response: HttpResponse;
let writer: AllowAcceptHeaderWriter;
beforeEach(async(): Promise<void> => {
response = createResponse();
writer = new AllowAcceptHeaderWriter(
[ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', 'DELETE' ],
{ patch: [ 'text/n3', 'application/sparql-update' ], post: [ '*/*' ], put: [ '*/*' ]},
);
});
it('returns all methods except POST for a document.', async(): Promise<void> => {
await expect(writer.handleSafe({ response, metadata: document })).resolves.toBeUndefined();
const headers = response.getHeaders();
expect(typeof headers.allow).toBe('string');
expect(new Set((headers.allow as string).split(', ')))
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'PATCH', 'DELETE' ]));
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
expect(headers['accept-put']).toBe('*/*');
expect(headers['accept-post']).toBeUndefined();
});
it('returns all methods for an empty container.', async(): Promise<void> => {
await expect(writer.handleSafe({ response, metadata: emptyContainer })).resolves.toBeUndefined();
const headers = response.getHeaders();
expect(typeof headers.allow).toBe('string');
expect(new Set((headers.allow as string).split(', ')))
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', 'DELETE' ]));
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
expect(headers['accept-put']).toBe('*/*');
expect(headers['accept-post']).toBe('*/*');
});
it('returns all methods except DELETE for a non-empty container.', async(): Promise<void> => {
await expect(writer.handleSafe({ response, metadata: fullContainer })).resolves.toBeUndefined();
const headers = response.getHeaders();
expect(typeof headers.allow).toBe('string');
expect(new Set((headers.allow as string).split(', ')))
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH' ]));
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
expect(headers['accept-put']).toBe('*/*');
expect(headers['accept-post']).toBe('*/*');
});
it('returns all methods except DELETE for a storage container.', async(): Promise<void> => {
await expect(writer.handleSafe({ response, metadata: storageContainer })).resolves.toBeUndefined();
const headers = response.getHeaders();
expect(typeof headers.allow).toBe('string');
expect(new Set((headers.allow as string).split(', ')))
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH' ]));
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
expect(headers['accept-put']).toBe('*/*');
expect(headers['accept-post']).toBe('*/*');
});
it('returns PATCH and PUT if the target does not exist.', async(): Promise<void> => {
await expect(writer.handleSafe({ response, metadata: error404 })).resolves.toBeUndefined();
const headers = response.getHeaders();
expect(typeof headers.allow).toBe('string');
expect(new Set((headers.allow as string).split(', ')))
.toEqual(new Set([ 'PUT', 'PATCH' ]));
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
expect(headers['accept-put']).toBe('*/*');
expect(headers['accept-post']).toBeUndefined();
});
it('removes methods that are not allowed by a 405 error.', async(): Promise<void> => {
await expect(writer.handleSafe({ response, metadata: error405 })).resolves.toBeUndefined();
const headers = response.getHeaders();
expect(typeof headers.allow).toBe('string');
expect(new Set((headers.allow as string).split(', ')))
.toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH', 'DELETE' ]));
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
expect(headers['accept-put']).toBeUndefined();
expect(headers['accept-post']).toBe('*/*');
});
it('only returns Accept- headers in case of a 415.', async(): Promise<void> => {
await expect(writer.handleSafe({ response, metadata: error415 })).resolves.toBeUndefined();
const headers = response.getHeaders();
expect(headers.allow).toBeUndefined();
expect(headers['accept-patch']).toBe('text/n3, application/sparql-update');
expect(headers['accept-put']).toBe('*/*');
expect(headers['accept-post']).toBe('*/*');
});
});

View File

@@ -248,6 +248,13 @@ describe('A RepresentationMetadata', (): void => {
);
});
it('can check the existence of a triple.', async(): Promise<void> => {
expect(metadata.has(namedNode('has'), literal('data'))).toBe(true);
expect(metadata.has(namedNode('has'))).toBe(true);
expect(metadata.has(undefined, literal('data'))).toBe(true);
expect(metadata.has(namedNode('has'), literal('wrongData'))).toBe(false);
});
it('can get all values for a predicate.', async(): Promise<void> => {
expect(metadata.getAll(namedNode('has'))).toEqualRdfTermArray(
[ literal('data'), literal('moreData'), literal('data') ],

View File

@@ -11,6 +11,7 @@ import type { HttpRequest } from '../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
import { ParsingHttpHandler } from '../../../src/server/ParsingHttpHandler';
import { HttpError } from '../../../src/util/errors/HttpError';
describe('A ParsingHttpHandler', (): void => {
const request: HttpRequest = {} as any;
@@ -78,4 +79,17 @@ describe('A ParsingHttpHandler', (): void => {
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
});
it('adds error metadata if able.', async(): Promise<void> => {
const error = new HttpError(0, 'error');
source.handleSafe.mockRejectedValueOnce(error);
const metaResponse = new ResponseDescription(0, new RepresentationMetadata());
errorHandler.handleSafe.mockResolvedValueOnce(metaResponse);
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, preferences });
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: metaResponse });
expect(metaResponse.metadata?.quads()).toHaveLength(1);
});
});

View File

@@ -17,7 +17,7 @@ export async function getResource(url: string,
expect(response.status).toBe(200);
expect(response.headers.get('link')).toContain(`<${LDP.Resource}>; rel="type"`);
expect(response.headers.get('link')).toContain(`<${url}.acl>; rel="acl"`);
expect(response.headers.get('accept-patch')).toBe('application/sparql-update, text/n3');
expect(response.headers.get('accept-patch')).toBe('text/n3, application/sparql-update');
expect(response.headers.get('ms-author-via')).toBe('SPARQL');
if (isContainer) {