feat: Add metadata to errors

This commit is contained in:
Joachim Van Herwegen
2023-07-25 14:10:46 +02:00
parent a333412e19
commit f373dff1d7
42 changed files with 455 additions and 419 deletions

View File

@@ -9,6 +9,7 @@ import { RepresentationMetadata } from '../../../../../src/http/representation/R
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { ContentType } from '../../../../../src/util/Header';
import { guardedStreamFrom } from '../../../../../src/util/StreamUtil';
const { namedNode, quad } = DataFactory;
@@ -24,11 +25,10 @@ describe('A SparqlUpdateBodyParser', (): void => {
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
input.metadata.contentType = 'text/plain';
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
input.metadata.contentType = 'application/sparql-update;charset=utf-8';
const contentType = new ContentType('application/sparql-update');
input.metadata.contentTypeObject = contentType;
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
input.metadata.contentType = 'application/sparql-update ; foo=bar';
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
input.metadata.contentType = 'application/sparql-update';
contentType.parameters = { charset: 'utf-8' };
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
});

View File

@@ -11,6 +11,7 @@ import type {
RepresentationConverter,
RepresentationConverterArgs,
} from '../../../../../src/storage/conversion/RepresentationConverter';
import type { HttpError } from '../../../../../src/util/errors/HttpError';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { HTTP, XSD } from '../../../../../src/util/Vocabularies';
import literal = DataFactory.literal;
@@ -33,7 +34,7 @@ async function expectValidArgs(args: RepresentationConverterArgs, stack?: string
describe('A ConvertingErrorHandler', (): void => {
// The error object can get modified by the handler
let error: Error;
let error: HttpError;
let stack: string | undefined;
const request = {} as HttpRequest;
let converter: jest.Mocked<RepresentationConverter>;

View File

@@ -2,6 +2,7 @@ import { createResponse } from 'node-mocks-http';
import { ContentTypeMetadataWriter } from '../../../../../src/http/output/metadata/ContentTypeMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { ContentType } from '../../../../../src/util/Header';
describe('A ContentTypeMetadataWriter', (): void => {
const writer = new ContentTypeMetadataWriter();
@@ -18,18 +19,12 @@ describe('A ContentTypeMetadataWriter', (): void => {
});
it('adds a Content-Type header with parameters if present.', async(): Promise<void> => {
const metadata = new RepresentationMetadata('text/plain; charset=utf-8');
const metadata = new RepresentationMetadata(new ContentType('text/plain', { charset: 'utf-8' }));
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'content-type': 'text/plain; charset=utf-8',
});
const metadata2 = new RepresentationMetadata('text/plain; charset="utf-8"');
await expect(writer.handle({ response, metadata: metadata2 })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'content-type': 'text/plain; charset=utf-8',
});
});
it('adds a Content-Type header without parameters.', async(): Promise<void> => {

View File

@@ -8,6 +8,7 @@ describe('A RedirectResponseDescription', (): void => {
it('has status the code and location of the error.', async(): Promise<void> => {
const description = new RedirectResponseDescription(error);
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(error.location);
expect(description.metadata).toBe(error.metadata);
expect(description.statusCode).toBe(error.statusCode);
});
});

View File

@@ -2,8 +2,8 @@ import 'jest-rdf';
import type { BlankNode } from 'n3';
import { DataFactory } from 'n3';
import type { NamedNode, Quad } from 'rdf-js';
import { ContentType } from '../../../../src';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { ContentType } from '../../../../src/util/Header';
import { CONTENT_TYPE_TERM, SOLID_META, RDFS } from '../../../../src/util/Vocabularies';
const { defaultGraph, literal, namedNode, quad } = DataFactory;
@@ -308,14 +308,14 @@ describe('A RepresentationMetadata', (): void => {
it('has a shorthand for Content-Type as string.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
expect(metadata.contentTypeObject).toEqual({
value: 'text/plain',
parameters: {
charset: 'utf-8',
test: 'value1',
},
});
metadata.contentType = 'text/plain';
expect(metadata.contentTypeObject).toEqual({ value: 'text/plain', parameters: {}});
});
it('errors trying to set a Content-Type with parameters using a string.', async(): Promise<void> => {
expect((): void => {
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
}).toThrow(Error);
});
it('has a shorthand for Content-Type as object.', async(): Promise<void> => {
@@ -341,7 +341,10 @@ describe('A RepresentationMetadata', (): void => {
it('can properly clear the Content-Type parameters explicitly.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
metadata.contentTypeObject = new ContentType('text/plain', {
charset: 'utf-8',
test: 'value1',
});
metadata.contentType = undefined;
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
@@ -353,7 +356,10 @@ describe('A RepresentationMetadata', (): void => {
it('can properly clear the Content-Type parameters implicitly.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
metadata.contentTypeObject = new ContentType('text/plain', {
charset: 'utf-8',
test: 'value1',
});
metadata.contentType = 'text/turtle';
expect(metadata.contentType).toBe('text/turtle');
expect(metadata.contentTypeObject).toEqual({
@@ -368,7 +374,10 @@ describe('A RepresentationMetadata', (): void => {
it('can return invalid parameters when too many quads are present.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
expect(metadata.contentTypeObject).toBeUndefined();
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
metadata.contentTypeObject = new ContentType('text/plain', {
charset: 'utf-8',
test: 'value1',
});
const param = metadata.quads(null, SOLID_META.terms.value)[0].subject;
metadata.addQuad(param as BlankNode, SOLID_META.terms.value, 'anomaly');
expect(metadata.contentTypeObject?.parameters).toMatchObject({ invalid: '' });

View File

@@ -14,6 +14,7 @@ import type { Interaction, InteractionHandler } from '../../../../src/identity/i
import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory';
import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage';
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
import { extractErrorTerms } from '../../../../src/util/errors/HttpErrorUtil';
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
/* eslint-disable @typescript-eslint/naming-convention */
@@ -269,12 +270,12 @@ describe('An IdentityProviderFactory', (): void => {
name: 'BadRequestHttpError',
message: 'Unknown client, you might need to clear the local storage on the client.',
errorCode: 'E0003',
details: {
client_id: 'CLIENT_ID',
redirect_uri: 'REDIRECT_URI',
},
}),
request: ctx.req });
expect(extractErrorTerms(errorHandler.handleSafe.mock.calls[0][0].error.metadata)).toEqual({
client_id: 'CLIENT_ID',
redirect_uri: 'REDIRECT_URI',
});
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }});
});

View File

@@ -5,6 +5,8 @@ import rdfDereferencer from 'rdf-dereference';
import { v4 } from 'uuid';
import { TokenOwnershipValidator } from '../../../../src/identity/ownership/TokenOwnershipValidator';
import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringStorage';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { extractErrorTerms } from '../../../../src/util/errors/HttpErrorUtil';
import { SOLID } from '../../../../src/util/Vocabularies';
const { literal, namedNode, quad } = DataFactory;
@@ -57,10 +59,16 @@ describe('A TokenOwnershipValidator', (): void => {
it('errors if no token is stored in the storage.', async(): Promise<void> => {
// Even if the token is in the WebId, it will error since it's not in the storage
mockDereference(tokenTriple);
await expect(validator.handle({ webId })).rejects.toThrow(expect.objectContaining({
message: expect.stringContaining(tokenString),
details: { quad: tokenString },
}));
let error: unknown;
try {
await validator.handle({ webId });
} catch (err: unknown) {
error = err;
}
expect(error).toEqual(expect.objectContaining({ message: expect.stringContaining(tokenString) }));
expect(BadRequestHttpError.isInstance(error)).toBe(true);
expect(extractErrorTerms((error as BadRequestHttpError).metadata))
.toEqual({ quad: tokenString });
expect(rdfDereferenceMock.dereference).toHaveBeenCalledTimes(0);
});

View File

@@ -4,12 +4,11 @@ import type { ErrorHandler } from '../../../src/http/output/error/ErrorHandler';
import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription';
import type { ResponseWriter } from '../../../src/http/output/ResponseWriter';
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
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';
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
describe('A ParsingHttpHandler', (): void => {
const request: HttpRequest = {} as any;
@@ -57,7 +56,7 @@ describe('A ParsingHttpHandler', (): void => {
});
it('calls the error handler if something goes wrong.', async(): Promise<void> => {
const error = new Error('bad data');
const error = new BadRequestHttpError('bad data');
source.handleSafe.mockRejectedValueOnce(error);
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
@@ -66,16 +65,14 @@ describe('A ParsingHttpHandler', (): void => {
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
});
it('adds error metadata if able.', async(): Promise<void> => {
const error = new HttpError(0, 'error');
it('creates an InternalServerError if th error was not an HttpError.', async(): Promise<void> => {
const error = new Error('bad data');
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, request });
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith(expect.objectContaining({ request }));
expect(errorHandler.handleSafe.mock.calls[0][0].error.cause).toBe(error);
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: metaResponse });
expect(metaResponse.metadata?.quads()).toHaveLength(1);
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
});
});

View File

@@ -20,6 +20,7 @@ import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
import { PreconditionFailedHttpError } from '../../../src/util/errors/PreconditionFailedHttpError';
import type { Guarded } from '../../../src/util/GuardedStream';
import { ContentType } from '../../../src/util/Header';
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
@@ -673,7 +674,7 @@ describe('A DataAccessorBasedStore', (): void => {
representation.metadata.add(
SOLID_META.terms.preserve, namedNode(metaResourceID.path), SOLID_META.terms.ResponseMetadata,
);
representation.metadata.contentType = 'text/plain; charset=UTF-8';
representation.metadata.contentTypeObject = new ContentType('text/plain', { charset: 'UTF-8' });
await store.setRepresentation(resourceID, representation);
const { metadata } = accessor.data[resourceID.path];
expect(metadata.quads(null, RDF.terms.type)).toHaveLength(2);

View File

@@ -1,6 +1,7 @@
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import { ErrorToJsonConverter } from '../../../../src/storage/conversion/ErrorToJsonConverter';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { errorTermsToMetadata } from '../../../../src/util/errors/HttpErrorUtil';
import type { OAuthErrorFields } from '../../../../src/util/errors/OAuthHttpError';
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
import { readJsonStream } from '../../../../src/util/StreamUtil';
@@ -28,11 +29,13 @@ describe('An ErrorToJsonConverter', (): void => {
statusCode: 400,
errorCode: 'H400',
stack: error.stack,
details: {},
});
});
it('copies the HttpError details.', async(): Promise<void> => {
const error = new BadRequestHttpError('error text', { details: { important: 'detail' }});
const metadata = errorTermsToMetadata({ important: 'detail' });
const error = new BadRequestHttpError('error text', { metadata });
const representation = new BasicRepresentation([ error ], 'internal/error', false);
const prom = converter.handle({ identifier, representation, preferences });
await expect(prom).resolves.toBeDefined();
@@ -75,11 +78,13 @@ describe('An ErrorToJsonConverter', (): void => {
error_description: 'error_description',
scope: 'scope',
state: 'state',
details: {},
});
});
it('does not copy the details if they are not serializable.', async(): Promise<void> => {
const error = new BadRequestHttpError('error text', { details: { object: BigInt(1) }});
it('only adds stack if it is defined.', async(): Promise<void> => {
const error = new BadRequestHttpError('error text');
delete error.stack;
const representation = new BasicRepresentation([ error ], 'internal/error', false);
const prom = converter.handle({ identifier, representation, preferences });
await expect(prom).resolves.toBeDefined();
@@ -91,39 +96,7 @@ describe('An ErrorToJsonConverter', (): void => {
message: 'error text',
statusCode: 400,
errorCode: 'H400',
stack: error.stack,
});
});
it('defaults to status code 500 for non-HTTP errors.', async(): Promise<void> => {
const error = new Error('error text');
const representation = new BasicRepresentation([ error ], 'internal/error', false);
const prom = converter.handle({ identifier, representation, preferences });
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.binary).toBe(true);
expect(result.metadata.contentType).toBe('application/json');
await expect(readJsonStream(result.data)).resolves.toEqual({
name: 'Error',
message: 'error text',
statusCode: 500,
stack: error.stack,
});
});
it('only adds stack if it is defined.', async(): Promise<void> => {
const error = new Error('error text');
delete error.stack;
const representation = new BasicRepresentation([ error ], 'internal/error', false);
const prom = converter.handle({ identifier, representation, preferences });
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.binary).toBe(true);
expect(result.metadata.contentType).toBe('application/json');
await expect(readJsonStream(result.data)).resolves.toEqual({
name: 'Error',
message: 'error text',
statusCode: 500,
details: {},
});
});
});

View File

@@ -1,6 +1,7 @@
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import { ErrorToTemplateConverter } from '../../../../src/storage/conversion/ErrorToTemplateConverter';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { errorTermsToMetadata } from '../../../../src/util/errors/HttpErrorUtil';
import { resolveModulePath } from '../../../../src/util/PathUtil';
import { readableToString } from '../../../../src/util/StreamUtil';
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
@@ -91,7 +92,8 @@ describe('An ErrorToTemplateConverter', (): void => {
});
it('adds additional information if an error code description is found.', async(): Promise<void> => {
const error = new BadRequestHttpError('error text', { errorCode, details: { key: 'val' }});
const metadata = errorTermsToMetadata({ key: 'val' });
const error = new BadRequestHttpError('error text', { errorCode, metadata });
const representation = new BasicRepresentation([ error ], 'internal/error', false);
const prom = converter.handle({ identifier, representation, preferences });
await expect(prom).resolves.toBeDefined();
@@ -154,8 +156,9 @@ describe('An ErrorToTemplateConverter', (): void => {
});
it('has default template options.', async(): Promise<void> => {
const metadata = errorTermsToMetadata({ key: 'val' });
converter = new ErrorToTemplateConverter(templateEngine);
const error = new BadRequestHttpError('error text', { errorCode, details: { key: 'val' }});
const error = new BadRequestHttpError('error text', { errorCode, metadata });
const representation = new BasicRepresentation([ error ], 'internal/error', false);
const prom = converter.handle({ identifier, representation, preferences });
await expect(prom).resolves.toBeDefined();

View File

@@ -1,7 +1,7 @@
import type { HttpResponse } from '../../../src/server/HttpResponse';
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
import { ContentType,
addHeader,
import { ContentType } from '../../../src/util/Header';
import { addHeader,
hasScheme,
matchesAuthorizationScheme,
parseAccept,

View File

@@ -2,6 +2,8 @@ import { promises as fsPromises } from 'fs';
import type { TargetExtractor } from '../../../src/http/input/identifier/TargetExtractor';
import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
import type { HttpRequest } from '../../../src/server/HttpRequest';
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
import { extractErrorTerms } from '../../../src/util/errors/HttpErrorUtil';
import {
absoluteFilePath,
createSubdomainRegexp,
@@ -218,8 +220,15 @@ describe('PathUtil', (): void => {
it('errors if the target is outside of the server scope.', async(): Promise<void> => {
targetExtractor.handleSafe.mockResolvedValueOnce({ path: 'http://somewhere.else/resource' });
await expect(getRelativeUrl(baseUrl, request, targetExtractor)).rejects
.toThrow(expect.objectContaining({ errorCode: 'E0001', details: { path: 'http://somewhere.else/resource' }}));
let error: unknown;
try {
await getRelativeUrl(baseUrl, request, targetExtractor);
} catch (err: unknown) {
error = err;
}
expect(error).toEqual(expect.objectContaining({ errorCode: 'E0001' }));
expect(BadRequestHttpError.isInstance(error)).toBe(true);
expect(extractErrorTerms((error as BadRequestHttpError).metadata)).toEqual({ path: 'http://somewhere.else/resource' });
});
});

View File

@@ -1,4 +1,4 @@
import { assertError, createErrorMessage, isError } from '../../../../src/util/errors/ErrorUtil';
import { createErrorMessage, isError } from '../../../../src/util/errors/ErrorUtil';
describe('ErrorUtil', (): void => {
describe('#isError', (): void => {
@@ -19,16 +19,6 @@ describe('ErrorUtil', (): void => {
});
});
describe('#assertError', (): void => {
it('returns undefined on native errors.', async(): Promise<void> => {
expect(assertError(new Error('error'))).toBeUndefined();
});
it('throws on other values.', async(): Promise<void> => {
expect((): void => assertError('apple')).toThrow('apple');
});
});
describe('#createErrorMessage', (): void => {
it('returns the given message for normal Errors.', async(): Promise<void> => {
expect(createErrorMessage(new Error('error msg'))).toBe('error msg');

View File

@@ -1,5 +1,5 @@
import 'jest-rdf';
import { DataFactory } from 'n3';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError';
import { ForbiddenHttpError } from '../../../../src/util/errors/ForbiddenHttpError';
@@ -15,9 +15,7 @@ import { PreconditionFailedHttpError } from '../../../../src/util/errors/Precond
import { UnauthorizedHttpError } from '../../../../src/util/errors/UnauthorizedHttpError';
import { UnprocessableEntityHttpError } from '../../../../src/util/errors/UnprocessableEntityHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { SOLID_ERROR } from '../../../../src/util/Vocabularies';
const { literal, namedNode, quad } = DataFactory;
import { HTTP, SOLID_ERROR } from '../../../../src/util/Vocabularies';
describe('HttpError', (): void => {
const errors: [string, number, HttpErrorClass][] = [
@@ -39,7 +37,7 @@ describe('HttpError', (): void => {
const options = {
cause: new Error('cause'),
errorCode: 'E1234',
details: {},
metadata: new RepresentationMetadata(),
};
const instance = new constructor('my message', options);
@@ -75,15 +73,11 @@ describe('HttpError', (): void => {
expect(new constructor().errorCode).toBe(`H${statusCode}`);
});
it('sets the details.', (): void => {
expect(instance.details).toBe(options.details);
});
it('generates metadata.', (): void => {
const subject = namedNode('subject');
expect(instance.generateMetadata(subject)).toBeRdfIsomorphic([
quad(subject, SOLID_ERROR.terms.errorResponse, constructor.uri),
]);
it('sets the metadata.', (): void => {
expect(instance.metadata).toBe(options.metadata);
expect(instance.metadata.get(SOLID_ERROR.terms.errorResponse)?.value)
.toBe(`${SOLID_ERROR.namespace}H${statusCode}`);
expect(instance.metadata.get(HTTP.terms.statusCodeNumber)?.value).toBe(`${statusCode}`);
});
});
@@ -92,7 +86,6 @@ describe('HttpError', (): void => {
const options = {
cause: new Error('cause'),
errorCode: 'E1234',
details: { some: 'detail' },
};
const instance = new MethodNotAllowedHttpError([ 'GET' ], 'my message', options);
@@ -107,11 +100,10 @@ describe('HttpError', (): void => {
expect(instance.errorCode).toBe(options.errorCode);
expect(new MethodNotAllowedHttpError([ 'GET' ]).errorCode).toBe(`H${405}`);
const subject = namedNode('subject');
expect(instance.generateMetadata(subject)).toBeRdfIsomorphic([
quad(subject, SOLID_ERROR.terms.errorResponse, MethodNotAllowedHttpError.uri),
quad(subject, SOLID_ERROR.terms.disallowedMethod, literal('GET')),
]);
expect(instance.metadata.get(SOLID_ERROR.terms.errorResponse)?.value)
.toBe(`${SOLID_ERROR.namespace}H405`);
expect(instance.metadata.get(HTTP.terms.statusCodeNumber)?.value).toBe('405');
expect(instance.metadata.get(SOLID_ERROR.terms.disallowedMethod)?.value).toBe('GET');
});
});
});

View File

@@ -1,8 +1,53 @@
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { HttpError } from '../../../../src/util/errors/HttpError';
import { createAggregateError, getStatusCode } from '../../../../src/util/errors/HttpErrorUtil';
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
import {
createAggregateError,
errorTermsToMetadata,
extractErrorTerms,
} from '../../../../src/util/errors/HttpErrorUtil';
import { toPredicateTerm } from '../../../../src/util/TermUtil';
describe('HttpErrorUtil', (): void => {
describe('#errorTermsToMetadata', (): void => {
it('creates a metadata object with the necessary triples.', async(): Promise<void> => {
const metadata = errorTermsToMetadata({
test: 'apple',
test2: 'pear',
not: undefined,
});
expect(metadata.quads()).toHaveLength(2);
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test'))?.value).toBe('apple');
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test2'))?.value).toBe('pear');
});
it('can add the necessary triples to existing metadata.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
const response = errorTermsToMetadata({
test: 'apple',
test2: 'pear',
not: undefined,
}, metadata);
expect(response).toBe(metadata);
expect(metadata.quads()).toHaveLength(2);
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test'))?.value).toBe('apple');
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test2'))?.value).toBe('pear');
});
});
describe('#extractErrorTerms', (): void => {
it('returns an object describing the terms.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({
'urn:npm:solid:community-server:error-term:test': 'apple',
'urn:npm:solid:community-server:error-term:test2': 'pear',
'urn:npm:solid:community-server:other:test3': 'mango',
});
expect(extractErrorTerms(metadata)).toEqual({
test: 'apple',
test2: 'pear',
});
});
});
describe('ErrorUtil', (): void => {
describe('#createAggregateError', (): void => {
const error401 = new HttpError(401, 'UnauthorizedHttpError');
const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError');
@@ -50,14 +95,4 @@ describe('ErrorUtil', (): void => {
.toBe('Multiple handler errors: noStatusCode, noStatusCode');
});
});
describe('#getStatusCode', (): void => {
it('returns the corresponding status code for HttpErrors.', async(): Promise<void> => {
expect(getStatusCode(new NotFoundHttpError())).toBe(404);
});
it('returns 500 for other errors.', async(): Promise<void> => {
expect(getStatusCode(new Error('404'))).toBe(500);
});
});
});

View File

@@ -7,6 +7,7 @@ import { RedirectHttpError } from '../../../../src/util/errors/RedirectHttpError
import type { RedirectHttpErrorClass } from '../../../../src/util/errors/RedirectHttpError';
import { SeeOtherHttpError } from '../../../../src/util/errors/SeeOtherHttpError';
import { TemporaryRedirectHttpError } from '../../../../src/util/errors/TemporaryRedirectHttpError';
import { HTTP, SOLID_ERROR, SOLID_HTTP } from '../../../../src/util/Vocabularies';
// Used to make sure the RedirectHttpError constructor also gets called in a test.
class FixedRedirectHttpError extends RedirectHttpError {
@@ -70,7 +71,10 @@ describe('RedirectHttpError', (): void => {
});
it('sets the details.', (): void => {
expect(instance.details).toBe(options.details);
expect(instance.metadata.get(SOLID_ERROR.terms.errorResponse)?.value)
.toBe(`${SOLID_ERROR.namespace}H${statusCode}`);
expect(instance.metadata.get(HTTP.terms.statusCodeNumber)?.value).toBe(`${statusCode}`);
expect(instance.metadata.get(SOLID_HTTP.terms.location)?.value).toBe(location);
});
});
});

View File

@@ -1,4 +1,6 @@
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import { extractErrorTerms } from '../../../../src/util/errors/HttpErrorUtil';
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
import { BaseIdentifierStrategy } from '../../../../src/util/identifiers/BaseIdentifierStrategy';
class DummyStrategy extends BaseIdentifierStrategy {
@@ -21,10 +23,16 @@ describe('A BaseIdentifierStrategy', (): void => {
});
it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise<void> => {
expect((): any => strategy.getParentContainer({ path: '/unsupported' }))
.toThrow('The identifier /unsupported is outside the configured identifier space.');
expect((): any => strategy.getParentContainer({ path: '/unsupported' }))
.toThrow(expect.objectContaining({ errorCode: 'E0001', details: { path: '/unsupported' }}));
let error: unknown;
try {
strategy.getParentContainer({ path: '/unsupported' });
} catch (err: unknown) {
error = err;
}
expect(error).toEqual(expect.objectContaining({ errorCode: 'E0001',
message: 'The identifier /unsupported is outside the configured identifier space.' }));
expect(InternalServerError.isInstance(error)).toBe(true);
expect(extractErrorTerms((error as InternalServerError).metadata)).toEqual({ path: '/unsupported' });
});
it('errors when attempting to get the parent of a root container.', async(): Promise<void> => {