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/drafts.test.js

169 lines · 6.2 KB · JavaScript
Raw
import assert from 'node:assert/strict'
import { afterEach, beforeEach, test } from 'node:test'

import { createDraft, createReplyDraft } from '../src/drafts.js'
import { HEY, installFetch, jsonResponse, resetKodySecrets } from './helpers.mjs'

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 routeIdentity(identity = { senders: [{ id: 3 }, { id: 5, default: true }], primary_contact: { id: 1 } }) {
  on('GET', `${HEY}/identity.json`, () => jsonResponse(200, identity))
}

function savedAt(location) {
  return () => new Response(null, { status: 204, headers: location ? { location } : {} })
}

function routeReplyPrefill(extra = {}) {
  on('GET', `${HEY}/entries/42/replies/new.json`, () =>
    jsonResponse(200, {
      url: `${HEY}/entries/42/replies`,
      subject: 'Re: Notes',
      content: '<blockquote>Earlier</blockquote>',
      creator: { id: 7, name_tag: '<p>Dan</p>' },
      addressed: {
        directly: [{ email_address: 'ada@example.com' }],
        copied: [{ email_address: 'cy@example.com' }],
        blindcopied: [],
      },
      ...extra,
    })
  )
}

test('createDraft posts the nested drafted body with the default sender from /identity.json', async () => {
  routeIdentity()
  on('POST', `${HEY}/messages.json`, savedAt(`${HEY}/messages/12345`))
  const draft = await createDraft({ subject: 'Hello World', text: 'Hello World', to: 'danielj@hey.com' })
  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: 'Hello World', content: '<div>Hello World</div>' },
    entry: { status: 'drafted', addressed: { directly: ['danielj@hey.com'] } },
  })
  assert.equal(draft.id, 12345)
  assert.equal(draft.url, `${HEY}/messages/12345`)
  assert.equal(draft.edit_url, `${HEY}/messages/12345/edit`)
  assert.deepEqual(draft.to, ['danielj@hey.com'])
})

test('createDraft saves to, cc, and bcc as directly, copied, and blindcopied', async () => {
  routeIdentity()
  on('POST', `${HEY}/messages.json`, savedAt(`${HEY}/messages/12345`))
  await createDraft({
    subject: 'Notes',
    html: '<p>Rich</p>',
    to: ['ada@example.com', 'bob@example.com'],
    cc: 'cy@example.com',
    bcc: ['eve@example.com'],
  })
  assert.deepEqual(JSON.parse(writes()[0].body).entry, {
    status: 'drafted',
    addressed: {
      directly: ['ada@example.com', 'bob@example.com'],
      copied: ['cy@example.com'],
      blindcopied: ['eve@example.com'],
    },
  })
})

test('createDraft with no recipients still sends entry.status drafted and an empty addressed', async () => {
  routeIdentity()
  on('POST', `${HEY}/messages.json`, savedAt(`${HEY}/messages/12345`))
  await createDraft({ subject: 'Notes', text: 'Later' })
  assert.deepEqual(JSON.parse(writes()[0].body).entry, { status: 'drafted', addressed: {} })
})

test('createDraft picks the first sender, then the primary contact, when no sender is default', async () => {
  const cases = [
    [{ senders: [{ id: 3 }, { id: 4 }], primary_contact: { id: 1 } }, 3],
    [{ senders: [], primary_contact: { id: 9 } }, 9],
  ]
  for (const [identity, expected] of cases) {
    fixture.requests.length = 0
    routeIdentity(identity)
    on('POST', `${HEY}/messages.json`, savedAt(`${HEY}/messages/12345`))
    await createDraft({ subject: 'Notes', text: 'Hi' })
    assert.equal(JSON.parse(writes()[0].body).acting_sender_id, expected)
  }
})

test('createDraft reads the id from a relative Location', async () => {
  routeIdentity()
  on('POST', `${HEY}/messages.json`, savedAt('/messages/98'))
  const draft = await createDraft({ subject: 'Notes', text: 'Hi' })
  assert.equal(draft.id, 98)
})

test('createDraft rejects when HEY names no Location', async () => {
  routeIdentity()
  on('POST', `${HEY}/messages.json`, savedAt(null))
  await assert.rejects(() => createDraft({ subject: 'Notes', text: 'Hi' }), /Location/)
})

test('createReplyDraft posts the drafted body with the thread recipients and no echoed quote', async () => {
  routeIdentity()
  routeReplyPrefill()
  on('POST', `${HEY}/entries/42/replies.json`, savedAt(`${HEY}/messages/777`))
  const draft = await createReplyDraft({ entryId: 42, text: 'Thanks' })
  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: {
      status: 'drafted',
      addressed: { directly: ['ada@example.com'], copied: ['cy@example.com'] },
    },
  })
  assert.equal(draft.id, 777)
  assert.equal(draft.edit_url, `${HEY}/messages/777/edit`)
  assert.deepEqual(draft.to, ['ada@example.com'])
  assert.deepEqual(draft.cc, ['cy@example.com'])
})

test('createReplyDraft acts as the prefill sender and skips the identity lookup', async () => {
  routeReplyPrefill({ sender: { id: 4242 } })
  on('POST', `${HEY}/entries/42/replies.json`, savedAt(`${HEY}/messages/777`))
  await createReplyDraft({ entryId: 42, text: 'Thanks' })
  assert.equal(JSON.parse(writes()[0].body).acting_sender_id, 4242)
  assert.equal(fixture.requests.some((row) => row.url === `${HEY}/identity.json`), false)
})

test('createReplyDraft input recipients replace the thread recipients', async () => {
  routeIdentity()
  routeReplyPrefill()
  on('POST', `${HEY}/entries/42/replies.json`, savedAt(`${HEY}/messages/777`))
  await createReplyDraft({ entryId: 42, text: 'Thanks', to: 'bob@example.com' })
  assert.deepEqual(JSON.parse(writes()[0].body).entry, {
    status: 'drafted',
    addressed: { directly: ['bob@example.com'] },
  })
})