From 5d86c874841774cc9ab2e44de22443262d741b05 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 2 Aug 2021 15:36:51 +0200 Subject: [PATCH] feat: Create ChainedTemplateEngine for combining engines --- config/identity/handler/default.json | 17 +- .../interaction/routes/forgot-password.json | 2 +- .../interaction/routes/reset-password.json | 2 +- .../converters/markdown.json | 4 +- src/identity/IdentityProviderHttpHandler.ts | 4 +- .../handler/InteractionHandler.ts | 2 +- .../handler/ResetPasswordHandler.ts | 2 +- src/index.ts | 1 + .../conversion/MarkdownToHtmlConverter.ts | 6 +- src/util/templates/ChainedTemplateEngine.ts | 39 ++ .../identity/email-password/confirm.html.ejs | 31 +- .../email-password/email-sent.html.ejs | 37 -- .../forgot-password-response.html.ejs | 13 + .../email-password/forgot-password.html.ejs | 55 +-- .../identity/email-password/login.html.ejs | 77 ++-- .../email-password/register-response.html.ejs | 93 ++--- .../identity/email-password/register.html.ejs | 355 ++++++++---------- .../reset-password-response.html.ejs | 2 + .../email-password/reset-password.html.ejs | 61 +-- .../message.html.ejs => main.html.ejs} | 11 +- templates/main.html.hbs | 26 -- .../IdentityProviderHttpHandler.test.ts | 13 + .../handler/ResetPasswordHandler.test.ts | 5 +- .../MarkdownToHtmlConverter.test.ts | 15 - .../templates/ChainedTemplateEngine.test.ts | 39 ++ 25 files changed, 409 insertions(+), 503 deletions(-) create mode 100644 src/util/templates/ChainedTemplateEngine.ts delete mode 100644 templates/identity/email-password/email-sent.html.ejs create mode 100644 templates/identity/email-password/forgot-password-response.html.ejs create mode 100644 templates/identity/email-password/reset-password-response.html.ejs rename templates/{identity/email-password/message.html.ejs => main.html.ejs} (74%) delete mode 100644 templates/main.html.hbs create mode 100644 test/unit/util/templates/ChainedTemplateEngine.test.ts diff --git a/config/identity/handler/default.json b/config/identity/handler/default.json index aa44e2bfaf..4fc4fbedab 100644 --- a/config/identity/handler/default.json +++ b/config/identity/handler/default.json @@ -23,7 +23,22 @@ "providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" }, "templateHandler": { "@type": "TemplateHandler", - "templateEngine": { "@type": "EjsTemplateEngine" } + "templateEngine": { + "comment": "Renders the specific page and embeds it into the main HTML body.", + "@type": "ChainedTemplateEngine", + "renderedName": "htmlBody", + "engines": [ + { + "comment": "Will be called with specific interaction templates to generate HTML snippets.", + "@type": "EjsTemplateEngine" + }, + { + "comment": "Will embed the result of the first engine into the main HTML template.", + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/main.html.ejs", + } + ] + } }, "interactionCompleter": { "comment": "Responsible for finishing OIDC interactions.", diff --git a/config/identity/handler/interaction/routes/forgot-password.json b/config/identity/handler/interaction/routes/forgot-password.json index 432f7e96de..8720429384 100644 --- a/config/identity/handler/interaction/routes/forgot-password.json +++ b/config/identity/handler/interaction/routes/forgot-password.json @@ -7,7 +7,7 @@ "@type": "InteractionRoute", "route": "^/forgotpassword/?$", "viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/forgot-password.html.ejs", - "responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/email-sent.html.ejs", + "responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/forgot-password-response.html.ejs", "handler": { "@type": "ForgotPasswordHandler", "args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }, diff --git a/config/identity/handler/interaction/routes/reset-password.json b/config/identity/handler/interaction/routes/reset-password.json index 29b4634ffc..3dac8c2821 100644 --- a/config/identity/handler/interaction/routes/reset-password.json +++ b/config/identity/handler/interaction/routes/reset-password.json @@ -8,7 +8,7 @@ "@type": "InteractionRoute", "route": "^/resetpassword(/[^/]*)?$", "viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/reset-password.html.ejs", - "responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/message.html.ejs", + "responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/reset-password-response.html.ejs", "handler": { "@type": "ResetPasswordHandler", "accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" } diff --git a/config/util/representation-conversion/converters/markdown.json b/config/util/representation-conversion/converters/markdown.json index 0ed217aba2..3ad49ca262 100644 --- a/config/util/representation-conversion/converters/markdown.json +++ b/config/util/representation-conversion/converters/markdown.json @@ -11,8 +11,8 @@ "@id": "urn:solid-server:default:MarkdownToHtmlConverter", "@type": "MarkdownToHtmlConverter", "templateEngine": { - "@type": "HandlebarsTemplateEngine", - "template": "$PACKAGE_ROOT/templates/main.html.hbs" + "@type": "EjsTemplateEngine", + "template": "$PACKAGE_ROOT/templates/main.html.ejs" } }, { diff --git a/src/identity/IdentityProviderHttpHandler.ts b/src/identity/IdentityProviderHttpHandler.ts index a12539be3a..afe34774c0 100644 --- a/src/identity/IdentityProviderHttpHandler.ts +++ b/src/identity/IdentityProviderHttpHandler.ts @@ -182,9 +182,9 @@ export class IdentityProviderHttpHandler extends HttpHandler { throw new BadRequestHttpError(`Unsupported request: ${request.method} ${request.url}`); } - private async handleTemplateResponse(response: HttpResponse, templateFile: string, contents: NodeJS.Dict): + private async handleTemplateResponse(response: HttpResponse, templateFile: string, contents?: NodeJS.Dict): Promise { - await this.templateHandler.handleSafe({ response, templateFile, contents }); + await this.templateHandler.handleSafe({ response, templateFile, contents: contents ?? {}}); } /** diff --git a/src/identity/interaction/email-password/handler/InteractionHandler.ts b/src/identity/interaction/email-password/handler/InteractionHandler.ts index 4901ab9e15..bc747b0cf8 100644 --- a/src/identity/interaction/email-password/handler/InteractionHandler.ts +++ b/src/identity/interaction/email-password/handler/InteractionHandler.ts @@ -6,7 +6,7 @@ export type InteractionHandlerResult = InteractionResponseResult | InteractionCo export interface InteractionResponseResult> { type: 'response'; - details: T; + details?: T; } export interface InteractionCompleteResult { diff --git a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts index 740e709327..e47a88c40f 100644 --- a/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts +++ b/src/identity/interaction/email-password/handler/ResetPasswordHandler.ts @@ -34,7 +34,7 @@ export class ResetPasswordHandler extends InteractionHandler { assertPassword(password, confirmPassword); await this.resetPassword(recordId, password); - return { type: 'response', details: { message: 'Your password was successfully reset.' }}; + return { type: 'response' }; } catch (error: unknown) { throwIdpInteractionError(error); } diff --git a/src/index.ts b/src/index.ts index 4e6992942d..49405f5d90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -304,6 +304,7 @@ export * from './util/locking/SingleThreadedResourceLocker'; export * from './util/locking/WrappedExpiringReadWriteLocker'; // Util/Templates +export * from './util/templates/ChainedTemplateEngine'; export * from './util/templates/EjsTemplateEngine'; export * from './util/templates/HandlebarsTemplateEngine'; export * from './util/templates/TemplateEngine'; diff --git a/src/storage/conversion/MarkdownToHtmlConverter.ts b/src/storage/conversion/MarkdownToHtmlConverter.ts index 1bf8c5ac96..a690f81bcd 100644 --- a/src/storage/conversion/MarkdownToHtmlConverter.ts +++ b/src/storage/conversion/MarkdownToHtmlConverter.ts @@ -23,12 +23,8 @@ export class MarkdownToHtmlConverter extends TypedRepresentationConverter { public async handle({ representation }: RepresentationConverterArgs): Promise { const markdown = await readableToString(representation.data); - // Try to extract the main title for use in the tag - const title = /^#+\s*([^\n]+)\n/u.exec(markdown)?.[1]; - - // Place the rendered Markdown into the HTML template const htmlBody = marked(markdown); - const html = await this.templateEngine.render({ htmlBody, title }); + const html = await this.templateEngine.render({ htmlBody }); return new BasicRepresentation(html, representation.metadata, TEXT_HTML); } diff --git a/src/util/templates/ChainedTemplateEngine.ts b/src/util/templates/ChainedTemplateEngine.ts new file mode 100644 index 0000000000..b453897acd --- /dev/null +++ b/src/util/templates/ChainedTemplateEngine.ts @@ -0,0 +1,39 @@ +import type { Template, TemplateEngine } from './TemplateEngine'; +import Dict = NodeJS.Dict; + +/** + * Calls the given array of {@link TemplateEngine}s in the order they appear, + * feeding the output of one into the input of the next. + * + * The first engine will be called with the provided contents and template parameters. + * All subsequent engines will be called with no template parameter. + * Contents will still be passed along and another entry will be added for the body of the previous output. + */ +export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> { + private readonly firstEngine: TemplateEngine<T>; + private readonly chainedEngines: TemplateEngine[]; + private readonly renderedName: string; + + /** + * @param engines - Engines will be executed in the same order as the array. + * @param renderedName - The name of the key used to pass the body of one engine to the next. + */ + public constructor(engines: TemplateEngine[], renderedName = 'body') { + if (engines.length === 0) { + throw new Error('At least 1 engine needs to be provided.'); + } + this.firstEngine = engines[0]; + this.chainedEngines = engines.slice(1); + this.renderedName = renderedName; + } + + 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> { + let body = await this.firstEngine.render(contents, template!); + for (const engine of this.chainedEngines) { + body = await engine.render({ ...contents, [this.renderedName]: body }); + } + return body; + } +} diff --git a/templates/identity/email-password/confirm.html.ejs b/templates/identity/email-password/confirm.html.ejs index e6862f55ef..8d8617afe9 100644 --- a/templates/identity/email-password/confirm.html.ejs +++ b/templates/identity/email-password/confirm.html.ejs @@ -1,27 +1,4 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="utf-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1"/> - <title>Authorize - - - -
- [Solid logo] -

Community Solid Server

-
-
-

Authorize

-
-

-
-
- - - +

Authorize

+
+

+
diff --git a/templates/identity/email-password/email-sent.html.ejs b/templates/identity/email-password/email-sent.html.ejs deleted file mode 100644 index e04f86b502..0000000000 --- a/templates/identity/email-password/email-sent.html.ejs +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Email sent - - - -
- [Solid logo] -

Community Solid Server

-
-
-

Email sent

-
-

If your account exists, an email has been sent with a link to reset your password.

-

If you do not receive your email in a couple of minutes, check your spam folder or click the link below to send another email.

- - - -

Back to Log In

- -
-

- -

-
-
- - - diff --git a/templates/identity/email-password/forgot-password-response.html.ejs b/templates/identity/email-password/forgot-password-response.html.ejs new file mode 100644 index 0000000000..73d99f644d --- /dev/null +++ b/templates/identity/email-password/forgot-password-response.html.ejs @@ -0,0 +1,13 @@ +

Email sent

+
+

If your account exists, an email has been sent with a link to reset your password.

+

If you do not receive your email in a couple of minutes, check your spam folder or click the link below to send another email.

+ + + +

Back to Log In

+ +

+ +

+
diff --git a/templates/identity/email-password/forgot-password.html.ejs b/templates/identity/email-password/forgot-password.html.ejs index 08288989d5..c21282de33 100644 --- a/templates/identity/email-password/forgot-password.html.ejs +++ b/templates/identity/email-password/forgot-password.html.ejs @@ -1,42 +1,19 @@ - - - - - - Forgot password - - - -
- [Solid logo] -

Community Solid Server

-
-
-

Forgot password

-
- <%if (errorMessage) { %> -

<%= errorMessage %>

- <% } %> +

Forgot password

+ + <%if (errorMessage) { %> +

<%= errorMessage %>

+ <% } %> -
-
    -
  1. - - -
  2. -
-
+
+
    +
  1. + + +
  2. +
+
-

+

-

Log in

-
-
- - - +

Log in

+ diff --git a/templates/identity/email-password/login.html.ejs b/templates/identity/email-password/login.html.ejs index 75b9b4a7b3..16752f15da 100644 --- a/templates/identity/email-password/login.html.ejs +++ b/templates/identity/email-password/login.html.ejs @@ -1,53 +1,30 @@ - - - - - - Log in - - - -
- [Solid logo] -

Community Solid Server

-
-
-

Log in

-
- <%if (errorMessage) { %> -

<%= errorMessage %>

- <% } %> +

Log in

+ + <%if (errorMessage) { %> +

<%= errorMessage %>

+ <% } %> -
- Your account -
    -
  1. - - value="<%= prefilled.email %>" <% } %>> -
  2. -
  3. - - -
  4. -
  5. - -
  6. -
-
+
+ Your account +
    +
  1. + + value="<%= prefilled.email %>" <% } %>> +
  2. +
  3. + + +
  4. +
  5. + +
  6. +
+
-

+

- -
-
- - - + + diff --git a/templates/identity/email-password/register-response.html.ejs b/templates/identity/email-password/register-response.html.ejs index 46dee30c79..bf5246af44 100644 --- a/templates/identity/email-password/register-response.html.ejs +++ b/templates/identity/email-password/register-response.html.ejs @@ -1,63 +1,40 @@ - - - - - - You are signed up - - - -
- [Solid logo] -

Community Solid Server

-
-
-

You are signed up

-

- Welcome to Solid. - We wish you an exciting experience! -

+

You've been signed up

+

+ Welcome to Solid. + We wish you an exciting experience! +

- <% if (createPod) { %> -

Your new Pod

-

- Your new Pod is located at <%= podBaseUrl %>. -
- You can store your documents and data there. -

- <% } %> +<% if (createPod) { %> +

Your new Pod

+

+ Your new Pod is located at <%= podBaseUrl %>. +
+ You can store your documents and data there. +

+<% } %> - <% if (createWebId) { %> -

Your new WebID

-

- Your new WebID is <%= webId %>. -
- You can use this identifier to interact with Solid pods and apps. -

- <% } %> +<% if (createWebId) { %> +

Your new WebID

+

+ Your new WebID is <%= webId %>. +
+ You can use this identifier to interact with Solid pods and apps. +

+<% } %> - <% if (register) { %> -

Your new account

-

- Via your email address <%= email %>, - this server lets you log in to Solid apps - with your WebID <%= webId %> -

- <% if (!createWebId) { %> -

- You will need to add the triple - <%= `<${webId}> <${oidcIssuer}>.`%> - to your existing WebID document <%= webId %> - to indicate that you trust this server as a login provider. -

- <% } %> - <% } %> -
-
+<% if (register) { %> +

Your new account

+

+ Via your email address <%= email %>, + this server lets you log in to Solid apps + with your WebID <%= webId %> +

+ <% if (!createWebId) { %>

- ©2019–2021 Inrupt Inc. - and imec + You will need to add the triple + <%= `<${webId}> <${oidcIssuer}>.`%> + to your existing WebID document <%= webId %> + to indicate that you trust this server as a login provider.

-
- - + <% } %> +<% } %> diff --git a/templates/identity/email-password/register.html.ejs b/templates/identity/email-password/register.html.ejs index 9ffb990ce9..16149d1cd0 100644 --- a/templates/identity/email-password/register.html.ejs +++ b/templates/identity/email-password/register.html.ejs @@ -1,212 +1,189 @@ - - - - - - Register - - - -
- [Solid logo] -

Community Solid Server

-
-
-

Sign up

-
- <% const isBlankForm = !('email' in prefilled); %> +

Sign up

+ + <% const isBlankForm = !('email' in prefilled); %> - <% if (errorMessage) { %> -

Error: <%= errorMessage %>

- <% } %> + <% if (errorMessage) { %> +

Error: <%= errorMessage %>

+ <% } %> -
- Your WebID -

- A WebID is a unique identifier for you - in the form of a URL. -
- You WebID lets you log in to Solid apps - and access non-public data in Pods. +

+ Your WebID +

+ A WebID is a unique identifier for you + in the form of a URL. +
+ You WebID lets you log in to Solid apps + and access non-public data in Pods. +

+
    +
  1. + +

    + Please also create a Pod below, since your WebID will be stored there.

    -
      -
    1. - -

      - Please also create a Pod below, since your WebID will be stored there. -

      +
    2. +
    3. + +
        +
      1. + +
      2. -
      3. +
      4. -
          -
        1. - - -
        2. -
        3. - -
        4. -
      -
+ + +
-
- Your Pod -

- A Pod is a place to store your data. -
- If you create a new WebID, you must also create a Pod to store that WebID. -

-
    -
  1. - -
      -
    1. - - value="<%= prefilled.podName %>" <% } %>> -
    2. -
    +
    + Your Pod +

    + A Pod is a place to store your data. +
    + If you create a new WebID, you must also create a Pod to store that WebID. +

    +
      +
    1. + +
        +
      1. + + value="<%= prefilled.podName %>" <% } %>>
      -
    +
  2. +
+
-
- Your account -
-

- Choose the credentials you want to use to log in to this server in the future. -

-
    -
  1. - - value="<%= prefilled.email %>" <% } %>> -
  2. -
-
    -
  1. - - -
  2. -
  3. - - -
  4. -
-
- -
+
+ Your account +
+

+ Choose the credentials you want to use to log in to this server in the future. +

+
    +
  1. + + value="<%= prefilled.email %>" <% } %>> +
  2. +
+
    +
  1. + + +
  2. +
  3. + + +
  4. +
+
+ +
-

-
+

+ - -
- - - + // Enable all elements on form submission (otherwise their value is not submitted) + elements.mainForm.addEventListener('submit', () => { + for (const child of getDescendants(elements.mainForm)) + child.disabled = false; + }); + elements.mainForm.addEventListener('formdata', updateUI); +})(); + diff --git a/templates/identity/email-password/reset-password-response.html.ejs b/templates/identity/email-password/reset-password-response.html.ejs new file mode 100644 index 0000000000..4c7169c582 --- /dev/null +++ b/templates/identity/email-password/reset-password-response.html.ejs @@ -0,0 +1,2 @@ +

Password reset

+

Your password was successfully reset.

diff --git a/templates/identity/email-password/reset-password.html.ejs b/templates/identity/email-password/reset-password.html.ejs index 788d022945..53daac5dcb 100644 --- a/templates/identity/email-password/reset-password.html.ejs +++ b/templates/identity/email-password/reset-password.html.ejs @@ -1,44 +1,21 @@ - - - - - - Reset password - - - -
- [Solid logo] -

Community Solid Server

-
-
-

Reset password

-
- <%if (errorMessage) { %> -

<%= errorMessage %>

- <% } %> +

Reset password

+ + <%if (errorMessage) { %> +

<%= errorMessage %>

+ <% } %> -
-
    -
  1. - - -
  2. -
  3. - - -
  4. -
-
+
+
    +
  1. + + +
  2. +
  3. + + +
  4. +
+
-

-
-
- - - +

+ diff --git a/templates/identity/email-password/message.html.ejs b/templates/main.html.ejs similarity index 74% rename from templates/identity/email-password/message.html.ejs rename to templates/main.html.ejs index 2ee0240f8b..0c3dfc1b20 100644 --- a/templates/identity/email-password/message.html.ejs +++ b/templates/main.html.ejs @@ -3,7 +3,7 @@ - <%= message %> + <%= extractTitle(htmlBody) %> @@ -12,7 +12,7 @@

Community Solid Server

-

<%= message %>

+ <%- htmlBody %>

@@ -22,3 +22,10 @@

+ +<% +function extractTitle(body) { + const match = /^]*>([^<]*)<\/h1>/u.exec(body); + return match ? match[1] : 'Solid'; +} +%> diff --git a/templates/main.html.hbs b/templates/main.html.hbs deleted file mode 100644 index d9c68bae30..0000000000 --- a/templates/main.html.hbs +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - {{#if title}} - {{ title }} - {{/if}} - - - -
- [Solid logo] -

Community Solid Server

-
-
- {{{ htmlBody }}} -
- - - diff --git a/test/unit/identity/IdentityProviderHttpHandler.test.ts b/test/unit/identity/IdentityProviderHttpHandler.test.ts index 3f085fec31..8722ad077f 100644 --- a/test/unit/identity/IdentityProviderHttpHandler.test.ts +++ b/test/unit/identity/IdentityProviderHttpHandler.test.ts @@ -100,6 +100,19 @@ describe('An IdentityProviderHttpHandler', (): void => { ); }); + it('supports InteractionResponseResults without details.', async(): Promise => { + request.url = '/idp/routeResponse'; + request.method = 'POST'; + (routes.response.handler as jest.Mocked).handleSafe.mockResolvedValueOnce({ type: 'response' }); + await expect(handler.handle({ request, response })).resolves.toBeUndefined(); + expect(routes.response.handler.handleSafe).toHaveBeenCalledTimes(1); + expect(routes.response.handler.handleSafe).toHaveBeenLastCalledWith({ request, response }); + expect(templateHandler.handleSafe).toHaveBeenCalledTimes(1); + expect(templateHandler.handleSafe).toHaveBeenLastCalledWith( + { response, templateFile: routes.response.responseTemplate, contents: {}}, + ); + }); + it('calls the interactionCompleter for InteractionCompleteResults.', async(): Promise => { request.url = '/idp/routeComplete'; request.method = 'POST'; diff --git a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts index fd6b3d413d..c6bedcd425 100644 --- a/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts +++ b/test/unit/identity/interaction/email-password/handler/ResetPasswordHandler.test.ts @@ -48,10 +48,7 @@ describe('A ResetPasswordHandler', (): void => { it('renders a message on success.', async(): Promise => { request = createPostFormRequest({ password: 'password!', confirmPassword: 'password!' }, url); - await expect(handler.handle({ request, response })).resolves.toEqual({ - details: { message: 'Your password was successfully reset.' }, - type: 'response', - }); + await expect(handler.handle({ request, response })).resolves.toEqual({ type: 'response' }); expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1); expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId); expect(accountStore.deleteForgotPasswordRecord).toHaveBeenCalledTimes(1); diff --git a/test/unit/storage/conversion/MarkdownToHtmlConverter.test.ts b/test/unit/storage/conversion/MarkdownToHtmlConverter.test.ts index ee140fdc82..14ab9f1cd3 100644 --- a/test/unit/storage/conversion/MarkdownToHtmlConverter.test.ts +++ b/test/unit/storage/conversion/MarkdownToHtmlConverter.test.ts @@ -35,19 +35,4 @@ describe('A MarkdownToHtmlConverter', (): void => { { htmlBody: '

Text code more text.

\n' }, ); }); - - it('uses the main markdown header as title if there is one.', async(): Promise => { - const markdown = '# title text\nmore text'; - const representation = new BasicRepresentation(markdown, 'text/markdown', true); - const prom = converter.handle({ identifier, representation, preferences }); - await expect(prom).resolves.toBeDefined(); - const result = await prom; - expect(result.binary).toBe(true); - expect(result.metadata.contentType).toBe('text/html'); - await expect(readableToString(result.data)).resolves.toBe(''); - expect(templateEngine.render).toHaveBeenCalledTimes(1); - expect(templateEngine.render).toHaveBeenLastCalledWith( - { htmlBody: '

title text

\n

more text

\n', title: 'title text' }, - ); - }); }); diff --git a/test/unit/util/templates/ChainedTemplateEngine.test.ts b/test/unit/util/templates/ChainedTemplateEngine.test.ts new file mode 100644 index 0000000000..96933a1eda --- /dev/null +++ b/test/unit/util/templates/ChainedTemplateEngine.test.ts @@ -0,0 +1,39 @@ +import { ChainedTemplateEngine } from '../../../../src/util/templates/ChainedTemplateEngine'; +import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine'; + +describe('A ChainedTemplateEngine', (): void => { + const contents = { title: 'myTitle' }; + const template = { templateFile: '/template.tmpl' }; + let engines: jest.Mocked[]; + let engine: ChainedTemplateEngine; + + beforeEach(async(): Promise => { + engines = [ + { render: jest.fn().mockResolvedValue('body1') }, + { render: jest.fn().mockResolvedValue('body2') }, + ]; + + engine = new ChainedTemplateEngine(engines); + }); + + it('errors if no engines are provided.', async(): Promise => { + expect((): any => new ChainedTemplateEngine([])).toThrow('At least 1 engine needs to be provided.'); + }); + + it('chains the engines.', async(): Promise => { + await expect(engine.render(contents, template)).resolves.toEqual('body2'); + expect(engines[0].render).toHaveBeenCalledTimes(1); + expect(engines[0].render).toHaveBeenLastCalledWith(contents, template); + expect(engines[1].render).toHaveBeenCalledTimes(1); + expect(engines[1].render).toHaveBeenLastCalledWith({ ...contents, body: 'body1' }); + }); + + it('can use a different field to pass along the body.', async(): Promise => { + engine = new ChainedTemplateEngine(engines, 'different'); + await expect(engine.render(contents, template)).resolves.toEqual('body2'); + expect(engines[0].render).toHaveBeenCalledTimes(1); + expect(engines[0].render).toHaveBeenLastCalledWith(contents, template); + expect(engines[1].render).toHaveBeenCalledTimes(1); + expect(engines[1].render).toHaveBeenLastCalledWith({ ...contents, different: 'body1' }); + }); +});