import request from 'supertest'; import app from '../../src/app'; import { pool } from '../../src/db'; import { resetDb, closeDb } from './setup/testDb'; import { renderTemplate } from '../../src/emailTemplates'; import { loadStoredTemplate } from '../../src/routes/adminEmailTemplates'; beforeEach(async () => { // resetDb clears stored email templates as well, so a template saved by one // test cannot change the mail another asserts on. await resetDb(); }); afterAll(async () => { await pool.end(); await closeDb(); }); const VALID_RESET_BODY = 'New copy. [Choose a new password]({{resetUrl}}) within the hour.'; describe('GET /api/admin/email-templates', () => { it('lists every template with its defaults and placeholder rules', async () => { const res = await request(app).get('/api/admin/email-templates'); expect(res.status).toBe(200); expect(res.body.map((t: { key: string }) => t.key).sort()).toEqual([ 'cartReminder', 'favoriteSold', 'favoriteWithdrawn', 'passwordReset', 'verification' ]); const reset = res.body.find((t: { key: string }) => t.key === 'passwordReset'); expect(reset.required).toEqual(['resetUrl']); expect(reset.defaultBody).toContain('{{resetUrl}}'); }); // Null rather than the default text, so the screen can tell "never edited" // from "edited to something identical to the default". it('reports an uncustomised template as null rather than as its default', async () => { const res = await request(app).get('/api/admin/email-templates'); const reset = res.body.find((t: { key: string }) => t.key === 'passwordReset'); expect(reset.subject).toBeNull(); expect(reset.body).toBeNull(); }); }); describe('PUT /api/admin/email-templates/:key', () => { it('stores a replacement subject and body', async () => { const res = await request(app) .put('/api/admin/email-templates/passwordReset') .send({ subject: 'Your reset link', body: VALID_RESET_BODY }); expect(res.status).toBe(200); const listed = await request(app).get('/api/admin/email-templates'); const reset = listed.body.find((t: { key: string }) => t.key === 'passwordReset'); expect(reset.subject).toBe('Your reset link'); expect(reset.body).toBe(VALID_RESET_BODY); }); // The rule that keeps this from being a way to break password resets from a // settings screen. it('refuses a body that has dropped the required placeholder', async () => { const res = await request(app) .put('/api/admin/email-templates/passwordReset') .send({ subject: 'Your reset link', body: 'Just click the thing.' }); expect(res.status).toBe(400); expect(res.body.error).toContain('{{resetUrl}}'); }); it('names every missing placeholder, not just the first', async () => { const res = await request(app) .put('/api/admin/email-templates/cartReminder') .send({ subject: 'Your cart', body: 'You left things behind.' }); expect(res.status).toBe(400); expect(res.body.error).toContain('{{itemList}}'); expect(res.body.error).toContain('{{cartUrl}}'); }); it('refuses an empty subject or body', async () => { const noSubject = await request(app) .put('/api/admin/email-templates/passwordReset') .send({ subject: ' ', body: VALID_RESET_BODY }); expect(noSubject.status).toBe(400); const noBody = await request(app) .put('/api/admin/email-templates/passwordReset') .send({ subject: 'Something', body: ' ' }); expect(noBody.status).toBe(400); }); it('refuses a template key it does not recognise', async () => { const res = await request(app) .put('/api/admin/email-templates/invoiceOverdue') .send({ subject: 'x', body: 'y' }); expect(res.status).toBe(404); }); // Nothing is stored when a save is refused, or a half-applied edit would sit // there looking accepted. it('stores nothing when it refuses', async () => { await request(app) .put('/api/admin/email-templates/passwordReset') .send({ subject: 'Your reset link', body: 'No link here.' }); const { rows } = await pool.query(`SELECT key FROM admin_settings WHERE key LIKE 'email%'`); expect(rows).toHaveLength(0); }); }); describe('DELETE /api/admin/email-templates/:key', () => { it('restores the built-in copy by forgetting the stored rows', async () => { await request(app) .put('/api/admin/email-templates/passwordReset') .send({ subject: 'Custom', body: VALID_RESET_BODY }); const res = await request(app).delete('/api/admin/email-templates/passwordReset'); expect(res.status).toBe(200); const listed = await request(app).get('/api/admin/email-templates'); const reset = listed.body.find((t: { key: string }) => t.key === 'passwordReset'); expect(reset.subject).toBeNull(); expect(reset.body).toBeNull(); }); }); // The point of the whole feature: what is stored is what customers receive. describe('what a stored template does to the email that gets sent', () => { it('is used in place of the default once saved', async () => { await request(app) .put('/api/admin/email-templates/passwordReset') .send({ subject: 'Reset requested', body: VALID_RESET_BODY }); const { subject, html } = renderTemplate( 'passwordReset', await loadStoredTemplate('passwordReset'), { greeting: 'Hi Thom,', resetUrl: 'https://shop.test/r?token=abc' } ); expect(subject).toBe('Reset requested'); expect(html).toContain('New copy.'); expect(html).toContain('https://shop.test/r?token=abc'); }); it('falls back to the built-in copy when nothing is stored', async () => { const { subject, html } = renderTemplate( 'passwordReset', await loadStoredTemplate('passwordReset'), { greeting: 'Hi,', resetUrl: 'https://shop.test/r?token=abc' } ); expect(subject).toBe('Reset your Redefined Designs password'); expect(html).toContain('Choose a new password'); }); // Editing the copy must not be able to remove the sentence that explains why // the email is lawful to send. it('keeps the consent footer on a favorite alert whose body was replaced', async () => { await request(app) .put('/api/admin/email-templates/favoriteSold') .send({ subject: 'Gone', body: 'Sorry, {{itemName}} sold.' }); const { html } = renderTemplate('favoriteSold', await loadStoredTemplate('favoriteSold'), { itemName: 'Oak table', siteUrl: 'https://shop.test' }); expect(html).toContain('Sorry, Oak table sold.'); expect(html).toContain('account page'); }); });