mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-15 08:17:21 +00:00
feat: initialize wheelofnames API
This commit is contained in:
1 parent
194caf17eb
commit
3f5ed700bb
41 files changed
+5031
No files matched your search
@@ -0,0 +1,21 @@
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import {loadConfig, minRenderTimeoutMs} from '../src/config.js';
|
||||
|
||||
describe('loadConfig', () => {
|
||||
test('keeps render timeout compatible with Remotion browser timeout limits', () => {
|
||||
const config = loadConfig({RENDER_TIMEOUT_MS: '500'});
|
||||
|
||||
expect(config.renderTimeoutMs).toBe(minRenderTimeoutMs);
|
||||
});
|
||||
|
||||
test('requires API_TOKEN in production', () => {
|
||||
expect(() => loadConfig({NODE_ENV: 'production'})).toThrow(/API_TOKEN/);
|
||||
});
|
||||
|
||||
test('accepts API_TOKEN in production', () => {
|
||||
const config = loadConfig({NODE_ENV: 'production', API_TOKEN: 'secret'});
|
||||
|
||||
expect(config.requiresApiToken).toBe(true);
|
||||
expect(config.apiToken).toBe('secret');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import {RenderTimeoutError} from '../src/lib/render-errors.js';
|
||||
import {buildServer} from '../src/server.js';
|
||||
|
||||
const config = {
|
||||
apiToken: undefined,
|
||||
host: '127.0.0.1',
|
||||
maxConcurrentRenders: 1,
|
||||
maxOptionChars: 40,
|
||||
maxOptions: 32,
|
||||
port: 0,
|
||||
requiresApiToken: false,
|
||||
renderTimeoutMs: 15000,
|
||||
};
|
||||
|
||||
describe('POST /api/gif', () => {
|
||||
test('returns gif bytes and winner metadata', async () => {
|
||||
const app = await buildServer({
|
||||
config,
|
||||
renderGif: async () => ({
|
||||
buffer: Buffer.from('GIF89a-test'),
|
||||
byteLength: 11,
|
||||
durationMs: 12,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/gif',
|
||||
payload: {
|
||||
options: ['alpha', 'beta', 'gamma'],
|
||||
winnerIndex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers['content-type']).toBe('image/gif');
|
||||
expect(response.headers['x-wheel-winner-index']).toBe('1');
|
||||
expect(response.headers['x-wheel-winner']).toBe('beta');
|
||||
expect(response.rawPayload.subarray(0, 6).toString()).toBe('GIF89a');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('rejects invalid payload', async () => {
|
||||
const app = await buildServer({
|
||||
config,
|
||||
renderGif: async () => ({
|
||||
buffer: Buffer.from('GIF89a-test'),
|
||||
byteLength: 11,
|
||||
durationMs: 12,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/gif',
|
||||
payload: {
|
||||
options: ['only one'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json().error).toBe('invalid_request');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('enforces bearer token when configured', async () => {
|
||||
const app = await buildServer({
|
||||
config: {...config, apiToken: 'secret'},
|
||||
renderGif: async () => ({
|
||||
buffer: Buffer.from('GIF89a-test'),
|
||||
byteLength: 11,
|
||||
durationMs: 12,
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/gif',
|
||||
payload: {
|
||||
options: ['a', 'b'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(401);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('returns timeout response when rendering exceeds configured limit', async () => {
|
||||
const app = await buildServer({
|
||||
config: {...config, renderTimeoutMs: 7},
|
||||
renderGif: async (_request, renderOptions) => {
|
||||
expect(renderOptions.timeoutInMilliseconds).toBe(7);
|
||||
throw new RenderTimeoutError(renderOptions.timeoutInMilliseconds);
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/gif',
|
||||
payload: {
|
||||
options: ['alpha', 'beta'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(504);
|
||||
expect(response.json().error).toBe('render_timeout');
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import {
|
||||
getFinalWheelRotationDegrees,
|
||||
getSliceCenterDegrees,
|
||||
getSliceDegrees,
|
||||
} from '../src/remotion/wheel-layout.js';
|
||||
|
||||
describe('wheel layout', () => {
|
||||
test('calculates equal slice sizes', () => {
|
||||
expect(getSliceDegrees(8)).toBe(45);
|
||||
});
|
||||
|
||||
test('calculates center angle from 3h clockwise', () => {
|
||||
expect(getSliceCenterDegrees(4, 0)).toBe(45);
|
||||
expect(getSliceCenterDegrees(4, 1)).toBe(135);
|
||||
});
|
||||
|
||||
test('final rotation places winner center at 3h', () => {
|
||||
const optionCount = 8;
|
||||
const winnerIndex = 3;
|
||||
const rotation = getFinalWheelRotationDegrees(optionCount, winnerIndex, 7);
|
||||
const winnerCenter = getSliceCenterDegrees(optionCount, winnerIndex);
|
||||
const screenAngle = ((winnerCenter + rotation) % 360 + 360) % 360;
|
||||
|
||||
expect(screenAngle).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import {parseWheelRequest} from '../src/schemas/wheel-request.js';
|
||||
|
||||
const limits = {
|
||||
maxOptionChars: 12,
|
||||
maxOptions: 4,
|
||||
};
|
||||
|
||||
describe('parseWheelRequest', () => {
|
||||
test('trims options and preserves provided winner', () => {
|
||||
const request = parseWheelRequest(
|
||||
{
|
||||
options: [' alpha ', ' beta '],
|
||||
winnerIndex: 1,
|
||||
},
|
||||
limits,
|
||||
() => 0,
|
||||
);
|
||||
|
||||
expect(request.options).toEqual(['alpha', 'beta']);
|
||||
expect(request.winnerIndex).toBe(1);
|
||||
expect(request.durationMs).toBe(6500);
|
||||
expect(request.fps).toBe(15);
|
||||
});
|
||||
|
||||
test('rejects blank options so winner index cannot shift after trimming', () => {
|
||||
expect(() =>
|
||||
parseWheelRequest(
|
||||
{
|
||||
options: ['', 'alpha', 'beta'],
|
||||
winnerIndex: 1,
|
||||
},
|
||||
limits,
|
||||
() => 0,
|
||||
),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
test('selects winner when omitted', () => {
|
||||
const request = parseWheelRequest({options: ['a', 'b']}, limits, () => 1);
|
||||
expect(request.winnerIndex).toBe(1);
|
||||
});
|
||||
|
||||
test('rejects winner outside option range', () => {
|
||||
expect(() =>
|
||||
parseWheelRequest(
|
||||
{
|
||||
options: ['a', 'b'],
|
||||
winnerIndex: 2,
|
||||
},
|
||||
limits,
|
||||
() => 0,
|
||||
),
|
||||
).toThrow(/winnerIndex/);
|
||||
});
|
||||
|
||||
test('rejects too many options', () => {
|
||||
expect(() => parseWheelRequest({options: ['a', 'b', 'c', 'd', 'e']}, limits, () => 0)).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user