mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Dynamically generate Allow and Accept-* headers
This commit is contained in:
parent
effc20a270
commit
6e98c6aae4
@ -18,6 +18,9 @@
|
||||
"options_preflightContinue": true,
|
||||
"options_exposedHeaders": [
|
||||
"Accept-Patch",
|
||||
"Accept-Post",
|
||||
"Accept-Put",
|
||||
"Allow",
|
||||
"ETag",
|
||||
"Last-Modified",
|
||||
"Link",
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^3.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"files-scs:config/ldp/metadata-writer/writers/allow-accept.json",
|
||||
"files-scs:config/ldp/metadata-writer/writers/constant.json",
|
||||
"files-scs:config/ldp/metadata-writer/writers/link-rel.json",
|
||||
"files-scs:config/ldp/metadata-writer/writers/mapped.json",
|
||||
@ -14,6 +15,7 @@
|
||||
"@id": "urn:solid-server:default:MetadataWriter",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_AllowAccept" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_Constant" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_Mapped" },
|
||||
{ "@id": "urn:solid-server:default:MetadataWriter_Modified" },
|
||||
|
14
config/ldp/metadata-writer/writers/allow-accept.json
Normal file
14
config/ldp/metadata-writer/writers/allow-accept.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^3.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Adds Allow and Accept-[Method] headers.",
|
||||
"@id": "urn:solid-server:default:MetadataWriter_AllowAccept",
|
||||
"@type": "AllowAcceptHeaderWriter",
|
||||
"supportedMethods": [ "OPTIONS", "HEAD", "GET", "PATCH", "POST", "PUT", "DELETE" ],
|
||||
"acceptTypes_patch": [ "text/n3", "application/sparql-update" ],
|
||||
"acceptTypes_put": [ "*/*" ],
|
||||
"acceptTypes_post": [ "*/*" ]
|
||||
}
|
||||
]
|
||||
}
|
@ -6,14 +6,6 @@
|
||||
"@id": "urn:solid-server:default:MetadataWriter_Constant",
|
||||
"@type": "ConstantMetadataWriter",
|
||||
"headers": [
|
||||
{
|
||||
"ConstantMetadataWriter:_headers_key": "Accept-Patch",
|
||||
"ConstantMetadataWriter:_headers_value": "application/sparql-update, text/n3"
|
||||
},
|
||||
{
|
||||
"ConstantMetadataWriter:_headers_key": "Allow",
|
||||
"ConstantMetadataWriter:_headers_value": "OPTIONS, HEAD, GET, PATCH, POST, PUT, DELETE"
|
||||
},
|
||||
{
|
||||
"ConstantMetadataWriter:_headers_key": "MS-Author-Via",
|
||||
"ConstantMetadataWriter:_headers_value": "SPARQL"
|
||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -50,7 +50,7 @@
|
||||
"lodash.orderby": "^4.6.0",
|
||||
"marked": "^4.0.12",
|
||||
"mime-types": "^2.1.34",
|
||||
"n3": "^1.13.0",
|
||||
"n3": "^1.16.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"oidc-provider": "^7.10.6",
|
||||
"pump": "^3.0.0",
|
||||
@ -11633,9 +11633,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/n3": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/n3/-/n3-1.13.0.tgz",
|
||||
"integrity": "sha512-GMB4ypBfnuf6mmwbtyN6Whc8TfuVDedxc4n+3wsacQH/h0+RjaEobGMhlWrFLDsqVbT94XA6+q9yysMO5SadKA==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/n3/-/n3-1.16.0.tgz",
|
||||
"integrity": "sha512-gE5KF07yhGXhEdAVru5QUqC4fKltA4sMwgASWpOrZSwn8fi8cuLHYPjRl9pR5WhQL96lhaNMZwT8enRIayVfLg==",
|
||||
"dependencies": {
|
||||
"queue-microtask": "^1.1.2",
|
||||
"readable-stream": "^3.6.0"
|
||||
@ -24106,9 +24106,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"n3": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/n3/-/n3-1.13.0.tgz",
|
||||
"integrity": "sha512-GMB4ypBfnuf6mmwbtyN6Whc8TfuVDedxc4n+3wsacQH/h0+RjaEobGMhlWrFLDsqVbT94XA6+q9yysMO5SadKA==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/n3/-/n3-1.16.0.tgz",
|
||||
"integrity": "sha512-gE5KF07yhGXhEdAVru5QUqC4fKltA4sMwgASWpOrZSwn8fi8cuLHYPjRl9pR5WhQL96lhaNMZwT8enRIayVfLg==",
|
||||
"requires": {
|
||||
"queue-microtask": "^1.1.2",
|
||||
"readable-stream": "^3.6.0"
|
||||
|
@ -116,7 +116,7 @@
|
||||
"lodash.orderby": "^4.6.0",
|
||||
"marked": "^4.0.12",
|
||||
"mime-types": "^2.1.34",
|
||||
"n3": "^1.13.0",
|
||||
"n3": "^1.16.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"oidc-provider": "^7.10.6",
|
||||
"pump": "^3.0.0",
|
||||
|
139
src/http/output/metadata/AllowAcceptHeaderWriter.ts
Normal file
139
src/http/output/metadata/AllowAcceptHeaderWriter.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import { MethodNotAllowedHttpError } from '../../../util/errors/MethodNotAllowedHttpError';
|
||||
import { NotFoundHttpError } from '../../../util/errors/NotFoundHttpError';
|
||||
import { UnsupportedMediaTypeHttpError } from '../../../util/errors/UnsupportedMediaTypeHttpError';
|
||||
import { addHeader } from '../../../util/HeaderUtil';
|
||||
import { isContainerPath } from '../../../util/PathUtil';
|
||||
import { LDP, PIM, RDF, SOLID_ERROR } from '../../../util/Vocabularies';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
// Only PUT and PATCH can be used to create a new resource
|
||||
const NEW_RESOURCE_ALLOWED_METHODS = new Set([ 'PUT', 'PATCH' ]);
|
||||
|
||||
/**
|
||||
* Generates Allow, Accept-Patch, Accept-Post, and Accept-Put headers.
|
||||
* The resulting values depend on the choses input methods and types.
|
||||
* The input metadata also gets used to remove methods from that list
|
||||
* if they are not valid in the given situation.
|
||||
*/
|
||||
export class AllowAcceptHeaderWriter extends MetadataWriter {
|
||||
private readonly supportedMethods: string[];
|
||||
private readonly acceptTypes: { patch: string[]; post: string[]; put: string[] };
|
||||
|
||||
public constructor(supportedMethods: string[], acceptTypes: { patch?: string[]; post?: string[]; put?: string[] }) {
|
||||
super();
|
||||
this.supportedMethods = supportedMethods;
|
||||
this.acceptTypes = { patch: [], post: [], put: [], ...acceptTypes };
|
||||
}
|
||||
|
||||
public async handle(input: { response: HttpResponse; metadata: RepresentationMetadata }): Promise<void> {
|
||||
const { response, metadata } = input;
|
||||
|
||||
// Filter out methods which are not allowed
|
||||
const allowedMethods = this.filterAllowedMethods(metadata);
|
||||
|
||||
// Generate the Allow headers (if required)
|
||||
const generateAllow = this.generateAllow(allowedMethods, response, metadata);
|
||||
|
||||
// Generate Accept-[Method] headers (if required)
|
||||
this.generateAccept(allowedMethods, generateAllow, response, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts from the stored set of methods and removes all those that are not allowed based on the metadata.
|
||||
*/
|
||||
private filterAllowedMethods(metadata: RepresentationMetadata): Set<string> {
|
||||
const disallowedMethods = new Set(metadata.getAll(SOLID_ERROR.terms.disallowedMethod)
|
||||
.map((term): string => term.value));
|
||||
const allowedMethods = new Set(this.supportedMethods.filter((method): boolean => !disallowedMethods.has(method)));
|
||||
|
||||
// POST is only allowed on containers.
|
||||
// Metadata only has the resource URI in case it has resource metadata.
|
||||
if (this.isPostAllowed(metadata)) {
|
||||
allowedMethods.delete('POST');
|
||||
}
|
||||
|
||||
if (!this.isDeleteAllowed(metadata)) {
|
||||
allowedMethods.delete('DELETE');
|
||||
}
|
||||
|
||||
// If we are sure the resource does not exist: only keep methods that can create a new resource.
|
||||
if (metadata.has(SOLID_ERROR.terms.errorResponse, NotFoundHttpError.uri)) {
|
||||
for (const method of allowedMethods) {
|
||||
if (!NEW_RESOURCE_ALLOWED_METHODS.has(method)) {
|
||||
allowedMethods.delete(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allowedMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST is only allowed on containers.
|
||||
* The metadata URI is only valid in case there is resource metadata,
|
||||
* otherwise it is just a blank node.
|
||||
*/
|
||||
private isPostAllowed(metadata: RepresentationMetadata): boolean {
|
||||
return metadata.has(RDF.terms.type, LDP.terms.Resource) && !isContainerPath(metadata.identifier.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE is allowed if the target exists,
|
||||
* is not a container,
|
||||
* or is an empty container that isn't a storage.
|
||||
*
|
||||
* Note that the identifier value check only works if the metadata is not about an error.
|
||||
*/
|
||||
private isDeleteAllowed(metadata: RepresentationMetadata): boolean {
|
||||
if (!isContainerPath(metadata.identifier.value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isStorage = metadata.has(RDF.terms.type, PIM.terms.Storage);
|
||||
const isEmpty = metadata.has(LDP.terms.contains);
|
||||
return !isStorage && !isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the Allow header if required.
|
||||
* It only needs to get added for successful GET/HEAD requests, 404s, or 405s.
|
||||
* The spec only requires it for GET/HEAD requests and 405s.
|
||||
* In the case of other error messages we can't deduce what the request method was,
|
||||
* so we do not add the header as we don't have enough information.
|
||||
*/
|
||||
private generateAllow(methods: Set<string>, response: HttpResponse, metadata: RepresentationMetadata): boolean {
|
||||
const methodDisallowed = metadata.has(SOLID_ERROR.terms.errorResponse, MethodNotAllowedHttpError.uri);
|
||||
// 405s indicate the target resource exists.
|
||||
// This is a heuristic, but one that should always be correct in our case.
|
||||
const resourceExists = methodDisallowed || metadata.has(RDF.terms.type, LDP.terms.Resource);
|
||||
const generateAllow = resourceExists || metadata.has(SOLID_ERROR.terms.errorResponse, NotFoundHttpError.uri);
|
||||
if (generateAllow) {
|
||||
addHeader(response, 'Allow', [ ...methods ].join(', '));
|
||||
}
|
||||
return generateAllow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the Accept-[Method] headers if required.
|
||||
* Will be added if the Allow header was added, or in case of a 415 error.
|
||||
* Specific Accept-[Method] headers will only be added if the method is in the `methods` set.
|
||||
*/
|
||||
private generateAccept(methods: Set<string>, generateAllow: boolean, response: HttpResponse,
|
||||
metadata: RepresentationMetadata): void {
|
||||
const typeWasUnsupported = metadata.has(SOLID_ERROR.terms.errorResponse, UnsupportedMediaTypeHttpError.uri);
|
||||
const generateAccept = generateAllow || typeWasUnsupported;
|
||||
if (generateAccept) {
|
||||
if (methods.has('PATCH')) {
|
||||
addHeader(response, 'Accept-Patch', this.acceptTypes.patch.join(', '));
|
||||
}
|
||||
if (methods.has('POST')) {
|
||||
addHeader(response, 'Accept-Post', this.acceptTypes.post.join(', '));
|
||||
}
|
||||
if (methods.has('PUT')) {
|
||||
addHeader(response, 'Accept-Put', this.acceptTypes.put.join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -257,6 +257,20 @@ export class RepresentationMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if a specific triple can be found in the metadata.
|
||||
* Undefined parameters are interpreted as wildcards.
|
||||
*/
|
||||
public has(
|
||||
predicate: NamedNode | string | null = null,
|
||||
object: NamedNode | BlankNode | Literal | string | null = null,
|
||||
graph: MetadataGraph | null = null,
|
||||
): boolean {
|
||||
// This works with N3.js but at the time of writing the typings have not been updated yet.
|
||||
// If you see this line of code check if the typings are already correct and update this if so.
|
||||
return (this.store.has as any)(this.id, predicate, object, graph);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all object values matching the given predicate and/or graph.
|
||||
* @param predicate - Optional predicate to get the values for.
|
||||
|
@ -94,6 +94,7 @@ export * from './http/output/error/RedirectingErrorHandler';
|
||||
export * from './http/output/error/SafeErrorHandler';
|
||||
|
||||
// HTTP/Output/Metadata
|
||||
export * from './http/output/metadata/AllowAcceptHeaderWriter';
|
||||
export * from './http/output/metadata/ConstantMetadataWriter';
|
||||
export * from './http/output/metadata/LinkRelMetadataWriter';
|
||||
export * from './http/output/metadata/MappedMetadataWriter';
|
||||
|
@ -6,6 +6,7 @@ import type { ResponseWriter } from '../http/output/ResponseWriter';
|
||||
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { assertError } from '../util/errors/ErrorUtil';
|
||||
import { HttpError } from '../util/errors/HttpError';
|
||||
import type { HttpHandlerInput } from './HttpHandler';
|
||||
import { HttpHandler } from './HttpHandler';
|
||||
import type { OperationHttpHandler } from './OperationHttpHandler';
|
||||
@ -73,6 +74,10 @@ export class ParsingHttpHandler extends HttpHandler {
|
||||
} catch (error: unknown) {
|
||||
assertError(error);
|
||||
result = await this.errorHandler.handleSafe({ error, preferences });
|
||||
if (HttpError.isInstance(error) && result.metadata) {
|
||||
const quads = error.generateMetadata(result.metadata.identifier);
|
||||
result.metadata.addQuads(quads);
|
||||
}
|
||||
}
|
||||
|
||||
if (result) {
|
||||
|
@ -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> => {
|
||||
|
115
test/unit/http/output/metadata/AllowAcceptHeaderWriter.test.ts
Normal file
115
test/unit/http/output/metadata/AllowAcceptHeaderWriter.test.ts
Normal 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('*/*');
|
||||
});
|
||||
});
|
@ -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') ],
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user