← 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 · JavaScriptimport 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="{"url":"/rails/active_storage/blobs/redirect/stub/receipt.pdf","filename":"receipt.pdf","contentType":"application/pdf","filesize":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=/)
})