Every subject and body moves out of `mailer.js` into `engagement_templates` rows an operator can edit. A relocation, not a regression: nothing that sends mail today starts depending on an operator authoring something first. - `email.*` block family in its own registry, sharing the page family's envelope walk and validate-then-sanitize order by binding rather than by copy. - A server-side renderer producing both parts of a multipart message; the text part is byte-identical to the literals this commit deletes. - Nine seeded templates, six of them wired now; the seeder's `customized = 0` guard lives in the UPDATE's own WHERE. - `renderByKey` falls back to the shipped seed when a row is missing or unusable, so no failure of the table can stop a password reset. Also fixes `check:hosts` reading the template key `auth.email-verify` as the hostname `auth.email`. Co-Authored-By: Claude <noreply@anthropic.com>
111 lines
5.0 KiB
JavaScript
111 lines
5.0 KiB
JavaScript
// Self-test for scripts/checkNoExternalHosts.js — ENGAGEMENT.md §3.2 rule 4.
|
|
//
|
|
// The same discipline checkModuleIdentifiers.test.js established: feed the
|
|
// checker code it MUST reject and code it MUST accept, because a check that
|
|
// silently stops checking is worse than no check. The rejection cases below are
|
|
// the exact shape of the literal this phase deleted.
|
|
|
|
const { test } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const { checkFile, maskComments, isAllowed, run } = require('../../scripts/checkNoExternalHosts')
|
|
|
|
const hostsIn = (src) => checkFile('fake.js', src).map((h) => h.host)
|
|
|
|
test('catches the literal this phase deleted', () => {
|
|
const src = `
|
|
const transport = nodemailer.createTransport({
|
|
host: 'smtp.gmail.com',
|
|
port: 465,
|
|
secure: true,
|
|
})
|
|
`
|
|
assert.deepEqual(hostsIn(src), ['smtp.gmail.com'])
|
|
})
|
|
|
|
test('catches an API base url, whatever the scheme', () => {
|
|
assert.deepEqual(hostsIn(`const BASE = 'https://api.mailgun.net/v3'`), ['api.mailgun.net'])
|
|
assert.deepEqual(hostsIn(`const relay = "smtps://mail.somewhere.io:465"`), ['mail.somewhere.io'])
|
|
})
|
|
|
|
test('catches a default sender address', () => {
|
|
assert.deepEqual(hostsIn(`const FROM = 'noreply@runicgateway.com'`), ['runicgateway.com'])
|
|
})
|
|
|
|
test('catches a host in a template literal', () => {
|
|
assert.deepEqual(hostsIn('const url = `https://api.postmarkapp.com/email`'), ['api.postmarkapp.com'])
|
|
})
|
|
|
|
test('reports the line the literal is on', () => {
|
|
const src = ['// a comment', '', "const h = 'smtp.sendgrid.net'"].join('\n')
|
|
assert.deepEqual(checkFile('fake.js', src), [
|
|
{ file: 'fake.js', line: 3, literal: 'smtp.sendgrid.net', host: 'smtp.sendgrid.net' },
|
|
])
|
|
})
|
|
|
|
// ── the accept cases: the whole reason it reads code, not prose ─────────────
|
|
|
|
test('a host named in a line comment is fine — that is the documentation this phase owes', () => {
|
|
assert.deepEqual(hostsIn(`// Gmail still works as plain SMTP: smtp.gmail.com:587 with an app password\nconst x = 1`), [])
|
|
})
|
|
|
|
test('a host named in a block comment is fine', () => {
|
|
assert.deepEqual(hostsIn(`/*\n * See https://mailgun.com/docs for the relay posture.\n */\nconst x = 1`), [])
|
|
})
|
|
|
|
test('example.com placeholders are allowed — a form hint is not a destination', () => {
|
|
assert.deepEqual(hostsIn(`const f = { placeholder: 'smtp.example.com' }`), [])
|
|
assert.deepEqual(hostsIn(`const f = { placeholder: 'noreply@example.com' }`), [])
|
|
})
|
|
|
|
test('loopback is allowed', () => {
|
|
assert.deepEqual(hostsIn(`const dev = 'http://127.0.0.1:3000'`), [])
|
|
assert.deepEqual(hostsIn(`const dev = 'http://localhost:1025'`), [])
|
|
})
|
|
|
|
test('a module path is not a hostname', () => {
|
|
assert.deepEqual(hostsIn(`const m = require('../model/emailConfig/emailConfig.model')`), [])
|
|
assert.deepEqual(hostsIn(`const n = require('nodemailer')`), [])
|
|
assert.deepEqual(hostsIn(`import x from './transports/smtp.js'`), [])
|
|
})
|
|
|
|
test('an ordinary sentence with a full stop is not a hostname', () => {
|
|
assert.deepEqual(hostsIn(`const msg = 'Send failed. Check the host and port.'`), [])
|
|
})
|
|
|
|
test('a dotted identifier is not a hostname just because a real TLD is a label', () => {
|
|
// `.email`, `.mail` and `.app` are real TLDs, so an engagement template key or a
|
|
// trigger id can look like a host to a regex. A real hostname's TLD is its LAST
|
|
// label; these carry on into another word.
|
|
assert.deepEqual(hostsIn(`const key = 'auth.email-verify'`), [])
|
|
assert.deepEqual(hostsIn(`await body('auth.email-verify', { verifyUrl })`), [])
|
|
assert.deepEqual(hostsIn(`const t = 'core.mail_bounced'`), [])
|
|
// …and the real thing still trips it, so the loosening did not blunt the check.
|
|
assert.deepEqual(hostsIn(`const h = 'smtp.somewhere.email'`), ['smtp.somewhere.email'])
|
|
assert.deepEqual(hostsIn(`const h = 'relay.somewhere.email:587'`), ['relay.somewhere.email'])
|
|
})
|
|
|
|
// ── the pieces, directly ────────────────────────────────────────────────────
|
|
|
|
test('maskComments blanks comments but keeps string bodies and line count', () => {
|
|
const src = "// smtp.gmail.com\nconst h = 'smtp.relay.net'\n"
|
|
const masked = maskComments(src)
|
|
assert.equal(masked.split('\n').length, src.split('\n').length)
|
|
assert.ok(!masked.includes('smtp.gmail.com'))
|
|
assert.ok(masked.includes('smtp.relay.net'))
|
|
})
|
|
|
|
test('isAllowed covers the reserved documentation names and nothing else', () => {
|
|
assert.equal(isAllowed('example.com'), true)
|
|
assert.equal(isAllowed('mail.example.org'), true)
|
|
assert.equal(isAllowed('localhost'), true)
|
|
assert.equal(isAllowed('smtp.gmail.com'), false)
|
|
assert.equal(isAllowed('api.postmarkapp.com'), false)
|
|
})
|
|
|
|
// ── and the real tree ───────────────────────────────────────────────────────
|
|
|
|
test('the shipped engagement tree is clean', () => {
|
|
assert.deepEqual(run(), [])
|
|
})
|