import test from 'node:test'; import assert from 'node:assert/strict'; import { createBrowserFetch } from '../src/browser-http.mjs'; const endpoint = 'https://httpbin.org/get'; test('HTTP preserves bytes/status and omits implicit credentials', async () => { let request; const fetch = createBrowserFetch({ transport: async (url, init) => { request = { url, init }; return new Response(Uint8Array.of(0, 128, 255), { status: 201, headers: { 'x-test': 'ok' } }); } }); const result = await fetch(endpoint, { method: 'POST', body: 'hello', headers: { 'Content-Type': 'text/plain' } }); assert.deepEqual([...result.body], [0, 128, 255]); assert.equal(result.status, 201); assert.equal(result.headers['x-test'], 'ok'); assert.equal(request.init.credentials, 'omit'); assert.equal(request.init.redirect, 'manual'); assert.equal(request.init.body, 'hello'); assert.equal(request.init.mode, 'cors'); }); test('Any HTTPS host is accepted; protocol/method/URL credentials are checked before requests', async () => { let calls = 0; const fetch = createBrowserFetch({ transport: async () => { calls++; return new Response(''); } }); for (const url of ['http://httpbin.org/get', 'ftp://example.com/file', 'file:///etc/passwd', 'data:text/plain,hello', 'http://127.0.0.1/']) { await assert.rejects(fetch(url), /Only HTTPS/); } await assert.rejects(fetch('https://user:pass@httpbin.org/get'), /Credentials/); await assert.rejects(fetch(endpoint, { method: 'DELETE' }), /method DELETE/); assert.equal(calls, 0); for (const url of ['https://example.com/', 'https://gist.github.com/', 'https://gist.githubusercontent.com/', 'https://another-host.test:8443/data']) { assert.equal((await fetch(url)).status, 200); } assert.equal(calls, 4); }); test('Opaque redirects cannot appear as successful empty responses', async () => { const fetch = createBrowserFetch({ transport: async () => ({ type: 'opaqueredirect', status: 0 }) }); await assert.rejects(fetch(endpoint, { followRedirects: true }), /final HTTPS URL/); const opaque = createBrowserFetch({ transport: async () => ({ type: 'opaque', status: 0 }) }); await assert.rejects(opaque(endpoint), /unreadable/); }); test('CORS/network failures are clear; HTTP error status is preserved for curl -f', async () => { const fetch = createBrowserFetch({ transport: async () => { throw new TypeError('Failed to fetch'); } }); await assert.rejects(fetch(endpoint), /CORS.*connection failed/); const notFound = createBrowserFetch({ transport: async () => new Response('missing', { status: 404 }) }); assert.equal((await notFound(endpoint)).status, 404); }); test('Response cap works for content-length and streaming bodies', async () => { let cancelled = false; const stream = () => new ReadableStream({ start(c) { c.enqueue(new Uint8Array(11)); }, cancel() { cancelled = true; } }); const fetch = createBrowserFetch({ maxBytes: 10, transport: async () => new Response(stream()) }); await assert.rejects(fetch(endpoint), /10-byte limit/); assert(cancelled); const known = createBrowserFetch({ maxBytes: 10, transport: async () => new Response('abc', { headers: { 'Content-Length': '11' } }) }); await assert.rejects(known(endpoint), /10-byte limit/); const head = createBrowserFetch({ maxBytes: 10, transport: async () => new Response(null, { headers: { 'Content-Length': '1000000' } }) }); assert.equal((await head(endpoint, { method: 'HEAD' })).body.length, 0); }); test('Timeout and explicit cancellation propagate and abort transport', async () => { const transport = (_url, { signal }) => new Promise((_, reject) => { signal.throwIfAborted(); signal.addEventListener('abort', () => reject(signal.reason), { once: true }); }); const fetch = createBrowserFetch({ transport, timeoutMs: 20 }); await assert.rejects(fetch(endpoint), /timed out after 20 ms/); const controller = new AbortController(); const pending = fetch(endpoint, { signal: controller.signal }); controller.abort(); await assert.rejects(pending, { name: 'AbortError' }); });