import {describe, expect, test} from 'vitest'; import {RenderTimeoutError} from '../src/lib/render-errors.js'; import {buildServer} from '../src/server.js'; /** @type {import('../src/config.js').AppConfig} */ const config = { apiToken: undefined, host: '127.0.0.1', maxConcurrentRenders: 1, maxOptionChars: 40, maxOptions: 32, port: 0, requiresApiToken: false, renderTimeoutMs: 15000, }; const mp4 = Buffer.from('\0\0\0\x18ftypisom-test'); /** * @param {Partial} [overrides] * @param {Parameters[0]} [options] */ const build = (overrides = {}, options = {}) => buildServer({ config: {...config, ...overrides}, renderGacha: async () => ({buffer: mp4, byteLength: mp4.byteLength, durationMs: 9}), ...options, }); describe('POST /api/gacha', () => { test('returns mp4 bytes with rarity metadata', async () => { /** @type {unknown} */ let rendered; const app = await build( {}, { renderGacha: async (request) => { rendered = request; return {buffer: mp4, byteLength: mp4.byteLength, durationMs: 9}; }, }, ); const response = await app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 5}}); expect(response.statusCode).toBe(200); expect(response.headers['content-type']).toBe('video/mp4'); expect(response.headers['x-gacha-rarity']).toBe('5'); expect(response.rawPayload.subarray(4, 8).toString()).toBe('ftyp'); expect(rendered).toEqual({label: 'Pizza', rarity: 5, fps: 24, width: 640}); await app.close(); }); test('rejects an invalid rarity', async () => { const app = await build(); const response = await app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 6}}); expect(response.statusCode).toBe(400); expect(response.json().error).toBe('invalid_request'); await app.close(); }); test('rejects labels longer than the configured limit', async () => { const app = await build({maxOptionChars: 4}); const response = await app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 3}}); expect(response.statusCode).toBe(400); await app.close(); }); test('enforces bearer token when configured', async () => { const app = await build({apiToken: 'secret'}); const denied = await app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 3}}); const allowed = await app.inject({ method: 'POST', url: '/api/gacha', headers: {authorization: 'Bearer secret'}, payload: {label: 'Pizza', rarity: 3}, }); expect(denied.statusCode).toBe(401); expect(allowed.statusCode).toBe(200); await app.close(); }); test('returns timeout response when rendering exceeds configured limit', async () => { const app = await build( {renderTimeoutMs: 7}, { renderGacha: async (_request, renderOptions) => { throw new RenderTimeoutError(renderOptions.timeoutInMilliseconds); }, }, ); const response = await app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 4}}); expect(response.statusCode).toBe(504); expect(response.json().error).toBe('render_timeout'); await app.close(); }); test('shares the render slot with the wheel route', async () => { /** @type {() => void} */ let finishGacha = () => {}; const app = await build( {}, { renderGacha: () => new Promise((resolve) => { finishGacha = () => resolve({buffer: mp4, byteLength: mp4.byteLength, durationMs: 9}); }), renderGif: async () => ({buffer: Buffer.from('GIF89a'), byteLength: 6, durationMs: 1}), }, ); const pending = app.inject({method: 'POST', url: '/api/gacha', payload: {label: 'Pizza', rarity: 3}}); await new Promise((resolve) => setTimeout(resolve, 20)); const busy = await app.inject({method: 'POST', url: '/api/gif', payload: {options: ['a', 'b']}}); finishGacha(); expect(busy.statusCode).toBe(429); expect((await pending).statusCode).toBe(200); await app.close(); }); });