import http from 'http'; import { AddressInfo } from 'net'; import { isRembgConfigured, removeBackground, SidecarRequestError } from '../../src/intake/rembgClient'; /** A real PNG header, so the client's own signature check sees what it expects. */ const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0]); const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0, 0, 0, 0, 0]); interface Capture { url: string; body: string; } /** * A stub sidecar on an ephemeral port. * * Port 0 rather than a fixed number: several ports in the 55000s are * Hyper-V-reserved on the development machine and bind with EACCES, and a * fixed port would also stop this suite running beside itself. * * A real HTTP server rather than a mocked `fetch`, because what is being * checked is the shape of the request that reaches the wire — above all that * `model=u2net` is in it. */ async function withStub( handler: (req: http.IncomingMessage, res: http.ServerResponse) => void, run: (capture: Capture) => Promise ): Promise { const capture: Capture = { url: '', body: '' }; const server = http.createServer((req, res) => { capture.url = req.url ?? ''; const chunks: Buffer[] = []; req.on('data', (c: Buffer) => chunks.push(c)); req.on('end', () => { capture.body = Buffer.concat(chunks).toString('latin1'); handler(req, res); }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const { port } = server.address() as AddressInfo; process.env.REMBG_URL = `http://127.0.0.1:${port}`; try { await run(capture); } finally { delete process.env.REMBG_URL; await new Promise((resolve) => server.close(() => resolve())); } } function respondWithPng(_req: http.IncomingMessage, res: http.ServerResponse): void { res.writeHead(200, { 'Content-Type': 'image/png' }); res.end(PNG); } describe('whether the sidecar is configured', () => { it('is false when REMBG_URL is unset', () => { delete process.env.REMBG_URL; expect(isRembgConfigured()).toBe(false); }); // A variable set to spaces is a configuration mistake, not a value — the // same reading envValidation applies everywhere else. it('is false when REMBG_URL is blank', () => { process.env.REMBG_URL = ' '; expect(isRembgConfigured()).toBe(false); delete process.env.REMBG_URL; }); it('is true when REMBG_URL is set', () => { process.env.REMBG_URL = 'http://rembg-syn:7000'; expect(isRembgConfigured()).toBe(true); delete process.env.REMBG_URL; }); }); describe('asking the sidecar to remove a background', () => { /** * The single most important assertion in this file. * * The image's default model is `bria-rmbg`, licensed non-commercial, and it * is selected by simply not naming a model. Nothing about the returned image * would reveal it had been used, so this is the only place it can be caught. */ it('names u2net explicitly, because the default is licensed non-commercial', async () => { await withStub(respondWithPng, async (capture) => { await removeBackground(JPEG, 'image/jpeg'); expect(capture.body).toContain('name="model"'); expect(capture.body).toContain('u2net'); }); }); it('posts the file to /api/remove and returns the PNG it gets back', async () => { await withStub(respondWithPng, async (capture) => { const out = await removeBackground(JPEG, 'image/jpeg'); expect(capture.url).toBe('/api/remove'); expect(capture.body).toContain('name="file"'); expect(out.subarray(0, 8)).toEqual(PNG.subarray(0, 8)); }); }); it('rejects rather than returning bytes when the sidecar errors', async () => { await withStub( (_req, res) => { res.writeHead(500); res.end('boom'); }, async () => { await expect(removeBackground(JPEG, 'image/jpeg')).rejects.toThrow(/500/); await expect(removeBackground(JPEG, 'image/jpeg')).rejects.toBeInstanceOf( SidecarRequestError ); } ); }); // The failure that would otherwise write an HTML error page over a // photograph. Checked with the same magic-byte helper the upload path uses, // rather than by trusting the Content-Type the sidecar sent. it('rejects a response that is not actually a PNG', async () => { await withStub( (_req, res) => { res.writeHead(200, { 'Content-Type': 'image/png' }); res.end('not an image'); }, async () => { await expect(removeBackground(JPEG, 'image/jpeg')).rejects.toThrow(/not a PNG/); await expect(removeBackground(JPEG, 'image/jpeg')).rejects.toBeInstanceOf( SidecarRequestError ); } ); }); // Deliberately not a SidecarRequestError (MINOR 3, #281 review): this // failure happens before any attempt to contact the sidecar, and a caller // has to be able to tell "never tried" apart from "tried and failed". it('rejects when it is not configured at all, without it being a sidecar failure', async () => { delete process.env.REMBG_URL; await expect(removeBackground(JPEG, 'image/jpeg')).rejects.toThrow(/REMBG_URL/); await expect(removeBackground(JPEG, 'image/jpeg')).rejects.not.toBeInstanceOf( SidecarRequestError ); }); });