mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add ErrorToHtmlConverter using templates
This commit is contained in:
parent
f054604950
commit
9c0fa77527
@ -24,7 +24,13 @@
|
||||
"converters": [
|
||||
{ "@id": "urn:solid-server:default:RdfToQuadConverter" },
|
||||
{ "@id": "urn:solid-server:default:QuadToRdfConverter" },
|
||||
{ "@type": "ErrorToQuadConverter" }
|
||||
{ "@type": "ErrorToQuadConverter" },
|
||||
{
|
||||
"@type": "ErrorToTemplateConverter",
|
||||
"engine": { "@type": "HandlebarsTemplateEngine" },
|
||||
"templatePath": "$PACKAGE_ROOT/templates/error/error.hbs",
|
||||
"contentType": "text/html"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -220,6 +220,7 @@ export * from './storage/conversion/ChainedConverter';
|
||||
export * from './storage/conversion/ConstantConverter';
|
||||
export * from './storage/conversion/ContentTypeReplacer';
|
||||
export * from './storage/conversion/ConversionUtil';
|
||||
export * from './storage/conversion/ErrorToTemplateConverter';
|
||||
export * from './storage/conversion/ErrorToQuadConverter';
|
||||
export * from './storage/conversion/IfNeededConverter';
|
||||
export * from './storage/conversion/PassthroughConverter';
|
||||
|
43
src/storage/conversion/ErrorToTemplateConverter.ts
Normal file
43
src/storage/conversion/ErrorToTemplateConverter.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import type { TemplateEngine } from '../../pods/generate/TemplateEngine';
|
||||
import { INTERNAL_ERROR } from '../../util/ContentTypes';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { resolveAssetPath } from '../../util/PathUtil';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
|
||||
/**
|
||||
* Serializes an Error by filling in the provided template.
|
||||
* Content-type is based on the constructor parameter.
|
||||
*/
|
||||
export class ErrorToTemplateConverter extends TypedRepresentationConverter {
|
||||
private readonly engine: TemplateEngine;
|
||||
private readonly templatePath: string;
|
||||
private readonly contentType: string;
|
||||
|
||||
public constructor(engine: TemplateEngine, templatePath: string, contentType: string) {
|
||||
super(INTERNAL_ERROR, contentType);
|
||||
this.engine = engine;
|
||||
this.templatePath = resolveAssetPath(templatePath);
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public async handle({ 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;
|
||||
|
||||
// Render the template
|
||||
const { name, message, stack } = error;
|
||||
const variables = { name, message, stack };
|
||||
const template = await fsPromises.readFile(this.templatePath, 'utf8');
|
||||
const html = this.engine.apply(template, variables);
|
||||
|
||||
return new BasicRepresentation(html, representation.metadata, this.contentType);
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
// Well-known content types
|
||||
export const TEXT_TURTLE = 'text/turtle';
|
||||
export const APPLICATION_JSON = 'application/json';
|
||||
export const APPLICATION_OCTET_STREAM = 'application/octet-stream';
|
||||
export const APPLICATION_SPARQL_UPDATE = 'application/sparql-update';
|
||||
export const APPLICATION_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded';
|
||||
export const TEXT_HTML = 'text/html';
|
||||
export const TEXT_TURTLE = 'text/turtle';
|
||||
|
||||
// Internal content types (not exposed over HTTP)
|
||||
export const INTERNAL_ALL = 'internal/*';
|
||||
|
15
templates/error/error.hbs
Normal file
15
templates/error/error.hbs
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{{ name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ name }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
{{#if stack}}
|
||||
<pre><code>{{ stack }}</code></pre>
|
||||
{{/if}}
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,75 @@
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { TemplateEngine } from '../../../../src/pods/generate/TemplateEngine';
|
||||
import { ErrorToTemplateConverter } from '../../../../src/storage/conversion/ErrorToTemplateConverter';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
||||
|
||||
const mockRead = jest.fn().mockResolvedValue('{{ template }}');
|
||||
jest.mock('fs', (): any => ({
|
||||
promises: { readFile: (...args: any[]): any => mockRead(...args) },
|
||||
}));
|
||||
|
||||
describe('An ErrorToTemplateConverter', (): void => {
|
||||
const identifier = { path: 'http://test.com/error' };
|
||||
let engine: TemplateEngine;
|
||||
const path = '/template/error.template';
|
||||
let converter: ErrorToTemplateConverter;
|
||||
const preferences = {};
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
mockRead.mockClear();
|
||||
engine = {
|
||||
apply: jest.fn().mockReturnValue('<html>'),
|
||||
};
|
||||
|
||||
converter = new ErrorToTemplateConverter(engine, path, 'text/html');
|
||||
});
|
||||
|
||||
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({ 'text/html': 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('calls the template engine with 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(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(mockRead).toHaveBeenCalledTimes(1);
|
||||
expect(mockRead).toHaveBeenLastCalledWith(path, 'utf8');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}', { name: 'BadRequestHttpError', message: 'error text', stack: 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(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(mockRead).toHaveBeenCalledTimes(1);
|
||||
expect(mockRead).toHaveBeenLastCalledWith(path, 'utf8');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}', { name: 'BadRequestHttpError', message: 'error text' },
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user