Files

343 lines
17 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { once } from 'node:events';
import { validateBaseUrl, validateGeneration, listModels, generateImages, validateEditImage, editImages } from '../lib/engine.mjs';
const apiKey = 'secret-do-not-echo';
const input = { model: 'explicit-third-party-model', prompt: 'a blue bird' };
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=', 'base64');
const json = (res, value) => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(value)); };
async function server(t, handler) {
const failures = [];
const instance = http.createServer((req, res) => {
Promise.resolve().then(() => handler(req, res)).catch(error => { failures.push(error); res.destroy(); });
});
instance.listen(0, '127.0.0.1');
await once(instance, 'listening');
t.after(async () => {
const closed = new Promise(resolve => instance.close(resolve));
instance.closeAllConnections();
await closed;
if (failures.length) throw failures[0];
});
return `http://127.0.0.1:${instance.address().port}`;
}
async function requestJson(req) {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
return JSON.parse(Buffer.concat(chunks).toString());
}
test('edit image validation enforces bytes, signatures, upload limit and owned copies', () => {
const padded = Buffer.concat([Buffer.from('prefix'), png, Buffer.from('suffix')]);
const view = new Uint8Array(padded.buffer, padded.byteOffset + 6, png.length);
const validated = validateEditImage(view);
assert.equal(validated.mediaType, 'image/png');
assert.ok(Buffer.isBuffer(validated.data));
assert.deepEqual(validated.data, png);
view.fill(0);
assert.deepEqual(validated.data, png);
for (const data of [undefined, null, 'image.png', [], new ArrayBuffer(8), new Uint16Array(8), Buffer.alloc(0), Buffer.from('GIF89a'), Buffer.from('<svg/>')]) {
assert.throws(() => validateEditImage(data), /PNG, JPEG, or WebP/);
}
const boundary = Buffer.alloc(20 * 1024 * 1024 + 1);
png.copy(boundary);
assert.throws(() => validateEditImage(boundary), /20 MiB/);
assert.equal(validateEditImage(boundary.subarray(0, -1)).data.length, 20 * 1024 * 1024);
});
test('edits upload real multipart with singular image, correct filename/type and validated fields', async t => {
const fixtures = [
[png, 'image/png', 'png'],
[Buffer.from([255, 216, 255, 224, 0, 0]), 'image/jpeg', 'jpg'],
[Buffer.from('RIFF\x04\0\0\0WEBP', 'latin1'), 'image/webp', 'webp'],
];
let index = 0;
const specified = { ...input, prompt: '把背景换成海边', n: 2, size: 'auto', quality: 'high', background: 'opaque', output_format: 'webp' };
const base = await server(t, async (req, res) => {
const [bytes, type, extension] = fixtures[index++];
assert.equal(req.method, 'POST');
assert.equal(req.url, '/custom/v1/images/edits');
assert.equal(req.headers.authorization, `Bearer ${apiKey}`);
assert.match(req.headers['content-type'], /^multipart\/form-data; boundary=/);
const request = new Request('http://localhost', { method: 'POST', headers: req.headers, body: req, duplex: 'half' });
const form = await request.formData();
const expected = index === 1 ? specified : { ...input, n: 1 };
assert.deepEqual([...form.keys()].sort(), [...Object.keys(expected), 'image'].sort());
for (const [key, value] of Object.entries(expected)) assert.equal(form.get(key), String(value));
assert.equal(form.getAll('image').length, 1);
const file = form.get('image');
assert.equal(file.name, `image.${extension}`);
assert.equal(file.type, type);
assert.deepEqual(Buffer.from(await file.arrayBuffer()), bytes);
json(res, { data: [{ b64_json: png.toString('base64'), revised_prompt: 'edited bird' }] });
});
for (const [i, [image]] of fixtures.entries()) {
const options = i === 0 ? specified : input;
const result = await editImages({ baseUrl: `${base}/custom/v1/`, apiKey, input: { ...options, ignored: true }, image });
assert.deepEqual(result, { model: options.model, prompt: options.prompt, images: [{ data: png, mediaType: 'image/png', revisedPrompt: 'edited bird' }] });
}
assert.equal(index, 3);
});
test('edits reject invalid images and options before any API request', async t => {
let count = 0;
const baseUrl = await server(t, (req, res) => { count++; json(res, { data: [] }); });
for (const image of [Buffer.alloc(0), Buffer.from('GIF89a'), Buffer.alloc(20 * 1024 * 1024 + 1)]) {
await assert.rejects(editImages({ baseUrl, apiKey, input, image }));
}
for (const change of [{ model: '' }, { prompt: '' }, { n: 5 }, { quality: 'invalid' }]) {
await assert.rejects(editImages({ baseUrl, apiKey, input: { ...input, ...change }, image: png }));
}
assert.equal(count, 0);
});
test('base URL normalization preserves API prefix and rejects unsafe parts', () => {
assert.equal(validateBaseUrl(' HTTPS://EXAMPLE.COM/v1/// '), 'https://example.com/v1');
for (const value of ['', null, '/v1', 'ftp://example.com', 'https://a:b@example.com', 'https://example.com?x=1', 'https://example.com#x', 'https://example.com?', 'https://example.com#']) {
assert.throws(() => validateBaseUrl(value));
}
});
test('generation requires explicit fields and sends only specified supported options', () => {
assert.deepEqual(validateGeneration({ ...input, ignored: true }), { ...input, n: 1 });
const options = { ...input, n: 4, size: '1536x1024', quality: 'high', background: 'transparent', output_format: 'webp' };
assert.deepEqual(validateGeneration(options), options);
assert.equal(validateGeneration({ ...input, size: 'auto' }).size, 'auto');
for (const change of [{ model: undefined }, { model: ' ' }, { model: 'x'.repeat(201) }, { prompt: '' }, { prompt: 'x'.repeat(32001) }, { n: 0 }, { n: 5 }, { n: 1.5 }, { n: '1' }, { n: null }, { size: '1024X1024' }, { quality: 'best' }, { background: null }, { output_format: 'gif' }]) {
assert.throws(() => validateGeneration({ ...input, ...change }));
}
assert.throws(() => validateGeneration(null));
assert.equal(validateGeneration({ model: 'm'.repeat(200), prompt: 'p'.repeat(32000) }).n, 1);
});
test('listModels uses exact API prefix, GET and bearer authentication', async t => {
const base = await server(t, (req, res) => {
assert.equal(req.method, 'GET');
assert.equal(req.url, '/custom/v1/models');
assert.equal(req.headers.authorization, `Bearer ${apiKey}`);
json(res, { data: [{ id: 'model-b' }, { id: 'model-a' }] });
});
assert.deepEqual(await listModels({ baseUrl: `${base}/custom/v1///`, apiKey }), ['model-b', 'model-a']);
});
test('generation posts explicit model, omits optional defaults and decodes base64', async t => {
let requests = 0;
const baseUrl = await server(t, async (req, res) => {
requests++;
assert.equal(req.url, '/images/generations');
assert.equal(req.method, 'POST');
assert.equal(req.headers.authorization, `Bearer ${apiKey}`);
assert.equal(req.headers['content-type'], 'application/json');
assert.deepEqual(await requestJson(req), { ...input, n: 1 });
json(res, { model: 'provider-model', data: [{ b64_json: png.toString('base64'), revised_prompt: 'revised bird' }] });
});
const result = await generateImages({ baseUrl, apiKey, input });
assert.deepEqual(result, { ...input, images: [{ data: png, mediaType: 'image/png', revisedPrompt: 'revised bird' }] });
assert.equal(requests, 1);
});
test('URL images never receive bearer auth and bytes determine media type', async t => {
let downloads = 0;
const imageBase = await server(t, (req, res) => {
downloads++;
assert.equal(req.url, '/asset?signature=public-token');
assert.equal(req.headers.authorization, undefined);
res.setHeader('Content-Type', 'text/html');
res.end(png);
});
const baseUrl = await server(t, (req, res) => json(res, { data: [{ url: `${imageBase}/asset?signature=public-token` }] }));
const result = await generateImages({ baseUrl, apiKey, input });
assert.deepEqual(result.images, [{ data: png, mediaType: 'image/png' }]);
assert.equal(downloads, 1);
});
test('recognizes JPEG, WebP and GIF signatures', async t => {
const fixtures = [Buffer.from([255, 216, 255, 224, 0, 0]), Buffer.from('RIFF\x04\0\0\0WEBP', 'latin1'), Buffer.from('GIF89a\x01\0\x01\0', 'latin1')];
const baseUrl = await server(t, (req, res) => json(res, { data: fixtures.map(data => ({ b64_json: data.toString('base64') })) }));
const result = await generateImages({ baseUrl, apiKey, input: { ...input, n: 3 } });
assert.deepEqual(result.images.map(image => image.mediaType), ['image/jpeg', 'image/webp', 'image/gif']);
});
test('HTTP errors expose status but never provider payload or key, with no retries', async t => {
let count = 0;
const baseUrl = await server(t, (req, res) => { count++; res.writeHead(429); res.end(`provider-private ${apiKey}`); });
await assert.rejects(generateImages({ baseUrl, apiKey, input }), error => {
assert.match(error.message, /HTTP 429/);
assert.doesNotMatch(error.message, /secret-do-not-echo|provider-private/);
assert.equal(error.cause, undefined);
return true;
});
assert.equal(count, 1);
});
test('API redirects are rejected without contacting redirect destination', async t => {
let redirected = 0;
const target = await server(t, (req, res) => { redirected++; json(res, { data: [] }); });
const baseUrl = await server(t, (req, res) => { res.writeHead(302, { Location: `${target}/models` }); res.end(); });
await assert.rejects(listModels({ baseUrl, apiKey }), /network request failed/);
assert.equal(redirected, 0);
});
test('malformed JSON and model lists are rejected without raw response echo', async t => {
let payload = 'private-provider-body';
const baseUrl = await server(t, (req, res) => res.end(payload));
await assert.rejects(listModels({ baseUrl, apiKey }), /malformed JSON/);
for (const value of [null, {}, { data: [null] }, { data: [{ id: 4 }] }, { data: [{ id: ' ' }] }]) {
payload = JSON.stringify(value);
await assert.rejects(listModels({ baseUrl, apiKey }), /malformed model list/);
}
});
test('invalid base64, invalid bytes and malformed image responses fail', async t => {
let payload;
const baseUrl = await server(t, (req, res) => json(res, payload));
for (const value of [null, {}, { data: [] }, { data: [null] }, { data: [{}] }, { data: [{ revised_prompt: 1, b64_json: png.toString('base64') }] }]) {
payload = value;
await assert.rejects(generateImages({ baseUrl, apiKey, input }));
}
for (const value of ['', '!!!!', 'aGk', 'aGk=\n', 'Zh==', 123, null]) {
payload = { data: [{ b64_json: value }] };
await assert.rejects(generateImages({ baseUrl, apiKey, input }), /invalid base64/);
}
payload = { data: [{ b64_json: Buffer.from('<html>not an image</html>').toString('base64') }] };
await assert.rejects(generateImages({ baseUrl, apiKey, input }), /not a supported/);
for (const url of ['file:///secret', 'http://user:password@localhost/image', '/relative']) {
payload = { data: [{ url }] };
await assert.rejects(generateImages({ baseUrl, apiKey, input }), /invalid image URL/);
}
});
test('response Content-Length above 64 MB is rejected before buffering', async t => {
const baseUrl = await server(t, (req, res) => { res.writeHead(200, { 'Content-Length': 64 * 1024 * 1024 + 1 }); res.flushHeaders(); });
await assert.rejects(listModels({ baseUrl, apiKey }), /64 MB response limit/);
});
test('streamed responses enforce 64 MB even without Content-Length', async t => {
const block = Buffer.alloc(1024 * 1024, 32);
const baseUrl = await server(t, async (req, res) => {
for (let i = 0; i < 65; i++) {
if (res.destroyed) return;
if (!res.write(block)) {
await new Promise(resolve => {
const done = () => { res.off('drain', done); res.off('close', done); resolve(); };
res.once('drain', done);
res.once('close', done);
});
}
}
res.end();
});
await assert.rejects(listModels({ baseUrl, apiKey }), /64 MB response limit/);
});
test('caller abort interrupts pending API request and sanitizes abort reason', async t => {
const controller = new AbortController();
const baseUrl = await server(t, () => controller.abort(new Error(apiKey)));
await assert.rejects(listModels({ baseUrl, apiKey, signal: controller.signal }), /^Error: Request aborted\.$/);
});
test('caller abort also interrupts image download', async t => {
const controller = new AbortController();
const imageBase = await server(t, (req, res) => { res.writeHead(200); res.write(png.subarray(0, 4)); controller.abort(); });
const baseUrl = await server(t, (req, res) => json(res, { data: [{ url: `${imageBase}/slow` }] }));
await assert.rejects(generateImages({ baseUrl, apiKey, input, signal: controller.signal }), /Request aborted/);
});
test('specified generation options are preserved on the wire', async t => {
const specified = { ...input, n: 2, size: 'auto', quality: 'hd', background: 'opaque', output_format: 'jpeg' };
const baseUrl = await server(t, async (req, res) => {
assert.deepEqual(await requestJson(req), specified);
json(res, { data: [{ b64_json: png.toString('base64') }] });
});
await generateImages({ baseUrl, apiKey, input: specified });
});
test('900 second timeout aborts a stalled request', async t => {
const baseUrl = await server(t, () => {});
const original = globalThis.setTimeout;
let configuredTimeout;
t.mock.method(globalThis, 'setTimeout', (callback, milliseconds, ...args) => {
if (milliseconds === 900_000) {
configuredTimeout = milliseconds;
return original(callback, 20, ...args);
}
return original(callback, milliseconds, ...args);
});
await assert.rejects(listModels({ baseUrl, apiKey }), /timed out after 900 seconds/);
assert.equal(configuredTimeout, 900_000);
});
test('edit response downloads omit auth and retain output validation and bounds', async t => {
const imageBase = await server(t, (req, res) => {
assert.equal(req.headers.authorization, undefined);
res.end(png);
});
let payload = { data: [{ url: `${imageBase}/image` }] };
let oversized = false;
const baseUrl = await server(t, (req, res) => {
if (oversized) { res.writeHead(200, { 'Content-Length': 64 * 1024 * 1024 + 1 }); res.flushHeaders(); }
else json(res, payload);
});
const edit = () => editImages({ baseUrl, apiKey, input, image: png });
assert.deepEqual((await edit()).images, [{ data: png, mediaType: 'image/png' }]);
payload = { data: [{ b64_json: '!!!!' }] };
await assert.rejects(edit(), /invalid base64/);
payload = { data: [] };
await assert.rejects(edit(), /malformed image response/);
oversized = true;
await assert.rejects(edit(), /64 MB response limit/);
});
test('edit API failure is sanitized and never retried', async t => {
let count = 0;
const baseUrl = await server(t, (req, res) => { count++; res.writeHead(503); res.end(`provider-private ${apiKey}`); });
await assert.rejects(editImages({ baseUrl, apiKey, input, image: png }), error => {
assert.equal(error.message, 'API request failed with HTTP 503.');
assert.equal(error.cause, undefined);
return true;
});
assert.equal(count, 1);
});
test('edits cancel pending API requests, downloads and pre-aborted requests', async t => {
let count = 0;
let controller = new AbortController();
let download = false;
const imageBase = await server(t, (req, res) => {
assert.equal(req.headers.authorization, undefined);
res.write(png.subarray(0, 4));
controller.abort(new Error(apiKey));
});
const baseUrl = await server(t, (req, res) => {
count++;
if (download) json(res, { data: [{ url: `${imageBase}/slow` }] });
else controller.abort(new Error(apiKey));
});
const edit = signal => editImages({ baseUrl, apiKey, input, image: png, signal });
await assert.rejects(edit(AbortSignal.abort(apiKey)), /^Error: Request aborted\.$/);
assert.equal(count, 0);
await assert.rejects(edit(controller.signal), /^Error: Request aborted\.$/);
controller = new AbortController();
download = true;
await assert.rejects(edit(controller.signal), /^Error: Request aborted\.$/);
assert.equal(count, 2);
});
test('edits share the 900 second operation timeout', async t => {
const baseUrl = await server(t, () => {});
const original = globalThis.setTimeout;
t.mock.method(globalThis, 'setTimeout', (callback, milliseconds, ...args) => original(callback, milliseconds === 900_000 ? 20 : milliseconds, ...args));
await assert.rejects(editImages({ baseUrl, apiKey, input, image: png }), /timed out after 900 seconds/);
});
test('pre-aborted signal does not send an API request', async t => {
let count = 0;
const baseUrl = await server(t, (req, res) => { count++; json(res, { data: [] }); });
await assert.rejects(listModels({ baseUrl, apiKey, signal: AbortSignal.abort(apiKey) }), /Request aborted/);
assert.equal(count, 0);
});