Skip to content
← 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 · JavaScript
Raw
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: '<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'] } },
  })
})