import assert from 'node:assert/strict' import { afterEach, beforeEach, test } from 'node:test' import { SENDING as COMMITTED_SENDING } from '../src/config.js' import { HEY, installFetch, jsonResponse, redirectResponse, resetKodySecrets } from './helpers.mjs' import { ALLOWED_QUERY } from './hooks.mjs' const { SENDING } = await import(`../src/config.js${ALLOWED_QUERY}`) const { sendDraft, sendMessage, sendReply } = await import(`../src/send.js${ALLOWED_QUERY}`) const UNCONFIRMED = [{ confirm: 'true' }, { confirm: 1 }, { confirm: false }, {}] let fixture beforeEach(() => { resetKodySecrets() fixture = installFetch(async (url, init) => { const handler = fixture.routes.get(`${String(init.method || 'GET').toUpperCase()} ${url}`) if (handler) return handler(url, init) throw new Error(`unexpected ${init.method || 'GET'} ${url}`) }) fixture.routes = new Map() }) afterEach(() => { fixture?.restore() }) function on(method, url, handler) { fixture.routes.set(`${method} ${url}`, handler) } function writes() { return fixture.requests.filter((row) => row.method !== 'GET') } function routeDraft(contacts = [{ name: 'Ada', email_address: 'ada@example.com' }], extra = {}) { on('GET', `${HEY}/messages/9/edit.json`, () => jsonResponse(200, { id: 9, subject: 'Draft', content: '
Hi
', creator: { id: 1 }, addressed: { directly: contacts, copied: [], blindcopied: [] }, ...extra, }) ) } function routeIdentity() { on('GET', `${HEY}/identity.json`, () => jsonResponse(200, { senders: [{ id: 5, default: true }] })) } function routeReplyPrefill(directly = [{ email_address: 'ada@example.com' }], extra = {}) { on('GET', `${HEY}/entries/42/replies/new.json`, () => jsonResponse(200, { subject: 'Re: Notes', content: '
Earlier
', creator: { id: 7, name_tag: '

Dan

' }, addressed: { directly }, ...extra, }) ) } test('the allowed graph is separate from the committed barred config', () => { assert.equal(COMMITTED_SENDING, 'barred') assert.equal(SENDING, 'allowed') }) test('allowed sendMessage without boolean confirm is a dry run with no request', async () => { for (const confirm of UNCONFIRMED) { const result = await sendMessage({ to: 'ada@example.com', subject: 'Notes', text: 'Hi', ...confirm }) assert.deepEqual(result, { dryRun: true, wouldSend: true, to: ['ada@example.com'], subject: 'Notes', content: '
Hi
', }) } assert.equal(fixture.requests.length, 0) }) test('allowed sendMessage dry run prefers html over text', async () => { const result = await sendMessage({ to: 'ada@example.com', subject: 'Notes', text: 'Hi', html: '

Rich

' }) assert.equal(result.content, '

Rich

') }) test('allowed sendMessage reports sent false when HEY does not say sent', async () => { routeIdentity() on('POST', `${HEY}/messages.json`, () => jsonResponse(202, { notice: null })) const result = await sendMessage({ to: 'ada@example.com', subject: 'Notes', text: 'Hi', confirm: true }) assert.equal(result.sent, false) }) test('allowed sendMessage with confirm true posts the nested body with no drafted status', async () => { routeIdentity() on('POST', `${HEY}/messages.json`, () => jsonResponse(200, { notice: 'sent' })) const result = await sendMessage({ to: 'ada@example.com', cc: 'bob@example.com', bcc: ['eve@example.com'], subject: 'Notes', text: 'Hi', confirm: true, }) assert.equal(result.sent, true) const [post] = writes() assert.equal(post.method, 'POST') assert.equal(post.url, `${HEY}/messages.json`) assert.equal(post.headers['content-type'], 'application/json') assert.deepEqual(JSON.parse(post.body), { acting_sender_id: 5, message: { subject: 'Notes', content: '
Hi
' }, entry: { addressed: { directly: ['ada@example.com'], copied: ['bob@example.com'], blindcopied: ['eve@example.com'], }, }, }) }) test('allowed sendReply without boolean confirm reads the prefill and does not post', async () => { routeReplyPrefill() for (const confirm of UNCONFIRMED) { const result = await sendReply({ entryId: 42, text: 'Thanks', subject: 'Custom', ...confirm }) assert.deepEqual(result, { dryRun: true, wouldSend: true, to: ['ada@example.com'], subject: 'Custom', content: '
Thanks

Dan

', }) } assert.deepEqual(writes(), []) }) test('allowed sendReply with no prefill recipients throws without a post', async () => { routeReplyPrefill([]) await assert.rejects(() => sendReply({ entryId: 42, text: 'Thanks', confirm: true }), /no recipients/) assert.deepEqual(writes(), []) }) test('allowed sendReply reports sent false when HEY does not say sent', async () => { routeIdentity() routeReplyPrefill() on('POST', `${HEY}/entries/42/replies.json`, () => jsonResponse(202, { notice: null })) const result = await sendReply({ entryId: 42, text: 'Thanks', confirm: true }) assert.equal(result.sent, false) }) test('allowed sendReply with confirm true posts the nested body to the prefill recipients', async () => { routeIdentity() routeReplyPrefill() on('POST', `${HEY}/entries/42/replies.json`, () => jsonResponse(200, { notice: 'sent' })) const result = await sendReply({ entryId: 42, text: 'Thanks', confirm: true }) assert.equal(result.sent, true) const [post] = writes() assert.equal(post.method, 'POST') assert.equal(post.url, `${HEY}/entries/42/replies.json`) assert.equal(post.headers['content-type'], 'application/json') assert.deepEqual(JSON.parse(post.body), { acting_sender_id: 5, message: { subject: 'Re: Notes', content: '
Thanks

Dan

' }, entry: { addressed: { directly: ['ada@example.com'] } }, }) }) test('allowed sendReply acts as the prefill sender', async () => { routeReplyPrefill([{ email_address: 'ada@example.com' }], { sender: { id: 4242 } }) on('POST', `${HEY}/entries/42/replies.json`, () => jsonResponse(200, { notice: 'sent' })) await sendReply({ entryId: 42, text: 'Thanks', confirm: true }) assert.equal(JSON.parse(writes()[0].body).acting_sender_id, 4242) }) test('allowed sendDraft without boolean confirm reads the draft and does not put', async () => { routeDraft() for (const confirm of UNCONFIRMED) { const result = await sendDraft({ id: 9, ...confirm }) assert.equal(result.dryRun, true) assert.equal(result.wouldSend, true) assert.deepEqual(result.to, ['ada@example.com']) assert.equal(result.draft.subject, 'Draft') assert.equal(result.draft.content, '
Hi
') } assert.deepEqual(writes(), []) }) test('allowed sendDraft with no recipients throws without a put', async () => { routeDraft([]) await assert.rejects(() => sendDraft({ id: 9, confirm: true }), /at least one to/) assert.deepEqual(writes(), []) }) test('allowed sendDraft input.to replaces the draft recipients', async () => { routeDraft() on('PUT', `${HEY}/messages/9.json`, () => jsonResponse(200, { notice: 'sent' })) const result = await sendDraft({ id: 9, to: ['bob@example.com'], confirm: true }) assert.deepEqual(result.to, ['bob@example.com']) assert.deepEqual(JSON.parse(writes()[0].body).entry.addressed, { directly: ['bob@example.com'] }) }) test('allowed sendDraft takes one to address as a string', async () => { routeDraft() on('PUT', `${HEY}/messages/9.json`, () => jsonResponse(200, { notice: 'sent' })) const result = await sendDraft({ id: 9, to: 'bob@example.com', confirm: true }) assert.deepEqual(result.to, ['bob@example.com']) assert.deepEqual(JSON.parse(writes()[0].body).entry.addressed, { directly: ['bob@example.com'] }) }) test('allowed sendDraft keeps the draft sender, cc, and bcc', async () => { routeDraft([{ email_address: 'ada@example.com' }], { sender: { id: 4242 }, addressed: { directly: [{ email_address: 'ada@example.com' }], copied: [{ email_address: 'cy@example.com' }], blindcopied: [{ email_address: 'eve@example.com' }], }, }) on('PUT', `${HEY}/messages/9.json`, () => jsonResponse(200, { notice: 'sent' })) await sendDraft({ id: 9, confirm: true }) assert.deepEqual(JSON.parse(writes()[0].body), { acting_sender_id: 4242, message: { subject: 'Draft', content: '
Hi
' }, entry: { addressed: { directly: ['ada@example.com'], copied: ['cy@example.com'], blindcopied: ['eve@example.com'], }, }, }) }) test('allowed sendDraft PUT 301 keeps PUT and the nested payload', async () => { routeDraft() on('PUT', `${HEY}/messages/9.json`, () => redirectResponse(301, `${HEY}/messages/9-canonical.json`)) on('PUT', `${HEY}/messages/9-canonical.json`, () => jsonResponse(200, { notice: 'sent' })) const result = await sendDraft({ id: 9, confirm: true }) assert.equal(result.sent, true) const put = writes().at(-1) assert.equal(put.url, `${HEY}/messages/9-canonical.json`) assert.equal(put.method, 'PUT') assert.equal(put.headers['content-type'], 'application/json') assert.deepEqual(JSON.parse(put.body), { acting_sender_id: 1, message: { subject: 'Draft', content: '
Hi
' }, entry: { addressed: { directly: ['ada@example.com'] } }, }) })