From 9628fe98b86d1fb36c5daa5dadff154d41118ac0 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Tue, 20 Jul 2021 19:39:28 +0200 Subject: [PATCH] refactor: Align EJS engine with Handlebars. --- .componentsignore | 1 + README.md | 1 - .../interaction/handlers/forgot-password.json | 23 +++--- .../handler/interaction/handlers/initial.json | 8 +- .../handler/interaction/handlers/login.json | 8 +- .../interaction/handlers/registration.json | 16 ++-- .../interaction/handlers/reset-password.json | 16 ++-- .../representation-conversion/default.json | 4 +- config/util/variables/default.json | 5 -- .../handler/ForgotPasswordHandler.ts | 12 +-- .../handler/RegistrationHandler.ts | 11 ++- .../handler/ResetPasswordHandler.ts | 10 +-- .../handler/ResetPasswordRenderHandler.ts | 4 +- .../handler/ResetPasswordViewHandler.ts | 2 +- .../interaction/util/EjsTemplateRenderer.ts | 25 ------ .../interaction/util/IdpRenderHandler.ts | 4 +- .../interaction/util/IdpRouteController.ts | 2 +- .../util/InitialInteractionHandler.ts | 2 +- .../interaction/util/TemplateRenderer.ts | 6 -- src/index.ts | 6 +- src/init/AppRunner.ts | 4 - src/server/util/RenderEjsHandler.ts | 26 ------- src/server/util/RenderHandler.ts | 9 --- src/server/util/TemplateHandler.ts | 26 +++++++ src/util/templates/EjsTemplateEngine.ts | 28 +++++++ .../templates/HandlebarsTemplateEngine.ts | 2 +- src/util/templates/TemplateEngine.ts | 8 +- test/integration/Config.ts | 1 - .../handler/ForgotPasswordHandler.test.ts | 12 +-- .../handler/RegistrationHandler.test.ts | 4 +- .../handler/ResetPasswordHandler.test.ts | 16 ++-- .../handler/ResetPasswordViewHandler.test.ts | 2 +- .../util/EjsTemplateRenderer.test.ts | 19 ----- .../util/IdpRouteController.test.ts | 6 +- .../util/InitialInteractionHandler.test.ts | 4 +- test/unit/init/AppRunner.test.ts | 8 -- .../unit/server/util/RenderEjsHandler.test.ts | 77 ------------------- test/unit/server/util/TemplateHandler.test.ts | 30 ++++++++ .../util/templates/EjsTemplateEngine.test.ts | 23 ++++++ .../util/templates/TemplateEngine.test.ts | 10 ++- 40 files changed, 215 insertions(+), 266 deletions(-) delete mode 100644 src/identity/interaction/util/EjsTemplateRenderer.ts delete mode 100644 src/identity/interaction/util/TemplateRenderer.ts delete mode 100644 src/server/util/RenderEjsHandler.ts delete mode 100644 src/server/util/RenderHandler.ts create mode 100644 src/server/util/TemplateHandler.ts create mode 100644 src/util/templates/EjsTemplateEngine.ts delete mode 100644 test/unit/identity/interaction/util/EjsTemplateRenderer.test.ts delete mode 100644 test/unit/server/util/RenderEjsHandler.test.ts create mode 100644 test/unit/server/util/TemplateHandler.test.ts create mode 100644 test/unit/util/templates/EjsTemplateEngine.test.ts diff --git a/.componentsignore b/.componentsignore index 1659e95c6..f6791d40b 100644 --- a/.componentsignore +++ b/.componentsignore @@ -5,5 +5,6 @@ "EventEmitter", "HttpErrorOptions", "Template", + "TemplateEngine", "ValuePreferencesArg" ] diff --git a/README.md b/README.md index 844cfe60f..82f515d32 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ Additional recipes for configuring and deploying the server can be found at [sol | `--sparqlEndpoint, -s` | | Endpoint to call when using a SPARQL-based config. | | `--showStackTrace, -t` | false | Whether error stack traces should be shown in responses. | | `--podConfigJson` | `"./pod-config.json"` | JSON file to store pod configuration when using a dynamic config. | -| `--idpTemplateFolder` | `"templates/idp"` | Folder containing the templates used for IDP interactions. | ## Using the identity provider diff --git a/config/identity/handler/interaction/handlers/forgot-password.json b/config/identity/handler/interaction/handlers/forgot-password.json index e24306d80..0cab5dcc0 100644 --- a/config/identity/handler/interaction/handlers/forgot-password.json +++ b/config/identity/handler/interaction/handlers/forgot-password.json @@ -12,10 +12,9 @@ "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }, "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "args_idpPath": "/idp", - "args_emailTemplateRenderer": { - "@type": "EjsTemplateRenderer", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/resetPasswordEmail.ejs" + "args_templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/resetPasswordEmail.ejs" }, "args_emailSender": { "@id": "urn:solid-server:default:EmailSender" } }, @@ -25,16 +24,20 @@ { "comment": "Renders the Email Sent message page", "@id": "urn:solid-server:auth:password:EmailSentRenderHandler", - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/emailSent.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/emailSent.ejs" + } }, { "comment": "Renders the forgot password page", "@id": "urn:solid-server:auth:password:ForgotPasswordRenderHandler", - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/forgotPassword.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/forgotPassword.ejs" + } } ] } diff --git a/config/identity/handler/interaction/handlers/initial.json b/config/identity/handler/interaction/handlers/initial.json index 2b66e8e94..623f2f595 100644 --- a/config/identity/handler/interaction/handlers/initial.json +++ b/config/identity/handler/interaction/handlers/initial.json @@ -13,9 +13,11 @@ { "InitialInteractionHandler:_renderHandlerMap_key": "consent", "InitialInteractionHandler:_renderHandlerMap_value": { - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/confirm.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/confirm.ejs" + } } } ], diff --git a/config/identity/handler/interaction/handlers/login.json b/config/identity/handler/interaction/handlers/login.json index c4b73334d..3ccaa4e4a 100644 --- a/config/identity/handler/interaction/handlers/login.json +++ b/config/identity/handler/interaction/handlers/login.json @@ -17,9 +17,11 @@ { "comment": "Renders the login page", "@id": "urn:solid-server:auth:password:LoginRenderHandler", - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/login.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/login.ejs" + } } ] } diff --git a/config/identity/handler/interaction/handlers/registration.json b/config/identity/handler/interaction/handlers/registration.json index 8edf6108e..d5e068f01 100644 --- a/config/identity/handler/interaction/handlers/registration.json +++ b/config/identity/handler/interaction/handlers/registration.json @@ -22,16 +22,20 @@ { "comment": "Renders the register page", "@id": "urn:solid-server:auth:password:RegisterRenderHandler", - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/register.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/register.ejs" + } }, { "comment": "Renders the successful registration page", "@id": "urn:solid-server:auth:password:RegisterResponseRenderHandler", - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/registerResponse.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/registerResponse.ejs" + } } ] } diff --git a/config/identity/handler/interaction/handlers/reset-password.json b/config/identity/handler/interaction/handlers/reset-password.json index 4dee646cc..f6381ba2a 100644 --- a/config/identity/handler/interaction/handlers/reset-password.json +++ b/config/identity/handler/interaction/handlers/reset-password.json @@ -30,16 +30,20 @@ { "comment": "Renders the reset password page", "@id": "urn:solid-server:auth:password:ResetPasswordRenderHandler", - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/resetPassword.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/resetPassword.ejs" + } }, { "comment": "Renders a generic page that says a message", "@id": "urn:solid-server:auth:password:MessageRenderHandler", - "@type": "RenderEjsHandler", - "templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, - "templateFile": "./email-password-interaction/message.ejs" + "@type": "TemplateHandler", + "templateEngine": { + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/message.ejs" + } } ] } diff --git a/config/util/representation-conversion/default.json b/config/util/representation-conversion/default.json index 0d7401b04..1f111e45d 100644 --- a/config/util/representation-conversion/default.json +++ b/config/util/representation-conversion/default.json @@ -30,7 +30,7 @@ "@type": "ErrorToTemplateConverter", "templateEngine": { "@type": "HandlebarsTemplateEngine", - "template": { "templateFile": "$PACKAGE_ROOT/templates/error/main.md" } + "template": "$PACKAGE_ROOT/templates/error/main.md" }, "templatePath": "$PACKAGE_ROOT/templates/error/descriptions/", "extension": ".md", @@ -42,7 +42,7 @@ "templateEngine": { "@id": "urn:solid-server:default:MainTemplateEngine", "@type": "HandlebarsTemplateEngine", - "template": { "templateFile": "$PACKAGE_ROOT/templates/main.html" } + "template": "$PACKAGE_ROOT/templates/main.html" } } ] diff --git a/config/util/variables/default.json b/config/util/variables/default.json index 14eb2e45e..8a3a18b07 100644 --- a/config/util/variables/default.json +++ b/config/util/variables/default.json @@ -36,11 +36,6 @@ "comment": "Path to the JSON file used to store configuration for dynamic pods.", "@id": "urn:solid-server:default:variable:podConfigJson", "@type": "Variable" - }, - { - "comment": "Folder containing the templates used for IDP interactions.", - "@id": "urn:solid-server:default:variable:idpTemplateFolder", - "@type": "Variable" } ] } diff --git a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts b/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts index 539662f72..8c49385af 100644 --- a/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts +++ b/src/identity/interaction/email-password/handler/ForgotPasswordHandler.ts @@ -3,12 +3,12 @@ import urljoin from 'url-join'; import { getLoggerFor } from '../../../../logging/LogUtil'; import type { HttpResponse } from '../../../../server/HttpResponse'; import { ensureTrailingSlash } from '../../../../util/PathUtil'; +import type { TemplateEngine } from '../../../../util/templates/TemplateEngine'; import type { InteractionHttpHandlerInput } from '../../InteractionHttpHandler'; import { InteractionHttpHandler } from '../../InteractionHttpHandler'; import type { EmailSender } from '../../util/EmailSender'; import { getFormDataRequestBody } from '../../util/FormDataUtil'; import type { IdpRenderHandler } from '../../util/IdpRenderHandler'; -import type { TemplateRenderer } from '../../util/TemplateRenderer'; import { throwIdpInteractionError } from '../EmailPasswordUtil'; import type { AccountStore } from '../storage/AccountStore'; @@ -17,7 +17,7 @@ export interface ForgotPasswordHandlerArgs { accountStore: AccountStore; baseUrl: string; idpPath: string; - emailTemplateRenderer: TemplateRenderer<{ resetLink: string }>; + templateEngine: TemplateEngine<{ resetLink: string }>; emailSender: EmailSender; } @@ -31,7 +31,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler { private readonly accountStore: AccountStore; private readonly baseUrl: string; private readonly idpPath: string; - private readonly emailTemplateRenderer: TemplateRenderer<{ resetLink: string }>; + private readonly templateEngine: TemplateEngine<{ resetLink: string }>; private readonly emailSender: EmailSender; public constructor(args: ForgotPasswordHandlerArgs) { @@ -40,7 +40,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler { this.accountStore = args.accountStore; this.baseUrl = ensureTrailingSlash(args.baseUrl); this.idpPath = args.idpPath; - this.emailTemplateRenderer = args.emailTemplateRenderer; + this.templateEngine = args.templateEngine; this.emailSender = args.emailSender; } @@ -80,7 +80,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler { private async sendResetMail(recordId: string, email: string): Promise { this.logger.info(`Sending password reset to ${email}`); const resetLink = urljoin(this.baseUrl, this.idpPath, `resetpassword?rid=${recordId}`); - const renderedEmail = await this.emailTemplateRenderer.handleSafe({ resetLink }); + const renderedEmail = await this.templateEngine.render({ resetLink }); await this.emailSender.handleSafe({ recipient: email, subject: 'Reset your password', @@ -98,7 +98,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler { // Send response await this.messageRenderHandler.handleSafe({ response, - props: { + contents: { errorMessage: '', prefilled: { email, diff --git a/src/identity/interaction/email-password/handler/RegistrationHandler.ts b/src/identity/interaction/email-password/handler/RegistrationHandler.ts index 4f28f7329..62b548e1c 100644 --- a/src/identity/interaction/email-password/handler/RegistrationHandler.ts +++ b/src/identity/interaction/email-password/handler/RegistrationHandler.ts @@ -7,7 +7,7 @@ import type { PodManager } from '../../../../pods/PodManager'; import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import { HttpHandler } from '../../../../server/HttpHandler'; import type { HttpRequest } from '../../../../server/HttpRequest'; -import type { RenderHandler } from '../../../../server/util/RenderHandler'; +import type { TemplateHandler } from '../../../../server/util/TemplateHandler'; import type { OwnershipValidator } from '../../../ownership/OwnershipValidator'; import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil'; @@ -43,7 +43,7 @@ export interface RegistrationHandlerArgs { /** * Renders the response when registration is successful. */ - responseHandler: RenderHandler>; + responseHandler: TemplateHandler; } /** @@ -83,7 +83,7 @@ export class RegistrationHandler extends HttpHandler { private readonly ownershipValidator: OwnershipValidator; private readonly accountStore: AccountStore; private readonly podManager: PodManager; - private readonly responseHandler: RenderHandler>; + private readonly responseHandler: TemplateHandler; public constructor(args: RegistrationHandlerArgs) { super(); @@ -100,9 +100,8 @@ export class RegistrationHandler extends HttpHandler { const result = await this.parseInput(request); try { - const props = await this.register(result); - - await this.responseHandler.handleSafe({ response, props }); + const contents = await this.register(result); + await this.responseHandler.handleSafe({ response, contents }); } catch (error: unknown) { throwIdpInteractionError(error, result.data as Record); } diff --git a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts index 6135f8f3c..02af6e89c 100644 --- a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts +++ b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import { getLoggerFor } from '../../../../logging/LogUtil'; import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import { HttpHandler } from '../../../../server/HttpHandler'; -import type { RenderHandler } from '../../../../server/util/RenderHandler'; +import type { TemplateHandler } from '../../../../server/util/TemplateHandler'; import { createErrorMessage } from '../../../../util/errors/ErrorUtil'; import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { assertPassword } from '../EmailPasswordUtil'; @@ -12,7 +12,7 @@ import type { ResetPasswordRenderHandler } from './ResetPasswordRenderHandler'; export interface ResetPasswordHandlerArgs { accountStore: AccountStore; renderHandler: ResetPasswordRenderHandler; - messageRenderHandler: RenderHandler<{ message: string }>; + messageRenderHandler: TemplateHandler<{ message: string }>; } /** @@ -24,7 +24,7 @@ export class ResetPasswordHandler extends HttpHandler { private readonly accountStore: AccountStore; private readonly renderHandler: ResetPasswordRenderHandler; - private readonly messageRenderHandler: RenderHandler<{ message: string }>; + private readonly messageRenderHandler: TemplateHandler<{ message: string }>; public constructor(args: ResetPasswordHandlerArgs) { super(); @@ -48,14 +48,14 @@ export class ResetPasswordHandler extends HttpHandler { await this.resetPassword(recordId, password); await this.messageRenderHandler.handleSafe({ response: input.response, - props: { + contents: { message: 'Your password was successfully reset.', }, }); } catch (err: unknown) { await this.renderHandler.handleSafe({ response: input.response, - props: { + contents: { errorMessage: createErrorMessage(err), recordId: prefilledRecordId, }, diff --git a/src/identity/interaction/email-password/handler/ResetPasswordRenderHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordRenderHandler.ts index 6957fa2c3..0f949acdb 100644 --- a/src/identity/interaction/email-password/handler/ResetPasswordRenderHandler.ts +++ b/src/identity/interaction/email-password/handler/ResetPasswordRenderHandler.ts @@ -1,4 +1,4 @@ -import { RenderHandler } from '../../../../server/util/RenderHandler'; +import { TemplateHandler } from '../../../../server/util/TemplateHandler'; export interface ResetPasswordRenderHandlerProps { errorMessage: string; @@ -9,4 +9,4 @@ export interface ResetPasswordRenderHandlerProps { * A special {@link RenderHandler} for the Reset Password form * that includes the required props for rendering the reset password form. */ -export abstract class ResetPasswordRenderHandler extends RenderHandler {} +export abstract class ResetPasswordRenderHandler extends TemplateHandler {} diff --git a/src/identity/interaction/email-password/handler/ResetPasswordViewHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordViewHandler.ts index a2db4f473..bf9c1fb3a 100644 --- a/src/identity/interaction/email-password/handler/ResetPasswordViewHandler.ts +++ b/src/identity/interaction/email-password/handler/ResetPasswordViewHandler.ts @@ -27,7 +27,7 @@ export class ResetPasswordViewHandler extends HttpHandler { ); await this.renderHandler.handleSafe({ response, - props: { errorMessage: '', recordId }, + contents: { errorMessage: '', recordId }, }); } catch (error: unknown) { throwIdpInteractionError(error, {}); diff --git a/src/identity/interaction/util/EjsTemplateRenderer.ts b/src/identity/interaction/util/EjsTemplateRenderer.ts deleted file mode 100644 index 040c6077a..000000000 --- a/src/identity/interaction/util/EjsTemplateRenderer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { renderFile } from 'ejs'; -import { joinFilePath } from '../../../util/PathUtil'; -import { TemplateRenderer } from './TemplateRenderer'; - -/** - * Renders options using a given EJS template location and returns the result as a string. - * This is useful for rendering emails. - */ -export class EjsTemplateRenderer extends TemplateRenderer { - private readonly templatePath: string; - private readonly templateFile: string; - - public constructor(templatePath: string, templateFile: string) { - super(); - this.templatePath = templatePath; - this.templateFile = templateFile; - } - - public async handle(options: T): Promise { - return renderFile( - joinFilePath(this.templatePath, this.templateFile), - options, - ); - } -} diff --git a/src/identity/interaction/util/IdpRenderHandler.ts b/src/identity/interaction/util/IdpRenderHandler.ts index a683ba977..40a2cfe15 100644 --- a/src/identity/interaction/util/IdpRenderHandler.ts +++ b/src/identity/interaction/util/IdpRenderHandler.ts @@ -1,4 +1,4 @@ -import { RenderHandler } from '../../../server/util/RenderHandler'; +import { TemplateHandler } from '../../../server/util/TemplateHandler'; export interface IdpRenderHandlerProps { errorMessage?: string; @@ -9,4 +9,4 @@ export interface IdpRenderHandlerProps { * A special Render Handler that renders an IDP form. * Contains an error message if something was wrong and prefilled values for forms. */ -export abstract class IdpRenderHandler extends RenderHandler {} +export abstract class IdpRenderHandler extends TemplateHandler {} diff --git a/src/identity/interaction/util/IdpRouteController.ts b/src/identity/interaction/util/IdpRouteController.ts index 863bcc4f8..d6ec99705 100644 --- a/src/identity/interaction/util/IdpRouteController.ts +++ b/src/identity/interaction/util/IdpRouteController.ts @@ -24,7 +24,7 @@ export class IdpRouteController extends RouterHandler { Promise { return this.renderHandler.handleSafe({ response: input.response, - props: { errorMessage, prefilled }, + contents: { errorMessage, prefilled }, }); } diff --git a/src/identity/interaction/util/InitialInteractionHandler.ts b/src/identity/interaction/util/InitialInteractionHandler.ts index 8a8b6b43b..663a72749 100644 --- a/src/identity/interaction/util/InitialInteractionHandler.ts +++ b/src/identity/interaction/util/InitialInteractionHandler.ts @@ -38,7 +38,7 @@ export class InitialInteractionHandler extends InteractionHttpHandler { await this.renderHandlerMap[name].handleSafe({ response, - props: { + contents: { errorMessage: '', prefilled: {}, }, diff --git a/src/identity/interaction/util/TemplateRenderer.ts b/src/identity/interaction/util/TemplateRenderer.ts deleted file mode 100644 index e2c38bf6f..000000000 --- a/src/identity/interaction/util/TemplateRenderer.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AsyncHandler } from '../../../util/handlers/AsyncHandler'; - -/** - * Renders given options - */ -export abstract class TemplateRenderer extends AsyncHandler {} diff --git a/src/index.ts b/src/index.ts index f87bac283..8e8dee8fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,6 @@ export * from './identity/interaction/email-password/EmailPasswordUtil'; // Identity/Interaction/Util export * from './identity/interaction/util/BaseEmailSender'; -export * from './identity/interaction/util/EjsTemplateRenderer'; export * from './identity/interaction/util/EmailSender'; export * from './identity/interaction/util/FormDataUtil'; export * from './identity/interaction/util/IdpInteractionError'; @@ -44,7 +43,6 @@ export * from './identity/interaction/util/IdpRenderHandler'; export * from './identity/interaction/util/IdpRouteController'; export * from './identity/interaction/util/InitialInteractionHandler'; export * from './identity/interaction/util/InteractionCompleter'; -export * from './identity/interaction/util/TemplateRenderer'; // Identity/Interaction export * from './identity/interaction/InteractionHttpHandler'; @@ -206,9 +204,8 @@ export * from './server/middleware/StaticAssetHandler'; export * from './server/middleware/WebSocketAdvertiser'; // Server/Util -export * from './server/util/RenderEjsHandler'; -export * from './server/util/RenderHandler'; export * from './server/util/RouterHandler'; +export * from './server/util/TemplateHandler'; // Storage/Accessors export * from './storage/accessors/DataAccessor'; @@ -310,6 +307,7 @@ export * from './util/locking/SingleThreadedResourceLocker'; export * from './util/locking/WrappedExpiringReadWriteLocker'; // Util/Templates +export * from './util/templates/EjsTemplateEngine'; export * from './util/templates/HandlebarsTemplateEngine'; export * from './util/templates/TemplateEngine'; diff --git a/src/init/AppRunner.ts b/src/init/AppRunner.ts index c6547ad5d..46fb3b8b6 100644 --- a/src/init/AppRunner.ts +++ b/src/init/AppRunner.ts @@ -79,7 +79,6 @@ export class AppRunner { config: { type: 'string', alias: 'c', requiresArg: true }, loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true }, mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, - idpTemplateFolder: { type: 'string', requiresArg: true }, port: { type: 'number', alias: 'p', default: 3000, requiresArg: true }, rootFilePath: { type: 'string', alias: 'f', default: './', requiresArg: true }, showStackTrace: { type: 'boolean', alias: 't', default: false }, @@ -137,8 +136,6 @@ export class AppRunner { 'urn:solid-server:default:variable:showStackTrace': params.showStackTrace, 'urn:solid-server:default:variable:podConfigJson': this.resolveFilePath(params.podConfigJson), - 'urn:solid-server:default:variable:idpTemplateFolder': - this.resolveFilePath(params.idpTemplateFolder, 'templates/idp'), }; } @@ -166,5 +163,4 @@ export interface ConfigVariables { sparqlEndpoint?: string; showStackTrace?: boolean; podConfigJson?: string; - idpTemplateFolder?: string; } diff --git a/src/server/util/RenderEjsHandler.ts b/src/server/util/RenderEjsHandler.ts deleted file mode 100644 index 07c444eaf..000000000 --- a/src/server/util/RenderEjsHandler.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { renderFile } from 'ejs'; -import { joinFilePath } from '../../util/PathUtil'; -import type { HttpResponse } from '../HttpResponse'; -import { RenderHandler } from './RenderHandler'; - -/** - * A Render Handler that uses EJS templates to render a response. - */ -export class RenderEjsHandler extends RenderHandler { - private readonly templatePath: string; - private readonly templateFile: string; - - public constructor(templatePath: string, templateFile: string) { - super(); - this.templatePath = templatePath; - this.templateFile = templateFile; - } - - public async handle(input: { response: HttpResponse; props: T }): Promise { - const { props, response } = input; - const renderedHtml = await renderFile(joinFilePath(this.templatePath, this.templateFile), props || {}); - // eslint-disable-next-line @typescript-eslint/naming-convention - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end(renderedHtml); - } -} diff --git a/src/server/util/RenderHandler.ts b/src/server/util/RenderHandler.ts deleted file mode 100644 index c1ab7f39d..000000000 --- a/src/server/util/RenderHandler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AsyncHandler } from '../../util/handlers/AsyncHandler'; -import type { HttpResponse } from '../HttpResponse'; - -export interface RenderHandlerInput {} - -/** - * Renders a result with the given props and sends it to the HttpResponse. - */ -export abstract class RenderHandler extends AsyncHandler<{ response: HttpResponse; props: T }> {} diff --git a/src/server/util/TemplateHandler.ts b/src/server/util/TemplateHandler.ts new file mode 100644 index 000000000..4bfd75079 --- /dev/null +++ b/src/server/util/TemplateHandler.ts @@ -0,0 +1,26 @@ +import { AsyncHandler } from '../../util/handlers/AsyncHandler'; +import type { TemplateEngine } from '../../util/templates/TemplateEngine'; +import type { HttpResponse } from '../HttpResponse'; +import Dict = NodeJS.Dict; + +/** + * A Render Handler that uses a template engine to render a response. + */ +export class TemplateHandler = Dict> + extends AsyncHandler<{ response: HttpResponse; contents: T }> { + private readonly templateEngine: TemplateEngine; + private readonly contentType: string; + + public constructor(templateEngine: TemplateEngine, contentType = 'text/html') { + super(); + this.templateEngine = templateEngine; + this.contentType = contentType; + } + + public async handle({ response, contents }: { response: HttpResponse; contents: T }): Promise { + const rendered = await this.templateEngine.render(contents); + // eslint-disable-next-line @typescript-eslint/naming-convention + response.writeHead(200, { 'Content-Type': this.contentType }); + response.end(rendered); + } +} diff --git a/src/util/templates/EjsTemplateEngine.ts b/src/util/templates/EjsTemplateEngine.ts new file mode 100644 index 000000000..c1183dbfb --- /dev/null +++ b/src/util/templates/EjsTemplateEngine.ts @@ -0,0 +1,28 @@ +/* eslint-disable tsdoc/syntax */ +// tsdoc/syntax cannot handle `@range` +import type { TemplateFunction } from 'ejs'; +import { compile, render } from 'ejs'; +import type { TemplateEngine, Template } from './TemplateEngine'; +import { readTemplate } from './TemplateEngine'; +import Dict = NodeJS.Dict; + +/** + * Fills in EJS templates. + */ +export class EjsTemplateEngine = Dict> implements TemplateEngine { + private readonly applyTemplate: Promise; + + /** + * @param template - The default template @range {json} + */ + public constructor(template?: Template) { + this.applyTemplate = readTemplate(template) + .then((templateString: string): TemplateFunction => compile(templateString)); + } + + public async render(contents: T): Promise; + public async render(contents: TCustom, template: Template): Promise; + public async render(contents: TCustom, template?: Template): Promise { + return template ? render(await readTemplate(template), contents) : (await this.applyTemplate)(contents); + } +} diff --git a/src/util/templates/HandlebarsTemplateEngine.ts b/src/util/templates/HandlebarsTemplateEngine.ts index dc9d513fb..4567752d5 100644 --- a/src/util/templates/HandlebarsTemplateEngine.ts +++ b/src/util/templates/HandlebarsTemplateEngine.ts @@ -1,5 +1,5 @@ /* eslint-disable tsdoc/syntax */ -// tsdoc/syntax can't handle {json} parameter +// tsdoc/syntax cannot handle `@range` import type { TemplateDelegate } from 'handlebars'; import { compile } from 'handlebars'; import type { TemplateEngine, Template } from './TemplateEngine'; diff --git a/src/util/templates/TemplateEngine.ts b/src/util/templates/TemplateEngine.ts index 9de294dea..fd166a6fe 100644 --- a/src/util/templates/TemplateEngine.ts +++ b/src/util/templates/TemplateEngine.ts @@ -2,7 +2,9 @@ import { promises as fsPromises } from 'fs'; import { joinFilePath, resolveAssetPath } from '../PathUtil'; import Dict = NodeJS.Dict; -export type Template = TemplateString | TemplatePath; +export type Template = TemplateFileName | TemplateString | TemplatePath; + +export type TemplateFileName = string; export interface TemplateString { // String contents of the template @@ -38,6 +40,10 @@ export interface TemplateEngine = Dict> { * Reads the template and returns it as a string. */ export async function readTemplate(template: Template = { templateString: '' }): Promise { + // The template has been passed as a filename + if (typeof template === 'string') { + return readTemplate({ templateFile: template }); + } // The template has already been given as a string if ('templateString' in template) { return template.templateString; diff --git a/test/integration/Config.ts b/test/integration/Config.ts index 4ca9efffc..72b5ffb88 100644 --- a/test/integration/Config.ts +++ b/test/integration/Config.ts @@ -48,6 +48,5 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record { @@ -16,13 +16,13 @@ describe('A ForgotPasswordHandler', (): void => { const email = 'test@test.email'; const recordId = '123456'; const html = `Reset Password`; - 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, }); }); diff --git a/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts b/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts index 68ade6b9e..dcefd7f2b 100644 --- a/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/RegistrationHandler.test.ts @@ -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>; + let responseHandler: TemplateHandler>; let handler: RegistrationHandler; beforeEach(async(): Promise => { diff --git a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts index 7effde8a5..9e6b2cddf 100644 --- a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts @@ -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 => { @@ -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 => { @@ -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 => { @@ -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 => { @@ -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 => { @@ -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 }}); }); }); diff --git a/test/unit/identity/interaction/email-password/handler/ResetPasswordViewHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ResetPasswordViewHandler.test.ts index 590e6e642..482076df2 100644 --- a/test/unit/identity/interaction/email-password/handler/ResetPasswordViewHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/ResetPasswordViewHandler.test.ts @@ -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' }, }); }); }); diff --git a/test/unit/identity/interaction/util/EjsTemplateRenderer.test.ts b/test/unit/identity/interaction/util/EjsTemplateRenderer.test.ts deleted file mode 100644 index 47482b7ce..000000000 --- a/test/unit/identity/interaction/util/EjsTemplateRenderer.test.ts +++ /dev/null @@ -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 = { email: 'alice@test.email', webId: 'http://alice.test.com/card#me' }; - const renderer = new EjsTemplateRenderer>(templatePath, templateFile); - - it('renders the given file with the given options.', async(): Promise => { - await expect(renderer.handle(options)).resolves.toBeUndefined(); - expect(renderFile).toHaveBeenCalledTimes(1); - expect(renderFile).toHaveBeenLastCalledWith('/var/templates/template.ejs', options); - }); -}); diff --git a/test/unit/identity/interaction/util/IdpRouteController.test.ts b/test/unit/identity/interaction/util/IdpRouteController.test.ts index b72264967..31b619345 100644 --- a/test/unit/identity/interaction/util/IdpRouteController.test.ts +++ b/test/unit/identity/interaction/util/IdpRouteController.test.ts @@ -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: {}}, }); }); diff --git a/test/unit/identity/interaction/util/InitialInteractionHandler.test.ts b/test/unit/identity/interaction/util/InitialInteractionHandler.test.ts index 5a04dbd88..45889df20 100644 --- a/test/unit/identity/interaction/util/InitialInteractionHandler.test.ts +++ b/test/unit/identity/interaction/util/InitialInteractionHandler.test.ts @@ -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: {}, }, diff --git a/test/unit/init/AppRunner.test.ts b/test/unit/init/AppRunner.test.ts index 40a80c30b..9d7f4b32c 100644 --- a/test/unit/init/AppRunner.test.ts +++ b/test/unit/init/AppRunner.test.ts @@ -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', }, }, ); diff --git a/test/unit/server/util/RenderEjsHandler.test.ts b/test/unit/server/util/RenderEjsHandler.test.ts deleted file mode 100644 index bb446b5eb..000000000 --- a/test/unit/server/util/RenderEjsHandler.test.ts +++ /dev/null @@ -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 => { - 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 => { - const handler = new RenderEjsHandler(templatePath, templateFile); - await expect(handler.handle({ - response, - props: 'This is an invalid prop.', - })).rejects.toThrow(); - }); - - it('successfully renders a page.', async(): Promise => { - 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('

cool

'); - expect(testResponse._getStatusCode()).toBe(200); - }); - - it('successfully escapes html input.', async(): Promise => { - const handler = new RenderEjsHandler<{ message: string }>(templatePath, templateFile); - await handler.handle({ - response, - props: { - message: '', - }, - }); - // 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('

<script>alert(1)</script>

'); - expect(testResponse._getStatusCode()).toBe(200); - }); - - it('successfully renders when no props are needed.', async(): Promise => { - const handler = new RenderEjsHandler(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('

secret message

'); - expect(testResponse._getStatusCode()).toBe(200); - }); -}); diff --git a/test/unit/server/util/TemplateHandler.test.ts b/test/unit/server/util/TemplateHandler.test.ts new file mode 100644 index 000000000..dcc1b3f3d --- /dev/null +++ b/test/unit/server/util/TemplateHandler.test.ts @@ -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; + let response: HttpResponse; + + beforeEach((): void => { + templateEngine = { + render: jest.fn().mockResolvedValue('rendered'), + }; + response = createResponse() as HttpResponse; + }); + + it('renders the template in the response.', async(): Promise => { + 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); + }); +}); diff --git a/test/unit/util/templates/EjsTemplateEngine.test.ts b/test/unit/util/templates/EjsTemplateEngine.test.ts new file mode 100644 index 000000000..216d34f11 --- /dev/null +++ b/test/unit/util/templates/EjsTemplateEngine.test.ts @@ -0,0 +1,23 @@ +import { EjsTemplateEngine } from '../../../../src/util/templates/EjsTemplateEngine'; + +jest.mock('../../../../src/util/templates/TemplateEngine', (): any => ({ + readTemplate: jest.fn(async({ templateString }): Promise => `${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 => { + await expect(templateEngine.render(contents)).resolves.toBe('xyz: a&b'); + }); + + it('uses the passed template.', async(): Promise => { + await expect(templateEngine.render(contents, { templateString: 'my' })).resolves.toBe('my: a&b'); + }); +}); diff --git a/test/unit/util/templates/TemplateEngine.test.ts b/test/unit/util/templates/TemplateEngine.test.ts index 8a517c0ee..94eb5d881 100644 --- a/test/unit/util/templates/TemplateEngine.test.ts +++ b/test/unit/util/templates/TemplateEngine.test.ts @@ -22,15 +22,19 @@ describe('readTemplate', (): void => { await expect(readTemplate()).resolves.toBe(''); }); - it('accepts string templates.', async(): Promise => { + it('accepts a filename.', async(): Promise => { + await expect(readTemplate(templateFile)).resolves.toBe('{{template}}'); + }); + + it('accepts options with a string template.', async(): Promise => { await expect(readTemplate({ templateString: 'abc' })).resolves.toBe('abc'); }); - it('accepts a filename.', async(): Promise => { + it('accepts options with a filename.', async(): Promise => { await expect(readTemplate({ templateFile })).resolves.toBe('{{template}}'); }); - it('accepts a filename and path.', async(): Promise => { + it('accepts options with a filename and a path.', async(): Promise => { await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}'); }); });