refactor: Align EJS engine with Handlebars.

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

View File

@ -5,5 +5,6 @@
"EventEmitter", "EventEmitter",
"HttpErrorOptions", "HttpErrorOptions",
"Template", "Template",
"TemplateEngine",
"ValuePreferencesArg" "ValuePreferencesArg"
] ]

View File

@ -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. | | `--sparqlEndpoint, -s` | | Endpoint to call when using a SPARQL-based config. |
| `--showStackTrace, -t` | false | Whether error stack traces should be shown in responses. | | `--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. | | `--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 ## Using the identity provider

View File

@ -12,10 +12,9 @@
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }, "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
"args_idpPath": "/idp", "args_idpPath": "/idp",
"args_emailTemplateRenderer": { "args_templateEngine": {
"@type": "EjsTemplateRenderer", "@type": "EjsTemplateEngine",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/resetPasswordEmail.ejs"
"templateFile": "./email-password-interaction/resetPasswordEmail.ejs"
}, },
"args_emailSender": { "@id": "urn:solid-server:default:EmailSender" } "args_emailSender": { "@id": "urn:solid-server:default:EmailSender" }
}, },
@ -25,16 +24,20 @@
{ {
"comment": "Renders the Email Sent message page", "comment": "Renders the Email Sent message page",
"@id": "urn:solid-server:auth:password:EmailSentRenderHandler", "@id": "urn:solid-server:auth:password:EmailSentRenderHandler",
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/emailSent.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/emailSent.ejs"
}
}, },
{ {
"comment": "Renders the forgot password page", "comment": "Renders the forgot password page",
"@id": "urn:solid-server:auth:password:ForgotPasswordRenderHandler", "@id": "urn:solid-server:auth:password:ForgotPasswordRenderHandler",
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/forgotPassword.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/forgotPassword.ejs"
}
} }
] ]
} }

View File

@ -13,9 +13,11 @@
{ {
"InitialInteractionHandler:_renderHandlerMap_key": "consent", "InitialInteractionHandler:_renderHandlerMap_key": "consent",
"InitialInteractionHandler:_renderHandlerMap_value": { "InitialInteractionHandler:_renderHandlerMap_value": {
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/confirm.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/confirm.ejs"
}
} }
} }
], ],

View File

@ -17,9 +17,11 @@
{ {
"comment": "Renders the login page", "comment": "Renders the login page",
"@id": "urn:solid-server:auth:password:LoginRenderHandler", "@id": "urn:solid-server:auth:password:LoginRenderHandler",
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/login.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/login.ejs"
}
} }
] ]
} }

View File

@ -22,16 +22,20 @@
{ {
"comment": "Renders the register page", "comment": "Renders the register page",
"@id": "urn:solid-server:auth:password:RegisterRenderHandler", "@id": "urn:solid-server:auth:password:RegisterRenderHandler",
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/register.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/register.ejs"
}
}, },
{ {
"comment": "Renders the successful registration page", "comment": "Renders the successful registration page",
"@id": "urn:solid-server:auth:password:RegisterResponseRenderHandler", "@id": "urn:solid-server:auth:password:RegisterResponseRenderHandler",
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/registerResponse.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/registerResponse.ejs"
}
} }
] ]
} }

View File

@ -30,16 +30,20 @@
{ {
"comment": "Renders the reset password page", "comment": "Renders the reset password page",
"@id": "urn:solid-server:auth:password:ResetPasswordRenderHandler", "@id": "urn:solid-server:auth:password:ResetPasswordRenderHandler",
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/resetPassword.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/resetPassword.ejs"
}
}, },
{ {
"comment": "Renders a generic page that says a message", "comment": "Renders a generic page that says a message",
"@id": "urn:solid-server:auth:password:MessageRenderHandler", "@id": "urn:solid-server:auth:password:MessageRenderHandler",
"@type": "RenderEjsHandler", "@type": "TemplateHandler",
"templatePath": { "@id": "urn:solid-server:default:variable:idpTemplateFolder" }, "templateEngine": {
"templateFile": "./email-password-interaction/message.ejs" "@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/idp/email-password-interaction/message.ejs"
}
} }
] ]
} }

View File

@ -30,7 +30,7 @@
"@type": "ErrorToTemplateConverter", "@type": "ErrorToTemplateConverter",
"templateEngine": { "templateEngine": {
"@type": "HandlebarsTemplateEngine", "@type": "HandlebarsTemplateEngine",
"template": { "templateFile": "$PACKAGE_ROOT/templates/error/main.md" } "template": "$PACKAGE_ROOT/templates/error/main.md"
}, },
"templatePath": "$PACKAGE_ROOT/templates/error/descriptions/", "templatePath": "$PACKAGE_ROOT/templates/error/descriptions/",
"extension": ".md", "extension": ".md",
@ -42,7 +42,7 @@
"templateEngine": { "templateEngine": {
"@id": "urn:solid-server:default:MainTemplateEngine", "@id": "urn:solid-server:default:MainTemplateEngine",
"@type": "HandlebarsTemplateEngine", "@type": "HandlebarsTemplateEngine",
"template": { "templateFile": "$PACKAGE_ROOT/templates/main.html" } "template": "$PACKAGE_ROOT/templates/main.html"
} }
} }
] ]

View File

@ -36,11 +36,6 @@
"comment": "Path to the JSON file used to store configuration for dynamic pods.", "comment": "Path to the JSON file used to store configuration for dynamic pods.",
"@id": "urn:solid-server:default:variable:podConfigJson", "@id": "urn:solid-server:default:variable:podConfigJson",
"@type": "Variable" "@type": "Variable"
},
{
"comment": "Folder containing the templates used for IDP interactions.",
"@id": "urn:solid-server:default:variable:idpTemplateFolder",
"@type": "Variable"
} }
] ]
} }

View File

@ -3,12 +3,12 @@ import urljoin from 'url-join';
import { getLoggerFor } from '../../../../logging/LogUtil'; import { getLoggerFor } from '../../../../logging/LogUtil';
import type { HttpResponse } from '../../../../server/HttpResponse'; import type { HttpResponse } from '../../../../server/HttpResponse';
import { ensureTrailingSlash } from '../../../../util/PathUtil'; import { ensureTrailingSlash } from '../../../../util/PathUtil';
import type { TemplateEngine } from '../../../../util/templates/TemplateEngine';
import type { InteractionHttpHandlerInput } from '../../InteractionHttpHandler'; import type { InteractionHttpHandlerInput } from '../../InteractionHttpHandler';
import { InteractionHttpHandler } from '../../InteractionHttpHandler'; import { InteractionHttpHandler } from '../../InteractionHttpHandler';
import type { EmailSender } from '../../util/EmailSender'; import type { EmailSender } from '../../util/EmailSender';
import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { getFormDataRequestBody } from '../../util/FormDataUtil';
import type { IdpRenderHandler } from '../../util/IdpRenderHandler'; import type { IdpRenderHandler } from '../../util/IdpRenderHandler';
import type { TemplateRenderer } from '../../util/TemplateRenderer';
import { throwIdpInteractionError } from '../EmailPasswordUtil'; import { throwIdpInteractionError } from '../EmailPasswordUtil';
import type { AccountStore } from '../storage/AccountStore'; import type { AccountStore } from '../storage/AccountStore';
@ -17,7 +17,7 @@ export interface ForgotPasswordHandlerArgs {
accountStore: AccountStore; accountStore: AccountStore;
baseUrl: string; baseUrl: string;
idpPath: string; idpPath: string;
emailTemplateRenderer: TemplateRenderer<{ resetLink: string }>; templateEngine: TemplateEngine<{ resetLink: string }>;
emailSender: EmailSender; emailSender: EmailSender;
} }
@ -31,7 +31,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler {
private readonly accountStore: AccountStore; private readonly accountStore: AccountStore;
private readonly baseUrl: string; private readonly baseUrl: string;
private readonly idpPath: string; private readonly idpPath: string;
private readonly emailTemplateRenderer: TemplateRenderer<{ resetLink: string }>; private readonly templateEngine: TemplateEngine<{ resetLink: string }>;
private readonly emailSender: EmailSender; private readonly emailSender: EmailSender;
public constructor(args: ForgotPasswordHandlerArgs) { public constructor(args: ForgotPasswordHandlerArgs) {
@ -40,7 +40,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler {
this.accountStore = args.accountStore; this.accountStore = args.accountStore;
this.baseUrl = ensureTrailingSlash(args.baseUrl); this.baseUrl = ensureTrailingSlash(args.baseUrl);
this.idpPath = args.idpPath; this.idpPath = args.idpPath;
this.emailTemplateRenderer = args.emailTemplateRenderer; this.templateEngine = args.templateEngine;
this.emailSender = args.emailSender; this.emailSender = args.emailSender;
} }
@ -80,7 +80,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler {
private async sendResetMail(recordId: string, email: string): Promise<void> { private async sendResetMail(recordId: string, email: string): Promise<void> {
this.logger.info(`Sending password reset to ${email}`); this.logger.info(`Sending password reset to ${email}`);
const resetLink = urljoin(this.baseUrl, this.idpPath, `resetpassword?rid=${recordId}`); 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({ await this.emailSender.handleSafe({
recipient: email, recipient: email,
subject: 'Reset your password', subject: 'Reset your password',
@ -98,7 +98,7 @@ export class ForgotPasswordHandler extends InteractionHttpHandler {
// Send response // Send response
await this.messageRenderHandler.handleSafe({ await this.messageRenderHandler.handleSafe({
response, response,
props: { contents: {
errorMessage: '', errorMessage: '',
prefilled: { prefilled: {
email, email,

View File

@ -7,7 +7,7 @@ import type { PodManager } from '../../../../pods/PodManager';
import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import type { HttpHandlerInput } from '../../../../server/HttpHandler';
import { HttpHandler } from '../../../../server/HttpHandler'; import { HttpHandler } from '../../../../server/HttpHandler';
import type { HttpRequest } from '../../../../server/HttpRequest'; 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 type { OwnershipValidator } from '../../../ownership/OwnershipValidator';
import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { getFormDataRequestBody } from '../../util/FormDataUtil';
import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil'; import { assertPassword, throwIdpInteractionError } from '../EmailPasswordUtil';
@ -43,7 +43,7 @@ export interface RegistrationHandlerArgs {
/** /**
* Renders the response when registration is successful. * 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 ownershipValidator: OwnershipValidator;
private readonly accountStore: AccountStore; private readonly accountStore: AccountStore;
private readonly podManager: PodManager; private readonly podManager: PodManager;
private readonly responseHandler: RenderHandler<NodeJS.Dict<any>>; private readonly responseHandler: TemplateHandler;
public constructor(args: RegistrationHandlerArgs) { public constructor(args: RegistrationHandlerArgs) {
super(); super();
@ -100,9 +100,8 @@ export class RegistrationHandler extends HttpHandler {
const result = await this.parseInput(request); const result = await this.parseInput(request);
try { try {
const props = await this.register(result); const contents = await this.register(result);
await this.responseHandler.handleSafe({ response, contents });
await this.responseHandler.handleSafe({ response, props });
} catch (error: unknown) { } catch (error: unknown) {
throwIdpInteractionError(error, result.data as Record<string, string>); throwIdpInteractionError(error, result.data as Record<string, string>);
} }

View File

@ -2,7 +2,7 @@ import assert from 'assert';
import { getLoggerFor } from '../../../../logging/LogUtil'; import { getLoggerFor } from '../../../../logging/LogUtil';
import type { HttpHandlerInput } from '../../../../server/HttpHandler'; import type { HttpHandlerInput } from '../../../../server/HttpHandler';
import { HttpHandler } 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 { createErrorMessage } from '../../../../util/errors/ErrorUtil';
import { getFormDataRequestBody } from '../../util/FormDataUtil'; import { getFormDataRequestBody } from '../../util/FormDataUtil';
import { assertPassword } from '../EmailPasswordUtil'; import { assertPassword } from '../EmailPasswordUtil';
@ -12,7 +12,7 @@ import type { ResetPasswordRenderHandler } from './ResetPasswordRenderHandler';
export interface ResetPasswordHandlerArgs { export interface ResetPasswordHandlerArgs {
accountStore: AccountStore; accountStore: AccountStore;
renderHandler: ResetPasswordRenderHandler; 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 accountStore: AccountStore;
private readonly renderHandler: ResetPasswordRenderHandler; private readonly renderHandler: ResetPasswordRenderHandler;
private readonly messageRenderHandler: RenderHandler<{ message: string }>; private readonly messageRenderHandler: TemplateHandler<{ message: string }>;
public constructor(args: ResetPasswordHandlerArgs) { public constructor(args: ResetPasswordHandlerArgs) {
super(); super();
@ -48,14 +48,14 @@ export class ResetPasswordHandler extends HttpHandler {
await this.resetPassword(recordId, password); await this.resetPassword(recordId, password);
await this.messageRenderHandler.handleSafe({ await this.messageRenderHandler.handleSafe({
response: input.response, response: input.response,
props: { contents: {
message: 'Your password was successfully reset.', message: 'Your password was successfully reset.',
}, },
}); });
} catch (err: unknown) { } catch (err: unknown) {
await this.renderHandler.handleSafe({ await this.renderHandler.handleSafe({
response: input.response, response: input.response,
props: { contents: {
errorMessage: createErrorMessage(err), errorMessage: createErrorMessage(err),
recordId: prefilledRecordId, recordId: prefilledRecordId,
}, },

View File

@ -1,4 +1,4 @@
import { RenderHandler } from '../../../../server/util/RenderHandler'; import { TemplateHandler } from '../../../../server/util/TemplateHandler';
export interface ResetPasswordRenderHandlerProps { export interface ResetPasswordRenderHandlerProps {
errorMessage: string; errorMessage: string;
@ -9,4 +9,4 @@ export interface ResetPasswordRenderHandlerProps {
* A special {@link RenderHandler} for the Reset Password form * A special {@link RenderHandler} for the Reset Password form
* that includes the required props for rendering 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> {}

View File

@ -27,7 +27,7 @@ export class ResetPasswordViewHandler extends HttpHandler {
); );
await this.renderHandler.handleSafe({ await this.renderHandler.handleSafe({
response, response,
props: { errorMessage: '', recordId }, contents: { errorMessage: '', recordId },
}); });
} catch (error: unknown) { } catch (error: unknown) {
throwIdpInteractionError(error, {}); throwIdpInteractionError(error, {});

View File

@ -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,
);
}
}

View File

@ -1,4 +1,4 @@
import { RenderHandler } from '../../../server/util/RenderHandler'; import { TemplateHandler } from '../../../server/util/TemplateHandler';
export interface IdpRenderHandlerProps { export interface IdpRenderHandlerProps {
errorMessage?: string; errorMessage?: string;
@ -9,4 +9,4 @@ export interface IdpRenderHandlerProps {
* A special Render Handler that renders an IDP form. * A special Render Handler that renders an IDP form.
* Contains an error message if something was wrong and prefilled values for forms. * 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> {}

View File

@ -24,7 +24,7 @@ export class IdpRouteController extends RouterHandler {
Promise<void> { Promise<void> {
return this.renderHandler.handleSafe({ return this.renderHandler.handleSafe({
response: input.response, response: input.response,
props: { errorMessage, prefilled }, contents: { errorMessage, prefilled },
}); });
} }

View File

@ -38,7 +38,7 @@ export class InitialInteractionHandler extends InteractionHttpHandler {
await this.renderHandlerMap[name].handleSafe({ await this.renderHandlerMap[name].handleSafe({
response, response,
props: { contents: {
errorMessage: '', errorMessage: '',
prefilled: {}, prefilled: {},
}, },

View File

@ -1,6 +0,0 @@
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
/**
* Renders given options
*/
export abstract class TemplateRenderer<T> extends AsyncHandler<T, string> {}

View File

@ -36,7 +36,6 @@ export * from './identity/interaction/email-password/EmailPasswordUtil';
// Identity/Interaction/Util // Identity/Interaction/Util
export * from './identity/interaction/util/BaseEmailSender'; export * from './identity/interaction/util/BaseEmailSender';
export * from './identity/interaction/util/EjsTemplateRenderer';
export * from './identity/interaction/util/EmailSender'; export * from './identity/interaction/util/EmailSender';
export * from './identity/interaction/util/FormDataUtil'; export * from './identity/interaction/util/FormDataUtil';
export * from './identity/interaction/util/IdpInteractionError'; 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/IdpRouteController';
export * from './identity/interaction/util/InitialInteractionHandler'; export * from './identity/interaction/util/InitialInteractionHandler';
export * from './identity/interaction/util/InteractionCompleter'; export * from './identity/interaction/util/InteractionCompleter';
export * from './identity/interaction/util/TemplateRenderer';
// Identity/Interaction // Identity/Interaction
export * from './identity/interaction/InteractionHttpHandler'; export * from './identity/interaction/InteractionHttpHandler';
@ -206,9 +204,8 @@ export * from './server/middleware/StaticAssetHandler';
export * from './server/middleware/WebSocketAdvertiser'; export * from './server/middleware/WebSocketAdvertiser';
// Server/Util // Server/Util
export * from './server/util/RenderEjsHandler';
export * from './server/util/RenderHandler';
export * from './server/util/RouterHandler'; export * from './server/util/RouterHandler';
export * from './server/util/TemplateHandler';
// Storage/Accessors // Storage/Accessors
export * from './storage/accessors/DataAccessor'; export * from './storage/accessors/DataAccessor';
@ -310,6 +307,7 @@ export * from './util/locking/SingleThreadedResourceLocker';
export * from './util/locking/WrappedExpiringReadWriteLocker'; export * from './util/locking/WrappedExpiringReadWriteLocker';
// Util/Templates // Util/Templates
export * from './util/templates/EjsTemplateEngine';
export * from './util/templates/HandlebarsTemplateEngine'; export * from './util/templates/HandlebarsTemplateEngine';
export * from './util/templates/TemplateEngine'; export * from './util/templates/TemplateEngine';

View File

@ -79,7 +79,6 @@ export class AppRunner {
config: { type: 'string', alias: 'c', requiresArg: true }, config: { type: 'string', alias: 'c', requiresArg: true },
loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true }, loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true },
mainModulePath: { type: 'string', alias: 'm', requiresArg: true }, mainModulePath: { type: 'string', alias: 'm', requiresArg: true },
idpTemplateFolder: { type: 'string', requiresArg: true },
port: { type: 'number', alias: 'p', default: 3000, requiresArg: true }, port: { type: 'number', alias: 'p', default: 3000, requiresArg: true },
rootFilePath: { type: 'string', alias: 'f', default: './', requiresArg: true }, rootFilePath: { type: 'string', alias: 'f', default: './', requiresArg: true },
showStackTrace: { type: 'boolean', alias: 't', default: false }, 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:showStackTrace': params.showStackTrace,
'urn:solid-server:default:variable:podConfigJson': 'urn:solid-server:default:variable:podConfigJson':
this.resolveFilePath(params.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; sparqlEndpoint?: string;
showStackTrace?: boolean; showStackTrace?: boolean;
podConfigJson?: string; podConfigJson?: string;
idpTemplateFolder?: string;
} }

View File

@ -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);
}
}

View File

@ -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 }> {}

View 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);
}
}

View 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);
}
}

View File

@ -1,5 +1,5 @@
/* eslint-disable tsdoc/syntax */ /* eslint-disable tsdoc/syntax */
// tsdoc/syntax can't handle {json} parameter // tsdoc/syntax cannot handle `@range`
import type { TemplateDelegate } from 'handlebars'; import type { TemplateDelegate } from 'handlebars';
import { compile } from 'handlebars'; import { compile } from 'handlebars';
import type { TemplateEngine, Template } from './TemplateEngine'; import type { TemplateEngine, Template } from './TemplateEngine';

View File

@ -2,7 +2,9 @@ import { promises as fsPromises } from 'fs';
import { joinFilePath, resolveAssetPath } from '../PathUtil'; import { joinFilePath, resolveAssetPath } from '../PathUtil';
import Dict = NodeJS.Dict; import Dict = NodeJS.Dict;
export type Template = TemplateString | TemplatePath; export type Template = TemplateFileName | TemplateString | TemplatePath;
export type TemplateFileName = string;
export interface TemplateString { export interface TemplateString {
// String contents of the template // 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. * Reads the template and returns it as a string.
*/ */
export async function readTemplate(template: Template = { templateString: '' }): Promise<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 // The template has already been given as a string
if ('templateString' in template) { if ('templateString' in template) {
return template.templateString; return template.templateString;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ describe('An IdpRouteController', (): void => {
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1); expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
response, response,
props: { errorMessage: '', prefilled: {}}, contents: { errorMessage: '', prefilled: {}},
}); });
expect(postHandler.handleSafe).toHaveBeenCalledTimes(0); expect(postHandler.handleSafe).toHaveBeenCalledTimes(0);
}); });
@ -61,7 +61,7 @@ describe('An IdpRouteController', (): void => {
expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1); expect(renderHandler.handleSafe).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
response, 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).toHaveBeenCalledTimes(1);
expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({ expect(renderHandler.handleSafe).toHaveBeenLastCalledWith({
response, response,
props: { errorMessage: 'Unknown error: apple!', prefilled: {}}, contents: { errorMessage: 'Unknown error: apple!', prefilled: {}},
}); });
}); });

View File

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

View File

@ -70,7 +70,6 @@ describe('AppRunner', (): void => {
'urn:solid-server:default:variable:loggingLevel': 'info', 'urn:solid-server:default:variable:loggingLevel': 'info',
'urn:solid-server:default:variable:showStackTrace': false, 'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json', '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:loggingLevel': 'info',
'urn:solid-server:default:variable:showStackTrace': false, 'urn:solid-server:default:variable:showStackTrace': false,
'urn:solid-server:default:variable:podConfigJson': '/var/cwd/pod-config.json', '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', '-s', 'http://localhost:5000/sparql',
'-t', '-t',
'--podConfigJson', '/different-path.json', '--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:sparqlEndpoint': 'http://localhost:5000/sparql',
'urn:solid-server:default:variable:showStackTrace': true, 'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:podConfigJson': '/different-path.json', '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', '--sparqlEndpoint', 'http://localhost:5000/sparql',
'--showStackTrace', '--showStackTrace',
'--podConfigJson', '/different-path.json', '--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:sparqlEndpoint': 'http://localhost:5000/sparql',
'urn:solid-server:default:variable:showStackTrace': true, 'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:podConfigJson': '/different-path.json', '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', '-s', 'http://localhost:5000/sparql',
'-t', '-t',
'--podConfigJson', '/different-path.json', '--podConfigJson', '/different-path.json',
'--idpTemplateFolder', 'templates/idp',
]; ];
new AppRunner().runCli(); 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:sparqlEndpoint': 'http://localhost:5000/sparql',
'urn:solid-server:default:variable:showStackTrace': true, 'urn:solid-server:default:variable:showStackTrace': true,
'urn:solid-server:default:variable:podConfigJson': '/different-path.json', 'urn:solid-server:default:variable:podConfigJson': '/different-path.json',
'urn:solid-server:default:variable:idpTemplateFolder': '/var/cwd/templates/idp',
}, },
}, },
); );

View File

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

View File

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

View File

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

View File

@ -22,15 +22,19 @@ describe('readTemplate', (): void => {
await expect(readTemplate()).resolves.toBe(''); 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'); 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}}'); 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}}'); await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}');
}); });
}); });