diff --git a/config/util/representation-conversion/converters/errors.json b/config/util/representation-conversion/converters/errors.json index c5bb4dd32..6ac1b8205 100644 --- a/config/util/representation-conversion/converters/errors.json +++ b/config/util/representation-conversion/converters/errors.json @@ -9,19 +9,13 @@ "@graph": [ { "@id": "urn:solid-server:default:ErrorToQuadConverter", - "@type": "ErrorToQuadConverter", + "@type": "ErrorToQuadConverter" }, { "comment": "Converts an error into a Markdown description of its details.", "@id": "urn:solid-server:default:ErrorToTemplateConverter", "@type": "ErrorToTemplateConverter", - "templateEngine": { - "@type": "HandlebarsTemplateEngine", - "template": "$PACKAGE_ROOT/templates/error/main.md.hbs" - }, - "templatePath": "$PACKAGE_ROOT/templates/error/descriptions/", - "extension": ".md.hbs", - "contentType": "text/markdown" + "templateEngine": { "@type": "HandlebarsTemplateEngine" } } ] } diff --git a/src/storage/conversion/ErrorToTemplateConverter.ts b/src/storage/conversion/ErrorToTemplateConverter.ts index 89a70ef48..00b3e058f 100644 --- a/src/storage/conversion/ErrorToTemplateConverter.ts +++ b/src/storage/conversion/ErrorToTemplateConverter.ts @@ -9,6 +9,21 @@ import type { TemplateEngine } from '../../util/templates/TemplateEngine'; import type { RepresentationConverterArgs } from './RepresentationConverter'; import { TypedRepresentationConverter } from './TypedRepresentationConverter'; +// Fields optional due to https://github.com/LinkedSoftwareDependencies/Components.js/issues/20 +export interface TemplateOptions { + mainTemplatePath?: string; + codeTemplatesPath?: string; + extension?: string; + contentType?: string; +} + +const DEFAULT_TEMPLATE_OPTIONS: TemplateOptions = { + mainTemplatePath: '$PACKAGE_ROOT/templates/error/main.md.hbs', + codeTemplatesPath: '$PACKAGE_ROOT/templates/error/descriptions/', + extension: '.md.hbs', + contentType: 'text/markdown', +}; + /** * Serializes an Error by filling in the provided template. * Content-type is based on the constructor parameter. @@ -22,16 +37,22 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter'; */ export class ErrorToTemplateConverter extends TypedRepresentationConverter { private readonly templateEngine: TemplateEngine; - private readonly templatePath: string; + private readonly mainTemplatePath: string; + private readonly codeTemplatesPath: string; private readonly extension: string; private readonly contentType: string; - public constructor(templateEngine: TemplateEngine, templatePath: string, extension: string, contentType: string) { - super(INTERNAL_ERROR, contentType); + public constructor(templateEngine: TemplateEngine, templateOptions?: TemplateOptions) { + super(INTERNAL_ERROR, templateOptions?.contentType ?? DEFAULT_TEMPLATE_OPTIONS.contentType); + // Workaround for https://github.com/LinkedSoftwareDependencies/Components.js/issues/20 + if (!templateOptions || Object.keys(templateOptions).length === 0) { + templateOptions = DEFAULT_TEMPLATE_OPTIONS; + } this.templateEngine = templateEngine; - this.templatePath = templatePath; - this.extension = extension; - this.contentType = contentType; + this.mainTemplatePath = templateOptions.mainTemplatePath!; + this.codeTemplatesPath = templateOptions.codeTemplatesPath!; + this.extension = templateOptions.extension!; + this.contentType = templateOptions.contentType!; } public async handle({ representation }: RepresentationConverterArgs): Promise { @@ -49,7 +70,7 @@ export class ErrorToTemplateConverter extends TypedRepresentationConverter { const templateFile = `${error.errorCode}${this.extension}`; assert(/^[\w.-]+$/u.test(templateFile), 'Invalid error template name'); description = await this.templateEngine.render(error.details ?? {}, - { templateFile, templatePath: this.templatePath }); + { templateFile, templatePath: this.codeTemplatesPath }); } catch { // In case no template is found, or rendering errors, we still want to convert } @@ -58,7 +79,7 @@ export class ErrorToTemplateConverter extends TypedRepresentationConverter { // Render the main template, embedding the rendered error description const { name, message, stack } = error; const variables = { name, message, stack, description }; - const rendered = await this.templateEngine.render(variables); + const rendered = await this.templateEngine.render(variables, { templateFile: this.mainTemplatePath }); return new BasicRepresentation(rendered, representation.metadata, this.contentType); } diff --git a/test/unit/storage/conversion/ErrorToTemplateConverter.test.ts b/test/unit/storage/conversion/ErrorToTemplateConverter.test.ts index 3e6b0354f..ab111e86a 100644 --- a/test/unit/storage/conversion/ErrorToTemplateConverter.test.ts +++ b/test/unit/storage/conversion/ErrorToTemplateConverter.test.ts @@ -7,7 +7,10 @@ import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngi describe('An ErrorToTemplateConverter', (): void => { const identifier = { path: 'http://test.com/error' }; - const templatePath = '/templates/codes'; + const mainTemplatePath = '/templates/main.html'; + const codeTemplatesPath = '/templates/codes'; + const extension = '.html'; + const contentType = 'text/html'; const errorCode = 'E0001'; let templateEngine: jest.Mocked; let converter: ErrorToTemplateConverter; @@ -17,7 +20,8 @@ describe('An ErrorToTemplateConverter', (): void => { templateEngine = { render: jest.fn().mockReturnValue(Promise.resolve('')), }; - converter = new ErrorToTemplateConverter(templateEngine, templatePath, '.html', 'text/html'); + converter = new ErrorToTemplateConverter(templateEngine, + { mainTemplatePath, codeTemplatesPath, extension, contentType }); }); it('supports going from errors to the given content type.', async(): Promise => { @@ -45,6 +49,7 @@ describe('An ErrorToTemplateConverter', (): void => { expect(templateEngine.render).toHaveBeenCalledTimes(1); expect(templateEngine.render).toHaveBeenLastCalledWith( { name: 'Error', message: 'error text', stack: error.stack }, + { templateFile: mainTemplatePath }, ); }); @@ -64,7 +69,8 @@ describe('An ErrorToTemplateConverter', (): void => { {}, { templatePath: '/templates/codes', templateFile: 'H400.html' }); expect(templateEngine.render).toHaveBeenNthCalledWith(2, - { name: 'BadRequestHttpError', message: 'error text', stack: error.stack }); + { name: 'BadRequestHttpError', message: 'error text', stack: error.stack }, + { templateFile: mainTemplatePath }); }); it('only adds stack if it is defined.', async(): Promise => { @@ -84,7 +90,8 @@ describe('An ErrorToTemplateConverter', (): void => { {}, { templatePath: '/templates/codes', templateFile: 'H400.html' }); expect(templateEngine.render).toHaveBeenNthCalledWith(2, - { name: 'BadRequestHttpError', message: 'error text' }); + { name: 'BadRequestHttpError', message: 'error text' }, + { templateFile: mainTemplatePath }); }); it('adds additional information if an error code description is found.', async(): Promise => { @@ -101,7 +108,8 @@ describe('An ErrorToTemplateConverter', (): void => { { key: 'val' }, { templatePath: '/templates/codes', templateFile: 'E0001.html' }); expect(templateEngine.render).toHaveBeenNthCalledWith(2, - { name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '' }); + { name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '' }, + { templateFile: mainTemplatePath }); }); it('sends an empty object for additional error code parameters if none are defined.', async(): Promise => { @@ -119,7 +127,8 @@ describe('An ErrorToTemplateConverter', (): void => { {}, { templatePath: '/templates/codes', templateFile: 'E0001.html' }); expect(templateEngine.render).toHaveBeenNthCalledWith(2, - { name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '' }); + { name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '' }, + { templateFile: mainTemplatePath }); }); it('converts errors with a code as usual if no corresponding template is found.', async(): Promise => { @@ -138,6 +147,26 @@ describe('An ErrorToTemplateConverter', (): void => { {}, { templatePath: '/templates/codes', templateFile: 'invalid.html' }); expect(templateEngine.render).toHaveBeenNthCalledWith(2, - { name: 'BadRequestHttpError', message: 'error text', stack: error.stack }); + { name: 'BadRequestHttpError', message: 'error text', stack: error.stack }, + { templateFile: mainTemplatePath }); + }); + + it('has default template options.', async(): Promise => { + converter = new ErrorToTemplateConverter(templateEngine); + const error = new BadRequestHttpError('error text', { errorCode, details: { key: 'val' }}); + 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('text/markdown'); + await expect(readableToString(result.data)).resolves.toBe(''); + expect(templateEngine.render).toHaveBeenCalledTimes(2); + expect(templateEngine.render).toHaveBeenNthCalledWith(1, + { key: 'val' }, + { templatePath: '$PACKAGE_ROOT/templates/error/descriptions/', templateFile: 'E0001.md.hbs' }); + expect(templateEngine.render).toHaveBeenNthCalledWith(2, + { name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '' }, + { templateFile: '$PACKAGE_ROOT/templates/error/main.md.hbs' }); }); });