import assert from 'node:assert/strict'; import test from 'node:test'; import * as http from 'node:http'; import type { AddressInfo } from 'node:net'; import { animetoshoFetchJson } from './utils.js'; interface TestServer { baseUrl: string; close: () => Promise; } function startServer(handler: http.RequestListener): Promise { return new Promise((resolve) => { const server = http.createServer(handler); server.listen(0, '127.0.0.1', () => { const { port } = server.address() as AddressInfo; resolve({ baseUrl: `http://127.0.0.1:${port}`, close: () => new Promise((done) => { server.closeAllConnections?.(); server.close(() => done()); }), }); }); }); } test('animetoshoFetchJson gives up on a hanging response', async () => { const server = await startServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); // Never end the response. res.write('['); }); try { const result = await animetoshoFetchJson( '/json', {}, { baseUrl: server.baseUrl, timeoutMs: 150 }, ); assert.equal(result.ok, false); if (!result.ok) { assert.match(result.error.error, /timed out/i); } } finally { await server.close(); } }); test('animetoshoFetchJson rejects an oversized response', async () => { const server = await startServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(Array.from({ length: 2000 }, (_, index) => ({ id: index })))); }); try { const result = await animetoshoFetchJson( '/json', {}, { baseUrl: server.baseUrl, maxResponseBytes: 512 }, ); assert.equal(result.ok, false); if (!result.ok) { assert.match(result.error.error, /too large/i); } } finally { await server.close(); } }); test('animetoshoFetchJson decodes multi-byte characters split across chunks', async () => { const title = '葬送のフリーレン'; const body = Buffer.from(JSON.stringify([{ id: 1, title }]), 'utf8'); // Split inside the first Japanese character's 3-byte UTF-8 sequence. const splitAt = body.indexOf(Buffer.from(title, 'utf8')) + 1; const server = await startServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.write(body.subarray(0, splitAt)); setTimeout(() => res.end(body.subarray(splitAt)), 10); }); try { const result = await animetoshoFetchJson>( '/json', {}, { baseUrl: server.baseUrl }, ); assert.equal(result.ok, true); if (result.ok) { assert.equal(result.data[0]!.title, title); } } finally { await server.close(); } }); test('animetoshoFetchJson still parses a normal response', async () => { const server = await startServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify([{ id: 1, title: 'ok' }])); }); try { const result = await animetoshoFetchJson>( '/json', { q: 'x' }, { baseUrl: server.baseUrl }, ); assert.equal(result.ok, true); if (result.ok) { assert.deepEqual(result.data, [{ id: 1, title: 'ok' }]); } } finally { await server.close(); } });