mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create ErrorHandler to convert errors to Representations
This commit is contained in:
parent
3ef815ee6d
commit
e1f95877da
@ -11,7 +11,7 @@
|
||||
"MappedMetadataWriter:_headerMap_value": "Content-Type"
|
||||
},
|
||||
{
|
||||
"MappedMetadataWriter:_headerMap_key": "urn:solid:http:location",
|
||||
"MappedMetadataWriter:_headerMap_key": "urn:npm:solid:community-server:http:location",
|
||||
"MappedMetadataWriter:_headerMap_value": "Location"
|
||||
}
|
||||
]
|
||||
|
94
src/ldp/http/ConvertingErrorHandler.ts
Normal file
94
src/ldp/http/ConvertingErrorHandler.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import type {
|
||||
RepresentationConverter,
|
||||
RepresentationConverterArgs,
|
||||
} from '../../storage/conversion/RepresentationConverter';
|
||||
import { INTERNAL_ERROR } from '../../util/ContentTypes';
|
||||
import { getStatusCode } from '../../util/errors/ErrorUtil';
|
||||
import { toLiteral } from '../../util/TermUtil';
|
||||
import { HTTP, XSD } from '../../util/Vocabularies';
|
||||
import { BasicRepresentation } from '../representation/BasicRepresentation';
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||
import type { ErrorHandlerArgs } from './ErrorHandler';
|
||||
import { ErrorHandler } from './ErrorHandler';
|
||||
import type { ResponseDescription } from './response/ResponseDescription';
|
||||
|
||||
// Used by internal helper function
|
||||
type PreparedArguments = {
|
||||
statusCode: number;
|
||||
conversionArgs: RepresentationConverterArgs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an error into a Representation of content type internal/error.
|
||||
* Then feeds that representation into its converter to create a representation based on the given preferences.
|
||||
*/
|
||||
export class ConvertingErrorHandler extends ErrorHandler {
|
||||
private readonly converter: RepresentationConverter;
|
||||
private readonly showStackTrace: boolean;
|
||||
|
||||
public constructor(converter: RepresentationConverter, showStackTrace = false) {
|
||||
super();
|
||||
this.converter = converter;
|
||||
this.showStackTrace = showStackTrace;
|
||||
}
|
||||
|
||||
public async canHandle(input: ErrorHandlerArgs): Promise<void> {
|
||||
const { conversionArgs } = this.prepareArguments(input);
|
||||
|
||||
await this.converter.canHandle(conversionArgs);
|
||||
}
|
||||
|
||||
public async handle(input: ErrorHandlerArgs): Promise<ResponseDescription> {
|
||||
const { statusCode, conversionArgs } = this.prepareArguments(input);
|
||||
|
||||
const converted = await this.converter.handle(conversionArgs);
|
||||
|
||||
return this.createResponse(statusCode, converted);
|
||||
}
|
||||
|
||||
public async handleSafe(input: ErrorHandlerArgs): Promise<ResponseDescription> {
|
||||
const { statusCode, conversionArgs } = this.prepareArguments(input);
|
||||
|
||||
const converted = await this.converter.handleSafe(conversionArgs);
|
||||
|
||||
return this.createResponse(statusCode, converted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the arguments used by all functions.
|
||||
*/
|
||||
private prepareArguments({ error, preferences }: ErrorHandlerArgs): PreparedArguments {
|
||||
const statusCode = getStatusCode(error);
|
||||
const representation = this.toRepresentation(error, statusCode);
|
||||
const identifier = { path: representation.metadata.identifier.value };
|
||||
return { statusCode, conversionArgs: { identifier, representation, preferences }};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ResponseDescription based on the Representation.
|
||||
*/
|
||||
private createResponse(statusCode: number, converted: Representation): ResponseDescription {
|
||||
return {
|
||||
statusCode,
|
||||
metadata: converted.metadata,
|
||||
data: converted.data,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Representation based on the given error.
|
||||
* Content type will be internal/error.
|
||||
* The status code is used for metadata.
|
||||
*/
|
||||
private toRepresentation(error: Error, statusCode: number): Representation {
|
||||
const metadata = new RepresentationMetadata(INTERNAL_ERROR);
|
||||
metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
|
||||
|
||||
if (!this.showStackTrace) {
|
||||
delete error.stack;
|
||||
}
|
||||
|
||||
return new BasicRepresentation([ error ], metadata, false);
|
||||
}
|
||||
}
|
13
src/ldp/http/ErrorHandler.ts
Normal file
13
src/ldp/http/ErrorHandler.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
||||
import type { ResponseDescription } from './response/ResponseDescription';
|
||||
|
||||
export interface ErrorHandlerArgs {
|
||||
error: Error;
|
||||
preferences: RepresentationPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an error into a {@link ResponseDescription} based on the request preferences.
|
||||
*/
|
||||
export abstract class ErrorHandler extends AsyncHandler<ErrorHandlerArgs, ResponseDescription> {}
|
37
src/ldp/http/TextErrorHandler.ts
Normal file
37
src/ldp/http/TextErrorHandler.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { getStatusCode } from '../../util/errors/ErrorUtil';
|
||||
import { guardedStreamFrom } from '../../util/StreamUtil';
|
||||
import { toLiteral } from '../../util/TermUtil';
|
||||
import { HTTP, XSD } from '../../util/Vocabularies';
|
||||
import { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||
import type { ErrorHandlerArgs } from './ErrorHandler';
|
||||
import { ErrorHandler } from './ErrorHandler';
|
||||
import type { ResponseDescription } from './response/ResponseDescription';
|
||||
|
||||
/**
|
||||
* Returns a simple text description of an error.
|
||||
* This class is mostly a failsafe in case all other solutions fail.
|
||||
*/
|
||||
export class TextErrorHandler extends ErrorHandler {
|
||||
private readonly showStackTrace: boolean;
|
||||
|
||||
public constructor(showStackTrace = false) {
|
||||
super();
|
||||
this.showStackTrace = showStackTrace;
|
||||
}
|
||||
|
||||
public async handle({ error }: ErrorHandlerArgs): Promise<ResponseDescription> {
|
||||
const statusCode = getStatusCode(error);
|
||||
const metadata = new RepresentationMetadata('text/plain');
|
||||
metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
|
||||
|
||||
const text = typeof error.stack === 'string' && this.showStackTrace ?
|
||||
`${error.stack}\n` :
|
||||
`${error.name}: ${error.message}\n`;
|
||||
|
||||
return {
|
||||
statusCode,
|
||||
metadata,
|
||||
data: guardedStreamFrom(text),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||
import type { HttpRequest } from '../../../server/HttpRequest';
|
||||
import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
|
||||
import { HTTP } from '../../../util/Vocabularies';
|
||||
import { SOLID_HTTP } from '../../../util/Vocabularies';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataParser } from './MetadataParser';
|
||||
|
||||
@ -19,7 +19,7 @@ export class SlugParser extends MetadataParser {
|
||||
throw new BadRequestHttpError('Request has multiple Slug headers');
|
||||
}
|
||||
this.logger.debug(`Request Slug is '${slug}'.`);
|
||||
input.metadata.set(HTTP.slug, slug);
|
||||
input.metadata.set(SOLID_HTTP.slug, slug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataFactory } from 'n3';
|
||||
import { HTTP } from '../../../util/Vocabularies';
|
||||
import { SOLID_HTTP } from '../../../util/Vocabularies';
|
||||
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../representation/ResourceIdentifier';
|
||||
import { ResponseDescription } from './ResponseDescription';
|
||||
@ -9,7 +9,7 @@ import { ResponseDescription } from './ResponseDescription';
|
||||
*/
|
||||
export class CreatedResponseDescription extends ResponseDescription {
|
||||
public constructor(location: ResourceIdentifier) {
|
||||
const metadata = new RepresentationMetadata({ [HTTP.location]: DataFactory.namedNode(location.path) });
|
||||
const metadata = new RepresentationMetadata({ [SOLID_HTTP.location]: DataFactory.namedNode(location.path) });
|
||||
super(201, metadata);
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
} from '../util/PathUtil';
|
||||
import { parseQuads } from '../util/QuadUtil';
|
||||
import { addResourceMetadata } from '../util/ResourceUtil';
|
||||
import { CONTENT_TYPE, DC, HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies';
|
||||
import { CONTENT_TYPE, DC, SOLID_HTTP, LDP, POSIX, PIM, RDF, VANN, XSD } from '../util/Vocabularies';
|
||||
import type { DataAccessor } from './accessors/DataAccessor';
|
||||
import type { ResourceStore } from './ResourceStore';
|
||||
|
||||
@ -407,8 +407,8 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
Promise<ResourceIdentifier> {
|
||||
// Get all values needed for naming the resource
|
||||
const isContainer = this.isNewContainer(metadata);
|
||||
const slug = metadata.get(HTTP.slug)?.value;
|
||||
metadata.removeAll(HTTP.slug);
|
||||
const slug = metadata.get(SOLID_HTTP.slug)?.value;
|
||||
metadata.removeAll(SOLID_HTTP.slug);
|
||||
|
||||
let newID: ResourceIdentifier = this.createURI(container, isContainer, slug);
|
||||
|
||||
@ -439,7 +439,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
if (this.hasContainerType(metadata.getAll(RDF.type))) {
|
||||
return true;
|
||||
}
|
||||
const slug = suffix ?? metadata.get(HTTP.slug)?.value;
|
||||
const slug = suffix ?? metadata.get(SOLID_HTTP.slug)?.value;
|
||||
return Boolean(slug && isContainerPath(slug));
|
||||
}
|
||||
|
||||
|
37
src/storage/conversion/ErrorToQuadConverter.ts
Normal file
37
src/storage/conversion/ErrorToQuadConverter.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { DC, SOLID_ERROR } from '../../util/Vocabularies';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
|
||||
/**
|
||||
* Converts an error object into quads by creating a triple for each of name/message/stack.
|
||||
*/
|
||||
export class ErrorToQuadConverter extends TypedRepresentationConverter {
|
||||
public constructor() {
|
||||
super(INTERNAL_ERROR, INTERNAL_QUADS);
|
||||
}
|
||||
|
||||
public async handle({ identifier, representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||
const errors = await arrayifyStream(representation.data);
|
||||
if (errors.length !== 1) {
|
||||
throw new InternalServerError('Only single errors are supported.');
|
||||
}
|
||||
const error = errors[0] as Error;
|
||||
|
||||
// A metadata object makes it easier to add triples due to the utility functions
|
||||
const data = new RepresentationMetadata(identifier);
|
||||
data.add(DC.terms.title, error.name);
|
||||
data.add(DC.terms.description, error.message);
|
||||
if (error.stack) {
|
||||
data.add(SOLID_ERROR.terms.stack, error.stack);
|
||||
}
|
||||
|
||||
// Update the content-type to quads
|
||||
return new BasicRepresentation(data.quads(), representation.metadata, INTERNAL_QUADS, false);
|
||||
}
|
||||
}
|
@ -8,3 +8,4 @@ export const APPLICATION_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlenco
|
||||
// Internal content types (not exposed over HTTP)
|
||||
export const INTERNAL_ALL = 'internal/*';
|
||||
export const INTERNAL_QUADS = 'internal/quads';
|
||||
export const INTERNAL_ERROR = 'internal/error';
|
||||
|
@ -75,16 +75,17 @@ export const AUTH = createUriAndTermNamespace('urn:solid:auth:',
|
||||
);
|
||||
|
||||
export const DC = createUriAndTermNamespace('http://purl.org/dc/terms/',
|
||||
'description',
|
||||
'modified',
|
||||
'title',
|
||||
);
|
||||
|
||||
export const FOAF = createUriAndTermNamespace('http://xmlns.com/foaf/0.1/',
|
||||
'Agent',
|
||||
);
|
||||
|
||||
export const HTTP = createUriAndTermNamespace('urn:solid:http:',
|
||||
'location',
|
||||
'slug',
|
||||
export const HTTP = createUriAndTermNamespace('http://www.w3.org/2011/http#',
|
||||
'statusCodeNumber',
|
||||
);
|
||||
|
||||
export const LDP = createUriAndTermNamespace('http://www.w3.org/ns/ldp#',
|
||||
@ -112,6 +113,21 @@ export const RDF = createUriAndTermNamespace('http://www.w3.org/1999/02/22-rdf-s
|
||||
'type',
|
||||
);
|
||||
|
||||
export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms#',
|
||||
'oidcIssuer',
|
||||
'oidcIssuerRegistrationToken',
|
||||
'oidcRegistration',
|
||||
);
|
||||
|
||||
export const SOLID_ERROR = createUriAndTermNamespace('urn:npm:solid:community-server:error:',
|
||||
'stack',
|
||||
);
|
||||
|
||||
export const SOLID_HTTP = createUriAndTermNamespace('urn:npm:solid:community-server:http:',
|
||||
'location',
|
||||
'slug',
|
||||
);
|
||||
|
||||
export const VANN = createUriAndTermNamespace('http://purl.org/vocab/vann/',
|
||||
'preferredNamespacePrefix',
|
||||
);
|
||||
@ -121,12 +137,6 @@ export const XSD = createUriAndTermNamespace('http://www.w3.org/2001/XMLSchema#'
|
||||
'integer',
|
||||
);
|
||||
|
||||
export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms#',
|
||||
'oidcIssuer',
|
||||
'oidcIssuerRegistrationToken',
|
||||
'oidcRegistration',
|
||||
);
|
||||
|
||||
// Alias for commonly used types
|
||||
export const CONTENT_TYPE = MA.format;
|
||||
export const CONTENT_TYPE_TERM = MA.terms.format;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { types } from 'util';
|
||||
import { HttpError } from './HttpError';
|
||||
|
||||
/**
|
||||
* Checks if the input is an {@link Error}.
|
||||
@ -6,3 +7,20 @@ import { types } from 'util';
|
||||
export function isNativeError(error: any): error is Error {
|
||||
return types.isNativeError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the input is a native error.
|
||||
* If not the input will be re-thrown.
|
||||
*/
|
||||
export function assertNativeError(error: any): asserts error is Error {
|
||||
if (!isNativeError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTTP status code corresponding to the error.
|
||||
*/
|
||||
export function getStatusCode(error: Error): number {
|
||||
return HttpError.isInstance(error) ? error.statusCode : 500;
|
||||
}
|
||||
|
95
test/unit/ldp/http/ConvertingErrorHandler.test.ts
Normal file
95
test/unit/ldp/http/ConvertingErrorHandler.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import 'jest-rdf';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory } from 'n3';
|
||||
import { ConvertingErrorHandler } from '../../../../src/ldp/http/ConvertingErrorHandler';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../../../src/ldp/representation/Representation';
|
||||
import type { RepresentationPreferences } from '../../../../src/ldp/representation/RepresentationPreferences';
|
||||
import type {
|
||||
RepresentationConverter,
|
||||
RepresentationConverterArgs,
|
||||
} from '../../../../src/storage/conversion/RepresentationConverter';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
import { HTTP, XSD } from '../../../../src/util/Vocabularies';
|
||||
import literal = DataFactory.literal;
|
||||
|
||||
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
|
||||
|
||||
async function expectValidArgs(args: RepresentationConverterArgs, stack?: string): Promise<void> {
|
||||
expect(args.preferences).toBe(preferences);
|
||||
expect(args.representation.metadata.get(HTTP.terms.statusCodeNumber))
|
||||
.toEqualRdfTerm(literal(404, XSD.terms.integer));
|
||||
expect(args.representation.metadata.contentType).toBe('internal/error');
|
||||
|
||||
// Error contents
|
||||
const errorArray = await arrayifyStream(args.representation.data);
|
||||
expect(errorArray).toHaveLength(1);
|
||||
const resultError = errorArray[0];
|
||||
expect(resultError).toMatchObject({ name: 'NotFoundHttpError', message: 'not here' });
|
||||
expect(resultError.stack).toBe(stack);
|
||||
}
|
||||
|
||||
describe('A ConvertingErrorHandler', (): void => {
|
||||
// The error object can get modified by the handler
|
||||
let error: Error;
|
||||
let stack: string | undefined;
|
||||
let converter: RepresentationConverter;
|
||||
let handler: ConvertingErrorHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
error = new NotFoundHttpError('not here');
|
||||
({ stack } = error);
|
||||
converter = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
|
||||
handleSafe: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
|
||||
} as any;
|
||||
|
||||
handler = new ConvertingErrorHandler(converter, true);
|
||||
});
|
||||
|
||||
it('rejects input not supported by the converter.', async(): Promise<void> => {
|
||||
(converter.canHandle as jest.Mock).mockRejectedValueOnce(new Error('rejected'));
|
||||
await expect(handler.canHandle({ error, preferences })).rejects.toThrow('rejected');
|
||||
expect(converter.canHandle).toHaveBeenCalledTimes(1);
|
||||
const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
||||
expect(args.preferences).toBe(preferences);
|
||||
expect(args.representation.metadata.contentType).toBe('internal/error');
|
||||
});
|
||||
|
||||
it('accepts input supported by the converter.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({ error, preferences })).resolves.toBeUndefined();
|
||||
expect(converter.canHandle).toHaveBeenCalledTimes(1);
|
||||
const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
||||
expect(args.preferences).toBe(preferences);
|
||||
expect(args.representation.metadata.contentType).toBe('internal/error');
|
||||
});
|
||||
|
||||
it('returns the converted error response.', async(): Promise<void> => {
|
||||
const prom = handler.handle({ error, preferences });
|
||||
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
|
||||
expect((await prom).metadata?.contentType).toBe('text/turtle');
|
||||
expect(converter.handle).toHaveBeenCalledTimes(1);
|
||||
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
||||
await expectValidArgs(args, stack);
|
||||
});
|
||||
|
||||
it('uses the handleSafe function of the converter during its own handleSafe call.', async(): Promise<void> => {
|
||||
const prom = handler.handleSafe({ error, preferences });
|
||||
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
|
||||
expect((await prom).metadata?.contentType).toBe('text/turtle');
|
||||
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
|
||||
const args = (converter.handleSafe as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
||||
await expectValidArgs(args, stack);
|
||||
});
|
||||
|
||||
it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
|
||||
handler = new ConvertingErrorHandler(converter);
|
||||
const prom = handler.handle({ error, preferences });
|
||||
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
|
||||
expect((await prom).metadata?.contentType).toBe('text/turtle');
|
||||
expect(converter.handle).toHaveBeenCalledTimes(1);
|
||||
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
|
||||
await expectValidArgs(args);
|
||||
});
|
||||
});
|
60
test/unit/ldp/http/TextErrorHandler.test.ts
Normal file
60
test/unit/ldp/http/TextErrorHandler.test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import 'jest-rdf';
|
||||
import { DataFactory } from 'n3';
|
||||
import stringifyStream from 'stream-to-string';
|
||||
import { TextErrorHandler } from '../../../../src/ldp/http/TextErrorHandler';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
import { HTTP, XSD } from '../../../../src/util/Vocabularies';
|
||||
import literal = DataFactory.literal;
|
||||
|
||||
describe('A TextErrorHandler', (): void => {
|
||||
// The error object can get modified by the handler
|
||||
let error: Error;
|
||||
let stack: string | undefined;
|
||||
let handler: TextErrorHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
error = new NotFoundHttpError('not here');
|
||||
({ stack } = error);
|
||||
|
||||
handler = new TextErrorHandler(true);
|
||||
});
|
||||
|
||||
it('can handle everything.', async(): Promise<void> => {
|
||||
await expect(handler.canHandle({} as any)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('creates a text representation of the error.', async(): Promise<void> => {
|
||||
const prom = handler.handle({ error } as any);
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
const result = await prom;
|
||||
expect(result.statusCode).toBe(404);
|
||||
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
|
||||
expect(result.metadata?.contentType).toBe('text/plain');
|
||||
const text = await stringifyStream(result.data!);
|
||||
expect(text).toBe(`${stack}\n`);
|
||||
});
|
||||
|
||||
it('concatenates name and message if there is no stack.', async(): Promise<void> => {
|
||||
delete error.stack;
|
||||
const prom = handler.handle({ error } as any);
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
const result = await prom;
|
||||
expect(result.statusCode).toBe(404);
|
||||
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
|
||||
expect(result.metadata?.contentType).toBe('text/plain');
|
||||
const text = await stringifyStream(result.data!);
|
||||
expect(text).toBe(`NotFoundHttpError: not here\n`);
|
||||
});
|
||||
|
||||
it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
|
||||
handler = new TextErrorHandler();
|
||||
const prom = handler.handle({ error } as any);
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
const result = await prom;
|
||||
expect(result.statusCode).toBe(404);
|
||||
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
|
||||
expect(result.metadata?.contentType).toBe('text/plain');
|
||||
const text = await stringifyStream(result.data!);
|
||||
expect(text).toBe(`NotFoundHttpError: not here\n`);
|
||||
});
|
||||
});
|
@ -2,7 +2,7 @@ import { SlugParser } from '../../../../../src/ldp/http/metadata/SlugParser';
|
||||
import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
||||
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
|
||||
import { HTTP } from '../../../../../src/util/Vocabularies';
|
||||
import { SOLID_HTTP } from '../../../../../src/util/Vocabularies';
|
||||
|
||||
describe('A SlugParser', (): void => {
|
||||
const parser = new SlugParser();
|
||||
@ -30,6 +30,6 @@ describe('A SlugParser', (): void => {
|
||||
request.headers.slug = 'slugA';
|
||||
await expect(parser.handle({ request, metadata })).resolves.toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(1);
|
||||
expect(metadata.get(HTTP.slug)?.value).toBe('slugA');
|
||||
expect(metadata.get(SOLID_HTTP.slug)?.value).toBe('slugA');
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import type { ResourceIdentifier } from '../../../../src/ldp/representation/Reso
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||
import { HTTP } from '../../../../src/util/Vocabularies';
|
||||
import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
||||
|
||||
describe('A PostOperationHandler', (): void => {
|
||||
const store = {
|
||||
@ -31,7 +31,7 @@ describe('A PostOperationHandler', (): void => {
|
||||
const result = await handler.handle({ method: 'POST', body: { metadata }} as Operation);
|
||||
expect(result.statusCode).toBe(201);
|
||||
expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
|
||||
expect(result.metadata?.get(HTTP.location)?.value).toBe('newPath');
|
||||
expect(result.metadata?.get(SOLID_HTTP.location)?.value).toBe('newPath');
|
||||
expect(result.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
@ -22,7 +22,7 @@ import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/Sing
|
||||
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
|
||||
import * as quadUtil from '../../../src/util/QuadUtil';
|
||||
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
|
||||
import { CONTENT_TYPE, HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
|
||||
import { CONTENT_TYPE, SOLID_HTTP, LDP, PIM, RDF } from '../../../src/util/Vocabularies';
|
||||
import quad = DataFactory.quad;
|
||||
import namedNode = DataFactory.namedNode;
|
||||
|
||||
@ -277,7 +277,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
it('creates a URI based on the incoming slug.', async(): Promise<void> => {
|
||||
const resourceID = { path: root };
|
||||
representation.metadata.removeAll(RDF.type);
|
||||
representation.metadata.add(HTTP.slug, 'newName');
|
||||
representation.metadata.add(SOLID_HTTP.slug, 'newName');
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result).toEqual({
|
||||
path: `${root}newName`,
|
||||
@ -286,7 +286,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
it('generates a new URI if adding the slug would create an existing URI.', async(): Promise<void> => {
|
||||
const resourceID = { path: root };
|
||||
representation.metadata.add(HTTP.slug, 'newName');
|
||||
representation.metadata.add(SOLID_HTTP.slug, 'newName');
|
||||
accessor.data[`${root}newName`] = representation;
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result).not.toEqual({
|
||||
@ -300,7 +300,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
it('generates http://test.com/%26%26 when slug is &%26.', async(): Promise<void> => {
|
||||
const resourceID = { path: root };
|
||||
representation.metadata.removeAll(RDF.type);
|
||||
representation.metadata.add(HTTP.slug, '&%26');
|
||||
representation.metadata.add(SOLID_HTTP.slug, '&%26');
|
||||
const result = await store.addResource(resourceID, representation);
|
||||
expect(result).toEqual({ path: `${root}%26%26` });
|
||||
});
|
||||
@ -309,7 +309,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const resourceID = { path: root };
|
||||
representation.metadata.removeAll(RDF.type);
|
||||
representation.data = guardedStreamFrom([ `` ]);
|
||||
representation.metadata.add(HTTP.slug, 'sla/sh/es');
|
||||
representation.metadata.add(SOLID_HTTP.slug, 'sla/sh/es');
|
||||
const result = store.addResource(resourceID, representation);
|
||||
await expect(result).rejects.toThrow(BadRequestHttpError);
|
||||
await expect(result).rejects.toThrow('Slugs should not contain slashes');
|
||||
@ -318,7 +318,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
it('errors if the slug would cause an auxiliary resource URI to be generated.', async(): Promise<void> => {
|
||||
const resourceID = { path: root };
|
||||
representation.metadata.removeAll(RDF.type);
|
||||
representation.metadata.add(HTTP.slug, 'test.dummy');
|
||||
representation.metadata.add(SOLID_HTTP.slug, 'test.dummy');
|
||||
const result = store.addResource(resourceID, representation);
|
||||
await expect(result).rejects.toThrow(ForbiddenHttpError);
|
||||
await expect(result).rejects.toThrow('Slug bodies that would result in an auxiliary resource are forbidden');
|
||||
|
59
test/unit/storage/conversion/ErrorToQuadConverter.test.ts
Normal file
59
test/unit/storage/conversion/ErrorToQuadConverter.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import 'jest-rdf';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory } from 'n3';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import { ErrorToQuadConverter } from '../../../../src/storage/conversion/ErrorToQuadConverter';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||
import { DC, SOLID_ERROR } from '../../../../src/util/Vocabularies';
|
||||
const { literal, namedNode, quad } = DataFactory;
|
||||
|
||||
describe('An ErrorToQuadConverter', (): void => {
|
||||
const identifier = { path: 'http://test.com/error' };
|
||||
const converter = new ErrorToQuadConverter();
|
||||
const preferences = {};
|
||||
|
||||
it('supports going from errors to quads.', async(): Promise<void> => {
|
||||
await expect(converter.getInputTypes()).resolves.toEqual({ 'internal/error': 1 });
|
||||
await expect(converter.getOutputTypes()).resolves.toEqual({ 'internal/quads': 1 });
|
||||
});
|
||||
|
||||
it('does not support multiple errors.', async(): Promise<void> => {
|
||||
const representation = new BasicRepresentation([ new Error(), new Error() ], 'internal/error', false);
|
||||
const prom = converter.handle({ identifier, representation, preferences });
|
||||
await expect(prom).rejects.toThrow('Only single errors are supported.');
|
||||
await expect(prom).rejects.toThrow(InternalServerError);
|
||||
});
|
||||
|
||||
it('adds triples for all error fields.', async(): Promise<void> => {
|
||||
const error = new BadRequestHttpError('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(false);
|
||||
expect(result.metadata.contentType).toBe('internal/quads');
|
||||
const quads = await arrayifyStream(result.data);
|
||||
expect(quads).toBeRdfIsomorphic([
|
||||
quad(namedNode(identifier.path), DC.terms.title, literal('BadRequestHttpError')),
|
||||
quad(namedNode(identifier.path), DC.terms.description, literal('error text')),
|
||||
quad(namedNode(identifier.path), SOLID_ERROR.terms.stack, literal(error.stack!)),
|
||||
]);
|
||||
});
|
||||
|
||||
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();
|
||||
const result = await prom;
|
||||
expect(result.binary).toBe(false);
|
||||
expect(result.metadata.contentType).toBe('internal/quads');
|
||||
const quads = await arrayifyStream(result.data);
|
||||
expect(quads).toBeRdfIsomorphic([
|
||||
quad(namedNode(identifier.path), DC.terms.title, literal('BadRequestHttpError')),
|
||||
quad(namedNode(identifier.path), DC.terms.description, literal('error text')),
|
||||
]);
|
||||
});
|
||||
});
|
34
test/unit/util/errors/ErrorUtil.test.ts
Normal file
34
test/unit/util/errors/ErrorUtil.test.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { assertNativeError, getStatusCode, isNativeError } from '../../../../src/util/errors/ErrorUtil';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
|
||||
describe('ErrorUtil', (): void => {
|
||||
describe('#isNativeError', (): void => {
|
||||
it('returns true on native errors.', async(): Promise<void> => {
|
||||
expect(isNativeError(new Error('error'))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false on other values.', async(): Promise<void> => {
|
||||
expect(isNativeError('apple')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#assertNativeError', (): void => {
|
||||
it('returns undefined on native errors.', async(): Promise<void> => {
|
||||
expect(assertNativeError(new Error('error'))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws on other values.', async(): Promise<void> => {
|
||||
expect((): void => assertNativeError('apple')).toThrow('apple');
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user