2026-04-17 15:09:31 +02:00
|
|
|
import { describe, it, expect, afterEach, beforeEach } from 'vitest';
|
|
|
|
|
import { createServer, type Server } from 'node:http';
|
|
|
|
|
import type { AddressInfo } from 'node:net';
|
|
|
|
|
import { fetchText, fetchBuffer } from '../../src/lib/server/http';
|
|
|
|
|
|
|
|
|
|
let server: Server;
|
|
|
|
|
let baseUrl: string;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
server = createServer();
|
|
|
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
|
|
|
const addr = server.address() as AddressInfo;
|
|
|
|
|
baseUrl = `http://127.0.0.1:${addr.port}`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('fetchText', () => {
|
|
|
|
|
it('returns body text', async () => {
|
|
|
|
|
server.on('request', (_req, res) => {
|
|
|
|
|
res.writeHead(200, { 'content-type': 'text/html' });
|
|
|
|
|
res.end('<html>hi</html>');
|
|
|
|
|
});
|
|
|
|
|
expect(await fetchText(`${baseUrl}/`)).toBe('<html>hi</html>');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects non-http schemes', async () => {
|
|
|
|
|
await expect(fetchText('file:///etc/passwd')).rejects.toThrow(/scheme/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('enforces max bytes', async () => {
|
|
|
|
|
const big = 'x'.repeat(200);
|
|
|
|
|
server.on('request', (_req, res) => {
|
|
|
|
|
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
|
|
|
res.end(big);
|
|
|
|
|
});
|
|
|
|
|
await expect(fetchText(`${baseUrl}/`, { maxBytes: 100 })).rejects.toThrow(/exceeds/i);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('times out slow responses', async () => {
|
|
|
|
|
server.on('request', () => {
|
|
|
|
|
// never respond
|
|
|
|
|
});
|
|
|
|
|
await expect(fetchText(`${baseUrl}/`, { timeoutMs: 150 })).rejects.toThrow();
|
|
|
|
|
});
|
2026-04-17 22:33:55 +02:00
|
|
|
|
|
|
|
|
it('allowTruncate returns first maxBytes instead of throwing', async () => {
|
|
|
|
|
const head = '<html><head><title>hi</title></head>';
|
|
|
|
|
const filler = 'x'.repeat(2000);
|
|
|
|
|
server.on('request', (_req, res) => {
|
|
|
|
|
res.writeHead(200, { 'content-type': 'text/html' });
|
|
|
|
|
res.end(head + filler);
|
|
|
|
|
});
|
|
|
|
|
const text = await fetchText(`${baseUrl}/`, { maxBytes: 100, allowTruncate: true });
|
|
|
|
|
// First 100 bytes of body — should contain the <head> opening at least
|
|
|
|
|
expect(text.length).toBeLessThanOrEqual(2048); // chunk boundary may overshoot exact bytes slightly
|
|
|
|
|
expect(text).toContain('<html>');
|
|
|
|
|
expect(text).toContain('<head>');
|
|
|
|
|
});
|
2026-04-17 15:09:31 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('fetchBuffer', () => {
|
|
|
|
|
it('returns bytes + content-type', async () => {
|
|
|
|
|
server.on('request', (_req, res) => {
|
|
|
|
|
res.writeHead(200, { 'content-type': 'image/png' });
|
|
|
|
|
res.end(Buffer.from([1, 2, 3, 4]));
|
|
|
|
|
});
|
|
|
|
|
const { data, contentType } = await fetchBuffer(`${baseUrl}/`);
|
|
|
|
|
expect(Array.from(data)).toEqual([1, 2, 3, 4]);
|
|
|
|
|
expect(contentType).toBe('image/png');
|
|
|
|
|
});
|
|
|
|
|
});
|