- Add the event results page with schedule, rankings, Battle bracket, athletes, records, bilingual labels, and navigation entry. - Add the read/write competition data service, a complete Excel example, empty public state, and validation smoke tests. - Handle Furion statusCode/data responses from the production API and retain static fallback for the public page. - Document Docker sidecar deployment beside test_eventapi, persistent storage, private admin access, Nginx routing, and OSS asset rewriting. Verification: - node server/competition-api.smoke.mjs - node scripts/competition-response-test.mjs - node scripts/battle-layout-test.mjs - npm run build -- --base=https://assets.example.test/static/test/ - npm run rewrite:oss -- https://assets.example.test/static/test/ - Production /api/competition/state returned HTTP 200 after the sidecar started. Deployment note: publish the updated YD-WEB static build before public results can consume the unwrapped API response. The event remains unpublished until an administrator saves and publishes data.
56 lines
3.1 KiB
JavaScript
56 lines
3.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import { get } from 'node:http';
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const directory = await mkdtemp(join(tmpdir(), 'competition-api-'));
|
|
const port = 19000 + Math.floor(Math.random() * 30000);
|
|
const base = `http://127.0.0.1:${port}/api/competition`;
|
|
const server = spawn(process.execPath, ['server/competition-api.mjs'], {
|
|
cwd: fileURLToPath(new URL('..', import.meta.url)),
|
|
env: { ...process.env, COMPETITION_ADMIN_ALLOWED_IPS: '', COMPETITION_DATA_PATH: join(directory, 'state.json'), COMPETITION_PORT: String(port), COMPETITION_ALLOWED_ORIGINS: 'https://www.yuedongsports.com' },
|
|
stdio: 'ignore',
|
|
});
|
|
|
|
try {
|
|
let ready = false;
|
|
for (let attempt = 0; attempt < 40; attempt++) {
|
|
try { ready = (await fetch(`${base}/ping`)).ok; if (ready) break; } catch {}
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
assert.ok(ready, 'API did not start');
|
|
assert.equal((await fetch(`${base}/admin`)).status, 200);
|
|
const deniedStatus = await new Promise((resolve, reject) => {
|
|
get(`${base}/admin`, { localAddress: '127.0.0.2' }, (response) => {
|
|
response.resume();
|
|
resolve(response.statusCode);
|
|
}).on('error', reject);
|
|
});
|
|
assert.equal(deniedStatus, 403, 'an unlisted IP reached the admin endpoint');
|
|
const crossOriginState = await fetch(`${base}/state`, { headers: { Origin: 'https://www.yuedongsports.com' } });
|
|
assert.equal(crossOriginState.headers.get('access-control-allow-origin'), 'https://www.yuedongsports.com');
|
|
const streamController = new AbortController();
|
|
const stream = await fetch(`${base}/stream`, { signal: streamController.signal });
|
|
const reader = stream.body.getReader();
|
|
await reader.read();
|
|
const draft = JSON.parse(await (await fetch(`${base}/state`)).text());
|
|
draft.competition.nameZh = '测试赛事';
|
|
draft.schedule.items = [{ id: 'M1', time: '09:00', nameZh: '单摇', nameEn: 'Speed', group: 'A', gender: 'MALE', venueZh: '主场', venueEn: 'Hall A' }];
|
|
assert.equal((await fetch(`${base}/admin`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(draft) })).status, 200);
|
|
const update = await Promise.race([reader.read(), new Promise((_, reject) => setTimeout(() => reject(new Error('SSE update timed out')), 2000))]);
|
|
assert.match(new TextDecoder().decode(update.value), /event: update/);
|
|
streamController.abort();
|
|
assert.equal((await (await fetch(`${base}/state`)).json()).competition.nameZh, '', 'draft leaked to public');
|
|
draft.published = true;
|
|
assert.equal((await fetch(`${base}/admin`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(draft) })).status, 200);
|
|
assert.equal((await (await fetch(`${base}/state`)).json()).competition.nameZh, '测试赛事');
|
|
assert.equal((await (await fetch(`${base}/admin`)).json()).schedule.items.length, 1);
|
|
console.log('Competition API smoke test passed');
|
|
} finally {
|
|
server.kill();
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|