import assert from 'node:assert/strict'
import { afterEach, beforeEach, test } from 'node:test'
import listPdfs, { downloadPdf } from '../src/attachments.js'
import {
BEARER,
HEY,
STORAGE,
installFetch,
jsonResponse,
redirectResponse,
resetKodySecrets,
} from './helpers.mjs'
const PDF_BYTES = new Uint8Array([0x25, 0x50, 0x44, 0x46])
const PDF_BASE64 = btoa(String.fromCharCode(...PDF_BYTES))
const MAX_RAW_BYTES = 24 * 1024 * 1024
const OVER_RAW = MAX_RAW_BYTES + 1
const TWO_PDFS =
'' +
''
const UNDERSTATED =
''
const RAILS_ONLY = '/rails/active_storage/blobs/redirect/stub/rails.pdf'
const DUP_PDFS =
'' +
''
const OVER_CAP =
``
const EXACT_CAP =
``
const SMALL_PDF_URL = `${HEY}/rails/active_storage/blobs/redirect/stub/small.pdf`
const BIG_PDF_URL = `${HEY}/rails/active_storage/blobs/redirect/stub/big.pdf`
const HUGE_PDF_URL = `${HEY}/rails/active_storage/blobs/redirect/stub/huge.pdf`
const RAILS_PDF_URL = `${HEY}/rails/active_storage/blobs/redirect/stub/rails.pdf`
const INVOICE_A_URL = `${HEY}/rails/active_storage/blobs/redirect/stub/a/invoice.pdf`
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)
}
function stubPdf(heyUrl, body = PDF_BYTES, extraHeaders = {}) {
const storageUrl = `${STORAGE}${new URL(heyUrl).pathname}`
on('GET', heyUrl, () => redirectResponse(302, storageUrl))
on('GET', storageUrl, () =>
new Response(body, { status: 200, headers: { 'content-type': 'application/pdf', ...extraHeaders } }),
)
return storageUrl
}
function stubSmallPdf() {
stubPdf(SMALL_PDF_URL)
}
function countingStream(chunkBytes = 65536) {
const state = { pulled: 0 }
const hardStop = MAX_RAW_BYTES + 1024 * 1024
const stream = new ReadableStream({
pull(controller) {
state.pulled += chunkBytes
if (state.pulled > hardStop) {
controller.error(new Error('read past the 24 MiB cap'))
return
}
controller.enqueue(new Uint8Array(chunkBytes))
},
})
return { stream, state }
}
function stubOversizePdf(heyUrl, { contentLength, chunkBytes = 65536 } = {}) {
const storageUrl = `${STORAGE}${new URL(heyUrl).pathname}`
const { stream, state } = countingStream(chunkBytes)
const headers = { 'content-type': 'application/pdf' }
if (contentLength != null) headers['content-length'] = String(contentLength)
on('GET', heyUrl, () => redirectResponse(302, storageUrl))
on('GET', storageUrl, () => new Response(stream, { status: 200, headers }))
return state
}
function envelopeSize({ filename, contentType, url, filesize, base64 }) {
return JSON.stringify({ filename, contentType, url, filesize, base64 }).length
}
function smallEnvelope() {
return envelopeSize({
filename: 'small.pdf',
contentType: 'application/pdf',
url: SMALL_PDF_URL,
filesize: PDF_BYTES.byteLength,
base64: PDF_BASE64,
})
}
function railsEnvelope() {
return envelopeSize({
filename: 'rails.pdf',
contentType: 'application/pdf',
url: RAILS_PDF_URL,
filesize: PDF_BYTES.byteLength,
base64: PDF_BASE64,
})
}
function assertMetadataOnly(item) {
assert.equal('bytes' in item, false)
assert.equal('base64' in item, false)
assert.ok(!item.url.startsWith(STORAGE))
assert.equal(new URL(item.url).origin, HEY)
}
function assertNoPdfDownload() {
assert.equal(
fixture.requests.some((row) => row.url.includes('/rails/active_storage/') || row.url.startsWith(STORAGE)),
false,
)
}
test('listPdfs returns metadata only and does not download bodies', async () => {
on('GET', `${HEY}/messages/8001.json`, () =>
jsonResponse(200, { id: 8001, content: TWO_PDFS }),
)
const pdfs = await listPdfs({ messageId: 8001 })
assert.equal(pdfs.length, 2)
assert.deepEqual(pdfs, [
{
index: 0,
filename: 'big.pdf',
contentType: 'application/pdf',
url: BIG_PDF_URL,
filesize: 6970000,
},
{
index: 1,
filename: 'small.pdf',
contentType: 'application/pdf',
url: SMALL_PDF_URL,
filesize: 4,
},
])
pdfs.forEach(assertMetadataOnly)
assert.deepEqual(
fixture.requests.map((row) => row.url),
[`${HEY}/messages/8001.json`],
)
assert.equal(fixture.requests[0].headers.authorization, BEARER)
})
test('downloadPdf returns one file as base64 by filename', async () => {
stubSmallPdf()
const pdf = await downloadPdf({
content: TWO_PDFS,
filename: 'small.pdf',
maxEncodedBytes: 1024,
})
assert.equal(typeof pdf.base64, 'string')
assert.equal(pdf.base64, PDF_BASE64)
assert.equal(pdf.filename, 'small.pdf')
assert.equal(pdf.contentType, 'application/pdf')
assert.equal(pdf.filesize, PDF_BYTES.byteLength)
assert.equal(pdf.url, SMALL_PDF_URL)
assert.equal('bytes' in pdf, false)
assert.equal(pdf.base64 instanceof Uint8Array, false)
assert.ok(!pdf.url.startsWith(STORAGE))
assert.equal(
fixture.requests.some((row) => row.url === BIG_PDF_URL || row.url.includes('big.pdf')),
false,
)
})
test('downloadPdf returns one file as base64 by index', async () => {
stubSmallPdf()
const pdf = await downloadPdf({
content: TWO_PDFS,
index: 1,
maxEncodedBytes: 1024,
})
assert.equal(pdf.filename, 'small.pdf')
assert.equal(pdf.base64, PDF_BASE64)
assert.equal(typeof pdf.base64, 'string')
assert.equal('bytes' in pdf, false)
assert.equal(pdf.url, SMALL_PDF_URL)
})
test('downloadPdf refuses over budget, names the full envelope size, and does not return a truncated body', async () => {
const required = smallEnvelope()
const budget = required - 1
await assert.rejects(
() => downloadPdf({
content: TWO_PDFS,
filename: 'small.pdf',
maxEncodedBytes: budget,
}),
(error) => {
assert.match(error.message, new RegExp(`encoded size is ${required}`))
assert.match(error.message, new RegExp(`Raise responseLimit to at least ${required}`))
assert.match(error.message, /small\.pdf/)
assert.notEqual(required, PDF_BASE64.length)
return true
},
)
assertNoPdfDownload()
})
test('downloadPdf hard-refuses when declared raw size is over 24 MiB', async () => {
await assert.rejects(
() => downloadPdf({
content: OVER_CAP,
filename: 'huge.pdf',
maxEncodedBytes: 40 * 1024 * 1024,
}),
(error) => {
assert.match(error.message, new RegExp(`raw size is ${OVER_RAW}`))
assert.match(error.message, /24 MiB/)
assert.match(error.message, new RegExp(String(MAX_RAW_BYTES)))
assert.match(error.message, /huge\.pdf/)
return true
},
)
assertNoPdfDownload()
})
test('downloadPdf envelope size equal to maxEncodedBytes succeeds, one byte over refuses', async () => {
stubSmallPdf()
const required = smallEnvelope()
const pdf = await downloadPdf({
content: TWO_PDFS,
filename: 'small.pdf',
maxEncodedBytes: required,
})
assert.equal(JSON.stringify(pdf).length, required)
assert.equal(pdf.base64, PDF_BASE64)
fixture.requests.length = 0
stubSmallPdf()
await assert.rejects(
() => downloadPdf({
content: TWO_PDFS,
filename: 'small.pdf',
maxEncodedBytes: required - 1,
}),
(error) => {
assert.match(error.message, new RegExp(`Raise responseLimit to at least ${required}`))
return true
},
)
})
test('downloadPdf allows a declared raw size of exactly 24 MiB and refuses one byte over', async () => {
await assert.rejects(
() => downloadPdf({
content: EXACT_CAP,
filename: 'exact.pdf',
maxEncodedBytes: 1,
}),
(error) => {
assert.match(error.message, /encoded size is/)
assert.match(error.message, /Raise responseLimit/)
assert.doesNotMatch(error.message, /24 MiB/)
return true
},
)
assertNoPdfDownload()
await assert.rejects(
() => downloadPdf({
content: OVER_CAP,
filename: 'huge.pdf',
maxEncodedBytes: 40 * 1024 * 1024,
}),
/24 MiB/,
)
})
test('downloadPdf refuses after download when filesize is missing, for budget and 24 MiB', async () => {
stubPdf(RAILS_PDF_URL)
const required = railsEnvelope()
await assert.rejects(
() => downloadPdf({
content: RAILS_ONLY,
filename: 'rails.pdf',
maxEncodedBytes: required - 1,
}),
(error) => {
assert.match(error.message, new RegExp(`encoded size is ${required}`))
assert.match(error.message, new RegExp(`Raise responseLimit to at least ${required}`))
return true
},
)
assert.equal(fixture.requests.some((row) => row.url === RAILS_PDF_URL), true)
fixture.requests.length = 0
const overState = stubOversizePdf(RAILS_PDF_URL)
await assert.rejects(
() => downloadPdf({
content: RAILS_ONLY,
filename: 'rails.pdf',
maxEncodedBytes: 40 * 1024 * 1024,
}),
(error) => {
assert.match(error.message, /24 MiB/)
const n = Number(error.message.match(/raw size is (\d+)/)?.[1])
assert.ok(n > MAX_RAW_BYTES)
return true
},
)
assert.ok(overState.pulled > 0)
assert.ok(overState.pulled <= MAX_RAW_BYTES + 1024 * 1024)
})
test('downloadPdf refuses after download when filesize is understated, for budget and 24 MiB', async () => {
stubSmallPdf()
const required = smallEnvelope()
await assert.rejects(
() => downloadPdf({
content: UNDERSTATED,
filename: 'small.pdf',
maxEncodedBytes: required - 1,
}),
(error) => {
assert.match(error.message, new RegExp(`encoded size is ${required}`))
assert.match(error.message, new RegExp(`Raise responseLimit to at least ${required}`))
return true
},
)
assert.equal(fixture.requests.some((row) => row.url === SMALL_PDF_URL), true)
fixture.requests.length = 0
const overState = stubOversizePdf(SMALL_PDF_URL, { contentLength: OVER_RAW })
await assert.rejects(
() => downloadPdf({
content: UNDERSTATED,
filename: 'small.pdf',
maxEncodedBytes: 40 * 1024 * 1024,
}),
(error) => {
assert.match(error.message, new RegExp(`raw size is ${OVER_RAW}`))
assert.match(error.message, /24 MiB/)
return true
},
)
assert.ok(overState.pulled === 0 || overState.pulled <= MAX_RAW_BYTES + 65536)
})
test('downloadPdf validates filename or index before any HEY request', async () => {
on('GET', `${HEY}/messages/1.json`, () => {
throw new Error('GET /messages should not run')
})
await assert.rejects(
() => downloadPdf({ messageId: 1, maxEncodedBytes: 10 }),
/filename or index is required/,
)
assert.equal(fixture.requests.length, 0)
await assert.rejects(
() => downloadPdf({ messageId: 1, index: '0', maxEncodedBytes: 10 }),
/index must be a non-negative integer/,
)
await assert.rejects(
() => downloadPdf({ messageId: 1, index: 1.5, maxEncodedBytes: 10 }),
/index must be a non-negative integer/,
)
assert.equal(fixture.requests.length, 0)
})
test('downloadPdf throws when two PDFs share a filename and tells the caller to use index', async () => {
stubPdf(INVOICE_A_URL)
await assert.rejects(
() => downloadPdf({
content: DUP_PDFS,
filename: 'invoice.pdf',
maxEncodedBytes: 1024,
}),
(error) => {
assert.match(error.message, /multiple PDFs named invoice\.pdf/)
assert.match(error.message, /pass index/)
return true
},
)
assertNoPdfDownload()
const pdf = await downloadPdf({
content: DUP_PDFS,
index: 0,
maxEncodedBytes: 1024,
})
assert.equal(pdf.filename, 'invoice.pdf')
assert.equal(pdf.url, INVOICE_A_URL)
assert.equal(pdf.base64, PDF_BASE64)
})
test('attachments default export is listPdfs and listAndDownloadPdfs is absent', async () => {
const mod = await import('../src/attachments.js')
assert.equal(mod.default, mod.listPdfs)
assert.equal('listAndDownloadPdfs' in mod, false)
assert.equal(typeof mod.listPdfs, 'function')
assert.equal(typeof mod.downloadPdf, 'function')
assert.deepEqual(Object.keys(mod).sort(), [
'default',
'downloadPdf',
'getMessage',
'listPdfs',
'parseActionTextAttachments',
'parseRailsPdfUrls',
'parseTrixAttachments',
])
})