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

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

import identity from '../src/index.js'
import getMessage from '../src/message.js'
import { downloadPdf } from '../src/attachments.js'
import { heyFetchBinary } from '../src/client.js'
import {
  BEARER,
  HEY,
  STORAGE,
  installFetch,
  jsonResponse,
  redirectResponse,
  resetKodySecrets,
} from './helpers.mjs'

const PDF_BYTES = new Uint8Array([0x25, 0x50, 0x44, 0x46])
const MESSAGE = {
  id: 8001,
  subject: 'Receipt from Acme',
  content:
    '<figure data-trix-attachment="{&quot;url&quot;:&quot;/rails/active_storage/blobs/redirect/stub/receipt.pdf&quot;,&quot;filename&quot;:&quot;receipt.pdf&quot;,&quot;contentType&quot;:&quot;application/pdf&quot;,&quot;filesize&quot;:4}"></figure>',
}

let fixture

beforeEach(() => {
  resetKodySecrets()
  fixture = installFetch(async (url, init) => {
    const method = String(init.method || 'GET').toUpperCase()
    const handler = fixture.routes.get(`${method} ${url}`)
    if (handler) return handler(url, init)
    throw new Error(`unexpected ${method} ${url}`)
  })
  fixture.routes = new Map()
})

afterEach(() => {
  fixture?.restore()
})

function on(method, url, handler) {
  fixture.routes.set(`${method} ${url}`, handler)
}

test('identity stays on app.hey.com with the placeholder bearer', async () => {
  on('GET', `${HEY}/identity`, () => jsonResponse(200, { id: 4242, name: 'Verify User' }))
  const me = await identity()
  assert.deepEqual(me, { id: 4242, name: 'Verify User' })
  assert.equal(fixture.requests[0].headers.authorization, BEARER)
  assert.equal(fixture.requests[0].redirect, 'manual')
})

test('getMessage reads the full body on app.hey.com', async () => {
  on('GET', `${HEY}/messages/8001.json`, () => jsonResponse(200, MESSAGE))
  const message = await getMessage({ messageId: 8001 })
  assert.equal(message.subject, 'Receipt from Acme')
  assert.match(message.content, /data-trix-attachment/)
})

test('PDF storage hops drop Authorization, Cookie, and the body', async () => {
  on('GET', `${HEY}/rails/active_storage/blobs/redirect/stub/receipt.pdf`, () =>
    redirectResponse(302, `${STORAGE}/receipt.pdf`)
  )
  on('GET', `${STORAGE}/receipt.pdf`, () =>
    new Response(PDF_BYTES, { status: 200, headers: { 'content-type': 'application/pdf' } })
  )
  on('GET', `${HEY}/messages/8001.json`, () => jsonResponse(200, MESSAGE))

  const pdf = await downloadPdf({
    messageId: 8001,
    filename: 'receipt.pdf',
    maxEncodedBytes: 1024,
  })
  assert.equal(typeof pdf.base64, 'string')
  assert.equal(pdf.base64, btoa(String.fromCharCode(...PDF_BYTES)))
  assert.equal('bytes' in pdf, false)
  assert.equal(pdf.url, `${HEY}/rails/active_storage/blobs/redirect/stub/receipt.pdf`)
  assert.ok(!pdf.url.startsWith(STORAGE))

  const heyHop = fixture.requests.find((row) => row.url.includes('/rails/active_storage/'))
  const storageHop = fixture.requests.find((row) => row.url.startsWith(STORAGE))
  assert.equal(heyHop.headers.authorization, BEARER)
  assert.equal(storageHop.method, 'GET')
  assert.equal(storageHop.headers.authorization, undefined)
  assert.equal(storageHop.headers.cookie, undefined)
  assert.equal(storageHop.body, undefined)
})

test('later storage hops stay anonymous even on 307', async () => {
  on('GET', `${HEY}/rails/file.pdf`, () => redirectResponse(302, `${STORAGE}/a.pdf`))
  on('GET', `${STORAGE}/a.pdf`, () => redirectResponse(307, `${STORAGE}/b.pdf`))
  on('GET', `${STORAGE}/b.pdf`, () => new Response(PDF_BYTES, { status: 200 }))
  const bytes = await heyFetchBinary('/rails/file.pdf', { headers: { Cookie: 'session=1' } })
  assert.deepEqual(bytes, PDF_BYTES)
  for (const hop of fixture.requests.filter((row) => row.url.startsWith(STORAGE))) {
    assert.equal(hop.method, 'GET')
    assert.equal(hop.headers.authorization, undefined)
    assert.equal(hop.headers.cookie, undefined)
    assert.equal(hop.body, undefined)
  }
})

test('token refresh refuses every 3xx and does not replay the grant', async () => {
  let identityHits = 0
  on('GET', `${HEY}/identity`, () => {
    identityHits += 1
    if (identityHits === 1) return jsonResponse(401, { error: 'expired' })
    return jsonResponse(200, { id: 1, name: 'nope' })
  })
  on('POST', `${HEY}/oauth/tokens`, () => redirectResponse(302, 'https://evil.example/steal'))
  await assert.rejects(() => identity(), /refused to follow a redirect/)
  const tokenCalls = fixture.requests.filter((row) => row.url === `${HEY}/oauth/tokens`)
  assert.equal(tokenCalls.length, 1)
  assert.equal(tokenCalls[0].redirect, 'manual')
  assert.equal(fixture.requests.some((row) => row.url.includes('evil.example')), false)
  assert.match(tokenCalls[0].body, /grant_type=refresh_token/)
  assert.match(tokenCalls[0].body, /install_id=/)
})