import 'jest-rdf'; import arrayifyStream from 'arrayify-stream'; import { DataFactory } from 'n3'; import type { PreferenceParser } from '../../../../../src/http/input/preferences/PreferenceParser'; import { ConvertingErrorHandler } from '../../../../../src/http/output/error/ConvertingErrorHandler'; import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation'; import type { Representation } from '../../../../../src/http/representation/Representation'; import type { RepresentationPreferences } from '../../../../../src/http/representation/RepresentationPreferences'; import type { HttpRequest } from '../../../../../src/server/HttpRequest'; 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 { 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; const request = {} as HttpRequest; let converter: jest.Mocked; let preferenceParser: jest.Mocked; let handler: ConvertingErrorHandler; beforeEach(async(): Promise => { 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; preferenceParser = { canHandle: jest.fn(), handle: jest.fn().mockResolvedValue(preferences), } as any; handler = new ConvertingErrorHandler(converter, preferenceParser, true); }); it('rejects input not supported by the converter.', async(): Promise => { converter.canHandle.mockRejectedValueOnce(new Error('rejected')); await expect(handler.canHandle({ error, request })).rejects.toThrow('rejected'); expect(converter.canHandle).toHaveBeenCalledTimes(1); const args = converter.canHandle.mock.calls[0][0]; expect(args.preferences).toBe(preferences); expect(args.representation.metadata.contentType).toBe('internal/error'); }); it('rejects input not supported by the preference parser.', async(): Promise => { preferenceParser.canHandle.mockRejectedValueOnce(new Error('rejected')); await expect(handler.canHandle({ error, request })).rejects.toThrow('rejected'); expect(preferenceParser.canHandle).toHaveBeenCalledTimes(1); expect(preferenceParser.canHandle).toHaveBeenLastCalledWith({ request }); expect(converter.canHandle).toHaveBeenCalledTimes(0); }); it('accepts input supported by the converter.', async(): Promise => { await expect(handler.canHandle({ error, request })).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 => { const prom = handler.handle({ error, request }); 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 => { const prom = handler.handleSafe({ error, request }); 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 => { handler = new ConvertingErrorHandler(converter, preferenceParser); const prom = handler.handle({ error, request }); 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); }); });