- invoice.paid classified welcome-vs-renewal via emailDeliveredAt (durable, webhook-order independent) through new classifySubscriptionEmail() - the old isNewLicense/row-existence signal broke under Stripe's real event order (subscription.created pre-creates the row before the first invoice). - renderLicenseEmail() extracted pure; first purchases now get 'Welcome to DashCaddy Premium' with unlocks panel (SSO/Recipes/Swarm/auto-renew) and 3-step getting-started; renewal copy byte-identical (test-pinned). - 9 new tests (37 total): behavioral classifier coverage, rendered-content assertions both variants, store flag regression, mkdtemp-sandboxed. - Codex judge: A, zero blockers (r5; prior rounds D-D-D-C drove the fixes). Verdict: run output 'A' with empty Blockers (direct codex exec, 2026-09-22).
60 lines
3.3 KiB
JavaScript
60 lines
3.3 KiB
JavaScript
import assert from 'node:assert';
|
|
import fs from 'node:fs';
|
|
import { test } from 'node:test';
|
|
|
|
// Render-only tests: no SMTP, no store. email.js imports only nodemailer.
|
|
const { renderLicenseEmail } = await import('../src/email.js');
|
|
|
|
const base = {
|
|
code: 'DC-20F00-0000D-NB3FN-S6HXJ-A7CA0',
|
|
durationDays: 30,
|
|
planCode: 'premium_30d',
|
|
expiresAt: '2026-10-21T23:33:00.000Z'
|
|
};
|
|
|
|
test('first purchase renders WELCOME email with walkthrough', () => {
|
|
const mail = renderLicenseEmail({ ...base, extended: false });
|
|
|
|
assert.equal(mail.subject, 'Your DashCaddy Premium license key',
|
|
'first purchase subject must NOT say "extended"');
|
|
assert.ok(!/extended/i.test(mail.subject), 'subject must contain no extended wording');
|
|
assert.ok(mail.html.includes('Welcome to DashCaddy Premium'), 'welcome headline');
|
|
assert.ok(mail.html.includes('What your subscription unlocks'), 'unlocks panel');
|
|
assert.ok(mail.html.includes('Auto-Login SSO'), 'feature: SSO');
|
|
assert.ok(mail.html.includes('Recipes'), 'feature: Recipes');
|
|
assert.ok(mail.html.includes('Swarm orchestration'), 'feature: Swarm');
|
|
assert.ok(mail.html.includes('Automatic renewals'), 'feature: auto-renewal value prop');
|
|
assert.ok(mail.html.includes('Getting started takes about a minute'), 'getting-started steps');
|
|
assert.ok(mail.html.includes('Admin'), 'activation path shown');
|
|
assert.ok(mail.text.includes('YOUR SUBSCRIPTION INCLUDES:'), 'plain text unlocks block');
|
|
assert.ok(mail.text.includes('GETTING STARTED:'), 'plain text steps block');
|
|
assert.equal((mail.text.match(/To activate:/g) || []).length, 0,
|
|
'welcome email replaces the bare activation line with the 3-step block');
|
|
assert.ok(mail.text.includes('Paste your license key and click Activate'),
|
|
'activation still covered, inside the getting-started steps');
|
|
assert.equal((mail.html.match(/data:image/g) || []).length, 2,
|
|
'two embedded logos (DashCaddy header + samiahmed7777 footer)');
|
|
});
|
|
|
|
test('renewal email keeps the extended copy and has NO welcome content', () => {
|
|
const mail = renderLicenseEmail({ ...base, extended: true });
|
|
|
|
assert.equal(mail.subject, 'Your DashCaddy Premium license has been extended (30 days added)');
|
|
assert.ok(mail.html.includes('License extended'), 'extended headline');
|
|
assert.ok(!mail.html.includes('Welcome to DashCaddy Premium'), 'no welcome headline');
|
|
assert.ok(!mail.html.includes('What your subscription unlocks'), 'no unlocks panel');
|
|
assert.ok(!mail.html.includes('Getting started takes about a minute'), 'no getting-started steps');
|
|
assert.ok(!mail.text.includes('YOUR SUBSCRIPTION INCLUDES:'), 'plain text: no unlocks block');
|
|
assert.ok(!mail.text.includes('GETTING STARTED:'), 'plain text: no steps block');
|
|
assert.equal((mail.text.match(/To activate:/g) || []).length, 1,
|
|
'renewal keeps its single activation line');
|
|
});
|
|
|
|
test('render matches production send path (sendLicenseEmail wraps renderLicenseEmail)', async () => {
|
|
const src = fs.readFileSync(new URL('../src/email.js', import.meta.url), 'utf8');
|
|
assert.ok(src.includes('renderLicenseEmail({ code, durationDays, planCode, expiresAt, extended })'),
|
|
'sendLicenseEmail must delegate to renderLicenseEmail — one shared code path');
|
|
assert.ok(src.includes('export async function sendLicenseEmail'),
|
|
'send path still exported');
|
|
});
|