const { test, before, after } = require('node:test'); const assert = require('node:assert'); const { spawn } = require('node:child_process'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const PORT = 3999; const BASE = `http://127.0.0.1:${PORT}`; const API_KEY = 'test-key'; let server; let dataDir; before(async () => { dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'form-capture-test-')); server = spawn('node', [path.join(__dirname, '..', 'index.js')], { env: { ...process.env, PORT: String(PORT), DATA_DIR: dataDir, API_KEY }, stdio: 'ignore', }); // Wait for the server to come up for (let i = 0; i < 50; i++) { try { const res = await fetch(`${BASE}/health`); if (res.ok) return; } catch {} await new Promise(r => setTimeout(r, 100)); } throw new Error('server did not start'); }); after(() => { server.kill(); fs.rmSync(dataDir, { recursive: true, force: true }); }); test('email-only submission still works (backward compat)', async () => { const res = await fetch(`${BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'a@b.com', form_name: 'newsletter' }), }); assert.strictEqual(res.status, 200); assert.strictEqual((await res.json()).success, true); }); test('message-only submission works (review intercept)', async () => { const res = await fetch(`${BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'The app crashed, twice', form_name: 'review_intercept', source: 'ios' }), }); assert.strictEqual(res.status, 200); }); test('message + reply email submission works', async () => { const res = await fetch(`${BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'reply@me.com', message: 'line1\nline2, "quoted"', form_name: 'review_intercept' }), }); assert.strictEqual(res.status, 200); }); test('empty body is rejected', async () => { const res = await fetch(`${BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ form_name: 'review_intercept' }), }); assert.strictEqual(res.status, 400); assert.match((await res.json()).error, /Email or message/); }); test('whitespace-only message is rejected', async () => { const res = await fetch(`${BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: ' ' }), }); assert.strictEqual(res.status, 400); }); test('invalid email is still rejected even with message', async () => { const res = await fetch(`${BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'not-an-email', message: 'hello' }), }); assert.strictEqual(res.status, 400); }); test('message longer than 10k chars is truncated, not rejected', async () => { const res = await fetch(`${BASE}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'x'.repeat(20000), form_name: 'review_intercept' }), }); assert.strictEqual(res.status, 200); const list = await (await fetch(`${BASE}/emails?form_name=review_intercept&limit=100`, { headers: { Authorization: `Bearer ${API_KEY}` }, })).json(); const long = list.submissions.find(s => s.message && s.message.startsWith('xxx')); assert.ok(long, 'truncated submission should be listed'); assert.strictEqual(long.message.length, 10000); }); test('/emails returns message field', async () => { const res = await fetch(`${BASE}/emails?form_name=review_intercept`, { headers: { Authorization: `Bearer ${API_KEY}` }, }); assert.strictEqual(res.status, 200); const body = await res.json(); const withMsg = body.submissions.find(s => s.message === 'The app crashed, twice'); assert.ok(withMsg, 'message should round-trip through /emails'); assert.strictEqual(withMsg.email, ''); }); test('/export CSV includes and escapes message', async () => { const res = await fetch(`${BASE}/export?form_name=review_intercept`, { headers: { Authorization: `Bearer ${API_KEY}` }, }); assert.strictEqual(res.status, 200); const csv = await res.text(); assert.match(csv.split('\n')[0], /^email,form_name,source,message,created_at$/); // Multi-line + quoted message must be wrapped and quote-doubled assert.ok(csv.includes('"line1\nline2, ""quoted"""'), 'CSV should escape newlines, commas, quotes'); });