refactor: Align EJS engine with Handlebars.

This commit is contained in:
Ruben Verborgh
2021-07-20 19:39:28 +02:00
committed by Joachim Van Herwegen
parent 19624dc729
commit 9628fe98b8
40 changed files with 215 additions and 266 deletions

View File

@@ -48,6 +48,5 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
'urn:solid-server:default:variable:port': port,
'urn:solid-server:default:variable:loggingLevel': 'off',
'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../templates/idp'),
};
}

View File

@@ -5,9 +5,9 @@ import {
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
import type { EmailSender } from '../../../../../../src/identity/interaction/util/EmailSender';
import type { IdpRenderHandler } from '../../../../../../src/identity/interaction/util/IdpRenderHandler';
import type { TemplateRenderer } from '../../../../../../src/identity/interaction/util/TemplateRenderer';
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
import type { TemplateEngine } from '../../../../../../src/util/templates/TemplateEngine';
import { createPostFormRequest } from './Util';
describe('A ForgotPasswordHandler', (): void => {
@@ -16,13 +16,13 @@ describe('A ForgotPasswordHandler', (): void => {
const email = 'test@test.email';
const recordId = '123456';
const html = `<a href="/base/idp/resetpassword?rid=${recordId}">Reset Password</a>`;
const renderParams = { response, props: { errorMessage: '', prefilled: { email }}};
const renderParams = { response, contents: { errorMessage: '', prefilled: { email }}};
const provider: Provider = {} as any;
let messageRenderHandler: IdpRenderHandler;
let accountStore: AccountStore;
const baseUrl = 'http://test.com/base/';
const idpPath = '/idp';
let emailTemplateRenderer: TemplateRenderer<{ resetLink: string }>;
let templateEngine: TemplateEngine<{ resetLink: string }>;
let emailSender: EmailSender;
let handler: ForgotPasswordHandler;
@@ -37,8 +37,8 @@ describe('A ForgotPasswordHandler', (): void => {
generateForgotPasswordRecord: jest.fn().mockResolvedValue(recordId),
} as any;
emailTemplateRenderer = {
handleSafe: jest.fn().mockResolvedValue(html),
templateEngine = {
render: jest.fn().mockResolvedValue(html),
} as any;
emailSender = {
@@ -50,7 +50,7 @@ describe('A ForgotPasswordHandler', (): void => {
accountStore,
baseUrl,
idpPath,
emailTemplateRenderer,
templateEngine,
emailSender,
});
});

View File

@@ -10,7 +10,7 @@ import type { IdentifierGenerator } from '../../../../../../src/pods/generate/Id
import type { PodManager } from '../../../../../../src/pods/PodManager';
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
import type { RenderHandler } from '../../../../../../src/server/util/RenderHandler';
import type { TemplateHandler } from '../../../../../../src/server/util/TemplateHandler';
import { createPostFormRequest } from './Util';
describe('A RegistrationHandler', (): void => {
@@ -34,7 +34,7 @@ describe('A RegistrationHandler', (): void => {
let ownershipValidator: OwnershipValidator;
let accountStore: AccountStore;
let podManager: PodManager;
let responseHandler: RenderHandler<NodeJS.Dict<any>>;
let responseHandler: TemplateHandler<NodeJS.Dict<any>>;
let handler: RegistrationHandler;
beforeEach(async(): Promise<void> => {

View File

@@ -7,7 +7,7 @@ import type {
import type { AccountStore } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
import type { HttpRequest } from '../../../../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../../../../src/server/HttpResponse';
import type { RenderHandler } from '../../../../../../src/server/util/RenderHandler';
import type { TemplateHandler } from '../../../../../../src/server/util/TemplateHandler';
import { createPostFormRequest } from './Util';
describe('A ResetPasswordHandler', (): void => {
@@ -17,7 +17,7 @@ describe('A ResetPasswordHandler', (): void => {
const email = 'alice@test.email';
let accountStore: AccountStore;
let renderHandler: ResetPasswordRenderHandler;
let messageRenderHandler: RenderHandler<{ message: string }>;
let messageRenderHandler: TemplateHandler<{ message: string }>;
let handler: ResetPasswordHandler;
beforeEach(async(): Promise<void> => {
@@ -47,11 +47,11 @@ describe('A ResetPasswordHandler', (): void => {
request = createPostFormRequest({});
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, props: { errorMessage, recordId: '' }});
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, contents: { errorMessage, recordId: '' }});
request = createPostFormRequest({ recordId: [ 'a', 'b' ]});
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(2);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, props: { errorMessage, recordId: '' }});
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, contents: { errorMessage, recordId: '' }});
});
it('renders errors for invalid passwords.', async(): Promise<void> => {
@@ -59,7 +59,7 @@ describe('A ResetPasswordHandler', (): void => {
request = createPostFormRequest({ recordId, password: 'password!', confirmPassword: 'otherPassword!' });
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, props: { errorMessage, recordId }});
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, contents: { errorMessage, recordId }});
});
it('renders errors for invalid emails.', async(): Promise<void> => {
@@ -68,7 +68,7 @@ describe('A ResetPasswordHandler', (): void => {
(accountStore.getForgotPasswordRecord as jest.Mock).mockResolvedValueOnce(undefined);
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, props: { errorMessage, recordId }});
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, contents: { errorMessage, recordId }});
});
it('renders a message on success.', async(): Promise<void> => {
@@ -82,7 +82,7 @@ describe('A ResetPasswordHandler', (): void => {
expect(accountStore.changePassword).toHaveBeenLastCalledWith(email, 'password!');
expect(messageRenderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(messageRenderHandler.handleSafe)
.toHaveBeenLastCalledWith({ response, props: { message: 'Your password was successfully reset.' }});
.toHaveBeenLastCalledWith({ response, contents: { message: 'Your password was successfully reset.' }});
});
it('has a default error for non-native errors.', async(): Promise<void> => {
@@ -91,6 +91,6 @@ describe('A ResetPasswordHandler', (): void => {
(accountStore.getForgotPasswordRecord as jest.Mock).mockRejectedValueOnce('not native');
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, props: { errorMessage, recordId }});
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ response, contents: { errorMessage, recordId }});
});
});

View File

@@ -42,7 +42,7 @@ describe('A ResetPasswordViewHandler', (): void => {
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
response,
props: { errorMessage: '', recordId: 'recordId' },
contents: { errorMessage: '', recordId: 'recordId' },
});
});
});

View File

@@ -1,19 +0,0 @@
import { renderFile } from 'ejs';
import {
EjsTemplateRenderer,
} from '../../../../../src/identity/interaction/util/EjsTemplateRenderer';
jest.mock('ejs');
describe('An EjsTemplateRenderer', (): void => {
const templatePath = '/var/templates/';
const templateFile = 'template.ejs';
const options: Record<string, string> = { email: 'alice@test.email', webId: 'http://alice.test.com/card#me' };
const renderer = new EjsTemplateRenderer<Record<string, string>>(templatePath, templateFile);
it('renders the given file with the given options.', async(): Promise<void> => {
await expect(renderer.handle(options)).resolves.toBeUndefined();
expect(renderFile).toHaveBeenCalledTimes(1);
expect(renderFile).toHaveBeenLastCalledWith('/var/templates/template.ejs', options);
});
});

View File

@@ -38,7 +38,7 @@ describe('An IdpRouteController', (): void => {
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
response,
props: { errorMessage: '', prefilled: {}},
contents: { errorMessage: '', prefilled: {}},
});
expect(postHandler.handleSafe).toHaveBeenCalledTimes(0);
});
@@ -61,7 +61,7 @@ describe('An IdpRouteController', (): void => {
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
response,
props: { errorMessage: 'bad request!', prefilled: { more: 'data!' }},
contents: { errorMessage: 'bad request!', prefilled: { more: 'data!' }},
});
});
@@ -74,7 +74,7 @@ describe('An IdpRouteController', (): void => {
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
response,
props: { errorMessage: 'Unknown error: apple!', prefilled: {}},
contents: { errorMessage: 'Unknown error: apple!', prefilled: {}},
});
});

View File

@@ -35,7 +35,7 @@ describe('An InitialInteractionHandler', (): void => {
expect(map.test.handleSafe).toHaveBeenCalledTimes(1);
expect(map.test.handleSafe).toHaveBeenLastCalledWith({
response,
props: {
contents: {
errorMessage: '',
prefilled: {},
},
@@ -51,7 +51,7 @@ describe('An InitialInteractionHandler', (): void => {
expect(map.test.handleSafe).toHaveBeenCalledTimes(0);
expect(map.default.handleSafe).toHaveBeenLastCalledWith({
response,
props: {
contents: {
errorMessage: '',
prefilled: {},
},

View File

@@ -70,7 +70,6 @@ describe('AppRunner', (): void => {
'urn:solid-server:default:variable:loggingLevel': 'info',
'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../../templates/idp'),
},
},
);
@@ -111,7 +110,6 @@ describe('AppRunner', (): void => {
'urn:solid-server:default:variable:loggingLevel': 'info',
'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json',
'urn:solid-server:default:variable:idpTemplateFolder': joinFilePath(__dirname, '../../../templates/idp'),
},
},
);
@@ -132,7 +130,6 @@ describe('AppRunner', (): void => {
'-s', 'http://localhost:5000/sparql',
'-t',
'--podConfigJson', '/different-path.json',
'--idpTemplateFolder', 'templates/idp',
],
});
@@ -160,7 +157,6 @@ describe('AppRunner', (): void => {
'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:podConfigJson': '/different-path.json',
'urn:solid-server:default:variable:idpTemplateFolder': '/var/cwd/templates/idp',
},
},
);
@@ -179,7 +175,6 @@ describe('AppRunner', (): void => {
'--sparqlEndpoint', 'http://localhost:5000/sparql',
'--showStackTrace',
'--podConfigJson', '/different-path.json',
'--idpTemplateFolder', 'templates/idp',
],
});
@@ -207,7 +202,6 @@ describe('AppRunner', (): void => {
'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:podConfigJson': '/different-path.json',
'urn:solid-server:default:variable:idpTemplateFolder': '/var/cwd/templates/idp',
},
},
);
@@ -226,7 +220,6 @@ describe('AppRunner', (): void => {
'-s', 'http://localhost:5000/sparql',
'-t',
'--podConfigJson', '/different-path.json',
'--idpTemplateFolder', 'templates/idp',
];
new AppRunner().runCli();
@@ -255,7 +248,6 @@ describe('AppRunner', (): void => {
'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql',
'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:podConfigJson': '/different-path.json',
'urn:solid-server:default:variable:idpTemplateFolder': '/var/cwd/templates/idp',
},
},
);

View File

@@ -1,77 +0,0 @@
import { createResponse } from 'node-mocks-http';
import { joinFilePath } from '../../../../src';
import type { HttpResponse } from '../../../../src';
import { RenderEjsHandler } from '../../../../src/server/util/RenderEjsHandler';
describe('RenderEjsHandler', (): void => {
let response: HttpResponse;
let templatePath: string;
let templateFile: string;
beforeEach((): void => {
response = createResponse() as HttpResponse;
templatePath = joinFilePath(__dirname, '../../../assets/idp');
templateFile = 'testHtml.ejs';
});
it('throws an error if the path is not valid.', async(): Promise<void> => {
const handler = new RenderEjsHandler<{ message: string }>('/bad/path', 'badFile.thing');
await expect(handler.handle({
response,
props: {
message: 'cool',
},
})).rejects.toThrow(`ENOENT: no such file or directory, open '/bad/path/badFile.thing'`);
});
it('throws an error if valid parameters were not provided.', async(): Promise<void> => {
const handler = new RenderEjsHandler<string>(templatePath, templateFile);
await expect(handler.handle({
response,
props: 'This is an invalid prop.',
})).rejects.toThrow();
});
it('successfully renders a page.', async(): Promise<void> => {
const handler = new RenderEjsHandler<{ message: string }>(templatePath, templateFile);
await handler.handle({
response,
props: {
message: 'cool',
},
});
// Cast to any because mock-response depends on express, which this project doesn't have
const testResponse = response as any;
expect(testResponse._isEndCalled()).toBe(true);
expect(testResponse._getData()).toBe('<html><body><p>cool</p></body></html>');
expect(testResponse._getStatusCode()).toBe(200);
});
it('successfully escapes html input.', async(): Promise<void> => {
const handler = new RenderEjsHandler<{ message: string }>(templatePath, templateFile);
await handler.handle({
response,
props: {
message: '<script>alert(1)</script>',
},
});
// Cast to any because mock-response depends on express, which this project doesn't have
const testResponse = response as any;
expect(testResponse._isEndCalled()).toBe(true);
expect(testResponse._getData()).toBe('<html><body><p>&lt;script&gt;alert(1)&lt;/script&gt;</p></body></html>');
expect(testResponse._getStatusCode()).toBe(200);
});
it('successfully renders when no props are needed.', async(): Promise<void> => {
const handler = new RenderEjsHandler<undefined>(templatePath, 'noPropsTestHtml.ejs');
await handler.handle({
response,
props: undefined,
});
// Cast to any because mock-response depends on express, which this project doesn't have
const testResponse = response as any;
expect(testResponse._isEndCalled()).toBe(true);
expect(testResponse._getData()).toBe('<html><body><p>secret message</p></body></html>');
expect(testResponse._getStatusCode()).toBe(200);
});
});

View File

@@ -0,0 +1,30 @@
import { createResponse } from 'node-mocks-http';
import type { HttpResponse } from '../../../../src';
import { TemplateHandler } from '../../../../src/server/util/TemplateHandler';
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
describe('A TemplateHandler', (): void => {
const contents = { contents: 'contents' };
let templateEngine: jest.Mocked<TemplateEngine>;
let response: HttpResponse;
beforeEach((): void => {
templateEngine = {
render: jest.fn().mockResolvedValue('rendered'),
};
response = createResponse() as HttpResponse;
});
it('renders the template in the response.', async(): Promise<void> => {
const handler = new TemplateHandler(templateEngine);
await handler.handle({ response, contents });
expect(templateEngine.render).toHaveBeenCalledTimes(1);
expect(templateEngine.render).toHaveBeenCalledWith(contents);
expect(response.getHeaders()).toHaveProperty('content-type', 'text/html');
expect((response as any)._isEndCalled()).toBe(true);
expect((response as any)._getData()).toBe('rendered');
expect((response as any)._getStatusCode()).toBe(200);
});
});

View File

@@ -0,0 +1,23 @@
import { EjsTemplateEngine } from '../../../../src/util/templates/EjsTemplateEngine';
jest.mock('../../../../src/util/templates/TemplateEngine', (): any => ({
readTemplate: jest.fn(async({ templateString }): Promise<string> => `${templateString}: <%= detail %>`),
}));
describe('A EjsTemplateEngine', (): void => {
const defaultTemplate = { templateString: 'xyz' };
const contents = { detail: 'a&b' };
let templateEngine: EjsTemplateEngine;
beforeEach((): void => {
templateEngine = new EjsTemplateEngine(defaultTemplate);
});
it('uses the default template when no template was passed.', async(): Promise<void> => {
await expect(templateEngine.render(contents)).resolves.toBe('xyz: a&amp;b');
});
it('uses the passed template.', async(): Promise<void> => {
await expect(templateEngine.render(contents, { templateString: 'my' })).resolves.toBe('my: a&amp;b');
});
});

View File

@@ -22,15 +22,19 @@ describe('readTemplate', (): void => {
await expect(readTemplate()).resolves.toBe('');
});
it('accepts string templates.', async(): Promise<void> => {
it('accepts a filename.', async(): Promise<void> => {
await expect(readTemplate(templateFile)).resolves.toBe('{{template}}');
});
it('accepts options with a string template.', async(): Promise<void> => {
await expect(readTemplate({ templateString: 'abc' })).resolves.toBe('abc');
});
it('accepts a filename.', async(): Promise<void> => {
it('accepts options with a filename.', async(): Promise<void> => {
await expect(readTemplate({ templateFile })).resolves.toBe('{{template}}');
});
it('accepts a filename and path.', async(): Promise<void> => {
it('accepts options with a filename and a path.', async(): Promise<void> => {
await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}');
});
});