← Public packages
@danj/hey-email
HEY.com email for Kody: search threads, read messages, download PDF attachments, and write drafts. Sending is barred until you allow it.
test/sending-allowed.test.js
253 lines · 9.2 KB · JavaScriptimport 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: '<div>Hi</div>',
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: '<blockquote>Earlier</blockquote>',
creator: { id: 7, name_tag: '<p>Dan</p>' },
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: '<div>Hi</div>',
})
}
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: '<p>Rich</p>' })
assert.equal(result.content, '<p>Rich</p>')
})
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: '<div>Hi</div>' },
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: '<div>Thanks</div><p>Dan</p>',
})
}
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: '<div>Thanks</div><p>Dan</p>' },
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, '<div>Hi</div>')
}
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: '<div>Hi</div>' },
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: '<div>Hi</div>' },
entry: { addressed: { directly: ['ada@example.com'] } },
})
})