import { createServer, request as httpRequest } from 'node:http'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { handleCodingAttachmentRoutes } from '../../electron/api/routes/coding-attachments'; import { CodingAttachmentStore } from '../../electron/coding-projects/attachment-store'; import type { HostApiContext } from '../../electron/api/context'; const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); async function startAttachmentServer(maxBytes = 16 * 1024 * 1024) { const root = await mkdtemp(path.join(tmpdir(), 'makelore-coding-attachments-')); roots.push(root); const attachments = new CodingAttachmentStore(root, { createId: () => 'attachment-1', maxBytes, }); const context = { codingProducts: { attachments } } as unknown as HostApiContext; const server = createServer((request, response) => { const url = new URL(request.url ?? '/', 'http://127.0.0.1'); void handleCodingAttachmentRoutes(request, response, url, context).then((handled) => { if (!handled) { response.statusCode = 404; response.end(); } }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); if (!address || typeof address === 'string') throw new Error('Attachment test server failed'); return { baseUrl: `http://127.0.0.1:${address.port}`, close: async () => await new Promise((resolve, reject) => { server.close((error) => error ? reject(error) : resolve()); }), }; } async function postDeclaredLength( url: string, contentLength: number, ): Promise<{ status: number; body: Record }> { return await new Promise((resolve, reject) => { const request = httpRequest(url, { method: 'POST', headers: { 'Content-Type': 'image/png', 'Content-Length': String(contentLength), }, }, (response) => { const chunks: Buffer[] = []; response.on('data', (chunk) => chunks.push(Buffer.from(chunk))); response.on('end', () => resolve({ status: response.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record, })); }); request.on('error', reject); request.end(); }); } describe('coding attachment routes', () => { it('round-trips bounded image bytes through attachment ids', async () => { const server = await startAttachmentServer(); try { const bytes = new Uint8Array([137, 80, 78, 71, 1, 2, 3]); const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, { method: 'POST', headers: { 'Content-Type': 'image/png' }, body: bytes, }); expect(upload.status).toBe(201); await expect(upload.json()).resolves.toEqual({ attachmentId: 'attachment-1', mime: 'image/png', byteLength: bytes.byteLength, }); const preview = await fetch( `${server.baseUrl}/api/coding/attachments/attachment-1/content`, ); expect(preview.status).toBe(200); expect(preview.headers.get('content-type')).toBe('image/png'); expect(preview.headers.get('cache-control')).toBe('private, no-store'); expect(Array.from(new Uint8Array(await preview.arrayBuffer()))).toEqual(Array.from(bytes)); } finally { await server.close(); } }); it('rejects unsupported media before writing and maps missing content to 404', async () => { const server = await startAttachmentServer(); try { const upload = await fetch(`${server.baseUrl}/api/coding/attachments`, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: 'not an image', }); expect(upload.status).toBe(400); await expect(upload.json()).resolves.toMatchObject({ code: 'CODING_ATTACHMENT_INVALID', }); const missing = await fetch( `${server.baseUrl}/api/coding/attachments/missing/content`, ); expect(missing.status).toBe(404); await expect(missing.json()).resolves.toMatchObject({ code: 'CODING_ATTACHMENT_NOT_FOUND', }); } finally { await server.close(); } }); it('rejects a declared body larger than the route limit', async () => { const server = await startAttachmentServer(); try { const response = await postDeclaredLength( `${server.baseUrl}/api/coding/attachments`, 16 * 1024 * 1024 + 1, ); expect(response.status).toBe(413); expect(response.body).toMatchObject({ code: 'CODING_ATTACHMENT_TOO_LARGE', }); } finally { await server.close(); } }); });