mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Align EJS engine with Handlebars.
This commit is contained in:
parent
19624dc729
commit
9628fe98b8
@ -5,5 +5,6 @@
|
||||
"EventEmitter",
|
||||
"HttpErrorOptions",
|
||||
"Template",
|
||||
"TemplateEngine",
|
||||
"ValuePreferencesArg"
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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<void> {
|
||||
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,
|
||||
|
@ -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<NodeJS.Dict<any>>;
|
||||
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<NodeJS.Dict<any>>;
|
||||
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<string, string>);
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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<ResetPasswordRenderHandlerProps> {}
|
||||
export abstract class ResetPasswordRenderHandler extends TemplateHandler<ResetPasswordRenderHandlerProps> {}
|
||||
|
@ -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, {});
|
||||
|
@ -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<T> extends TemplateRenderer<T> {
|
||||
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<string> {
|
||||
return renderFile(
|
||||
joinFilePath(this.templatePath, this.templateFile),
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
@ -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<IdpRenderHandlerProps> {}
|
||||
export abstract class IdpRenderHandler extends TemplateHandler<IdpRenderHandlerProps> {}
|
||||
|
@ -24,7 +24,7 @@ export class IdpRouteController extends RouterHandler {
|
||||
Promise<void> {
|
||||
return this.renderHandler.handleSafe({
|
||||
response: input.response,
|
||||
props: { errorMessage, prefilled },
|
||||
contents: { errorMessage, prefilled },
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ export class InitialInteractionHandler extends InteractionHttpHandler {
|
||||
|
||||
await this.renderHandlerMap[name].handleSafe({
|
||||
response,
|
||||
props: {
|
||||
contents: {
|
||||
errorMessage: '',
|
||||
prefilled: {},
|
||||
},
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
|
||||
|
||||
/**
|
||||
* Renders given options
|
||||
*/
|
||||
export abstract class TemplateRenderer<T> extends AsyncHandler<T, string> {}
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<T> extends RenderHandler<T> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
@ -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<T> extends AsyncHandler<{ response: HttpResponse; props: T }> {}
|
26
src/server/util/TemplateHandler.ts
Normal file
26
src/server/util/TemplateHandler.ts
Normal file
@ -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<T extends Dict<any> = Dict<any>>
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
28
src/util/templates/EjsTemplateEngine.ts
Normal file
28
src/util/templates/EjsTemplateEngine.ts
Normal file
@ -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<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
||||
private readonly applyTemplate: Promise<TemplateFunction>;
|
||||
|
||||
/**
|
||||
* @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<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
||||
return template ? render(await readTemplate(template), contents) : (await this.applyTemplate)(contents);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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<T extends Dict<any> = Dict<any>> {
|
||||
* Reads the template and returns it as a string.
|
||||
*/
|
||||
export async function readTemplate(template: Template = { templateString: '' }): Promise<string> {
|
||||
// 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;
|
||||
|
@ -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'),
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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> => {
|
||||
|
@ -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 }});
|
||||
});
|
||||
});
|
||||
|
@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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: {}},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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: {},
|
||||
},
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -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><script>alert(1)</script></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);
|
||||
});
|
||||
});
|
30
test/unit/server/util/TemplateHandler.test.ts
Normal file
30
test/unit/server/util/TemplateHandler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
23
test/unit/util/templates/EjsTemplateEngine.test.ts
Normal file
23
test/unit/util/templates/EjsTemplateEngine.test.ts
Normal 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&b');
|
||||
});
|
||||
|
||||
it('uses the passed template.', async(): Promise<void> => {
|
||||
await expect(templateEngine.render(contents, { templateString: 'my' })).resolves.toBe('my: a&b');
|
||||
});
|
||||
});
|
@ -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}}');
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user