// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import { BINARY_CAPTURE_SIZE_CAP, getResponseBody } from '../http-utils'; type XHRStubOptions = { responseType?: XMLHttpRequestResponseType; responseText?: string; response?: unknown; contentType?: string; }; const makeXHRStub = ({ responseType = '', responseText = '', response = null, contentType = '', }: XHRStubOptions): XMLHttpRequest => ({ responseType, responseText, response, getResponseHeader: (name: string) => name.toLowerCase() === 'content-type' ? contentType : null, }) as unknown as XMLHttpRequest; describe('getResponseBody', () => { it('returns plain text when responseType is empty', async () => { const xhr = makeXHRStub({ responseType: '', responseText: 'hello world', }); expect(await getResponseBody(xhr)).toBe('hello world'); }); it('returns plain text when responseType is "text"', async () => { const xhr = makeXHRStub({ responseType: 'text', responseText: 'hello world', }); expect(await getResponseBody(xhr)).toBe('hello world'); }); it('stringifies the JSON response when responseType is "json"', async () => { const xhr = makeXHRStub({ responseType: 'json', response: { ok: true, n: 1 }, }); expect(await getResponseBody(xhr)).toBe('{"ok":true,"n":1}'); }); it('reads a text blob as text', async () => { const blob = new Blob(['

hello

'], { type: 'text/html' }); const xhr = makeXHRStub({ responseType: 'blob', response: blob, contentType: 'text/html; charset=utf-8', }); expect(await getResponseBody(xhr)).toBe('

hello

'); }); it('reads a JSON blob as text', async () => { const blob = new Blob(['{"ok":true}'], { type: 'application/json' }); const xhr = makeXHRStub({ responseType: 'blob', response: blob, contentType: 'application/json', }); expect(await getResponseBody(xhr)).toBe('{"ok":true}'); }); it('routes image/svg+xml through the text path so the source is preserved', async () => { const svg = ''; const blob = new Blob([svg], { type: 'image/svg+xml' }); const xhr = makeXHRStub({ responseType: 'blob', response: blob, contentType: 'image/svg+xml', }); expect(await getResponseBody(xhr)).toBe(svg); }); it('reads application/xml as text', async () => { const xml = 'Demo'; const blob = new Blob([xml], { type: 'application/xml' }); const xhr = makeXHRStub({ responseType: 'blob', response: blob, contentType: 'application/xml', }); expect(await getResponseBody(xhr)).toBe(xml); }); it('reads text/xml as text', async () => { const xml = ''; const blob = new Blob([xml], { type: 'text/xml' }); const xhr = makeXHRStub({ responseType: 'blob', response: blob, contentType: 'text/xml; charset=utf-8', }); expect(await getResponseBody(xhr)).toBe(xml); }); it('reads RFC 7303 +xml composite types (Atom, RSS, SOAP) as text', async () => { const atom = 'x'; const xhr = makeXHRStub({ responseType: 'blob', response: new Blob([atom], { type: 'application/atom+xml' }), contentType: 'application/atom+xml; charset=utf-8', }); expect(await getResponseBody(xhr)).toBe(atom); }); it('returns a binary union variant with base64 for an image blob under the cap', async () => { // Three bytes (0x01 0x02 0x03) base64-encodes to "AQID". const blob = new Blob([new Uint8Array([1, 2, 3])], { type: 'image/png' }); const xhr = makeXHRStub({ responseType: 'blob', response: blob, contentType: 'image/png', }); const result = await getResponseBody(xhr); expect(result).toEqual({ kind: 'binary', base64: 'AQID' }); }); it('returns a binary-too-large variant without shipping bytes when the blob exceeds the cap', async () => { // Stub a blob whose .size lies — we only care about the size-check // path here, no FileReader.readAsDataURL should be invoked. const oversizedBlob = { size: BINARY_CAPTURE_SIZE_CAP + 1, type: 'image/jpeg', } as unknown as Blob; const xhr = makeXHRStub({ responseType: 'blob', response: oversizedBlob, contentType: 'image/jpeg', }); expect(await getResponseBody(xhr)).toEqual({ kind: 'binary-too-large', size: BINARY_CAPTURE_SIZE_CAP + 1, }); }); it('returns a binary union variant for non-image, non-text blob content-types', async () => { const blob = new Blob([new Uint8Array([1, 2, 3])], { type: 'application/pdf', }); const xhr = makeXHRStub({ responseType: 'blob', response: blob, contentType: 'application/pdf', }); expect(await getResponseBody(xhr)).toEqual({ kind: 'binary', base64: 'AQID', }); }); it('captures arraybuffer responses as binary', async () => { const buffer = new Uint8Array([1, 2, 3]).buffer; const xhr = makeXHRStub({ responseType: 'arraybuffer', response: buffer, }); expect(await getResponseBody(xhr)).toEqual({ kind: 'binary', base64: 'AQID', }); }); it('chunks large arraybuffer responses without exhausting fromCharCode', async () => { // 100 KB of bytes — past the 32 KB chunk boundary used by the // base64 encoder, well under the 5 MB cap. Confirms the chunked // path produces correct base64 output. const size = 100 * 1024; const bytes = new Uint8Array(size); for (let i = 0; i < size; i++) { bytes[i] = i & 0xff; } const xhr = makeXHRStub({ responseType: 'arraybuffer', response: bytes.buffer, }); const result = await getResponseBody(xhr); expect(typeof result === 'object' && result?.kind === 'binary').toBe(true); if (typeof result === 'object' && result?.kind === 'binary') { // Round-trip: decode the base64 back and compare with the input. const binary = atob(result.base64); const decoded = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { decoded[i] = binary.charCodeAt(i); } expect(decoded.length).toBe(size); expect(decoded[0]).toBe(0); expect(decoded[size - 1]).toBe((size - 1) & 0xff); } }); it('short-circuits arraybuffer responses above the size cap', async () => { // Stub a buffer whose byteLength lies — the cap check should fire // before any encoding work happens. const oversized = { byteLength: BINARY_CAPTURE_SIZE_CAP + 1, } as unknown as ArrayBuffer; const xhr = makeXHRStub({ responseType: 'arraybuffer', response: oversized, }); expect(await getResponseBody(xhr)).toEqual({ kind: 'binary-too-large', size: BINARY_CAPTURE_SIZE_CAP + 1, }); }); it('returns null for an arraybuffer responseType with no payload', async () => { const xhr = makeXHRStub({ responseType: 'arraybuffer' }); expect(await getResponseBody(xhr)).toBeNull(); }); it('returns null for an arraybuffer responseType with an empty buffer', async () => { const xhr = makeXHRStub({ responseType: 'arraybuffer', response: new ArrayBuffer(0), }); expect(await getResponseBody(xhr)).toBeNull(); }); });