75 lines
3.2 KiB
TypeScript
75 lines
3.2 KiB
TypeScript
import { randomInt, randomUUID } from 'node:crypto';
|
||
import { eq } from 'drizzle-orm';
|
||
import { getGameDb, persist } from '@/lib/db/client';
|
||
import * as schema from '@/lib/db/schema';
|
||
import { loadAirportsGeoJson } from '@/lib/airports';
|
||
import { generateContract, mulberry32 } from '@/lib/game/seed';
|
||
import { tickNow } from '@/lib/sim/engine';
|
||
|
||
// TEST-ONLY hook (v0.7.0) — lets the e2e suite observe contract expiry without
|
||
// waiting hours of game time. Disabled unless FLIGHT_TEST_HOOKS=1, which is set in
|
||
// docker-compose.yml for the e2e instance only; every other deployment 404s here.
|
||
//
|
||
// POST /api/test/expire-contract
|
||
// body { contractId?: string }
|
||
// - with contractId: moves that OPEN contract's deadline to now + HOOK_DEADLINE_MS
|
||
// (game time) so the next fast tick expires it; 404 if unknown/not open.
|
||
// - without: creates a fresh open contract with the same short deadline.
|
||
|
||
const HOOK_DEADLINE_MS = 10_000; // game ms past the current clock (~0.2 s real at 60×)
|
||
|
||
export async function POST(req: Request) {
|
||
if (process.env.FLIGHT_TEST_HOOKS !== '1') {
|
||
return Response.json({ error: 'test hooks disabled' }, { status: 404 });
|
||
}
|
||
|
||
let body: unknown = {};
|
||
try {
|
||
const text = await req.text();
|
||
body = text ? JSON.parse(text) : {};
|
||
} catch {
|
||
return Response.json({ error: 'invalid JSON body' }, { status: 400 });
|
||
}
|
||
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
||
return Response.json({ error: 'body must be a JSON object' }, { status: 400 });
|
||
}
|
||
const contractId = (body as Record<string, unknown>).contractId;
|
||
if (contractId !== undefined && typeof contractId !== 'string') {
|
||
return Response.json({ error: 'contractId must be a string' }, { status: 400 });
|
||
}
|
||
|
||
try {
|
||
const inst = await getGameDb();
|
||
const sim = await tickNow(); // clock is current before we set the deadline
|
||
const deadlineSimTime = Math.round(sim.clockMs + HOOK_DEADLINE_MS);
|
||
|
||
if (typeof contractId === 'string') {
|
||
const rows = await inst.db.select().from(schema.contracts).where(eq(schema.contracts.id, contractId));
|
||
const row = rows[0];
|
||
if (!row || row.status !== 'open') {
|
||
return Response.json({ error: 'contract not found or not open' }, { status: 404 });
|
||
}
|
||
await inst.db.update(schema.contracts).set({ deadlineSimTime }).where(eq(schema.contracts.id, contractId));
|
||
await persist(inst);
|
||
const updated = (await inst.db.select().from(schema.contracts).where(eq(schema.contracts.id, contractId)))[0];
|
||
return Response.json(updated ?? row);
|
||
}
|
||
|
||
const fc = await loadAirportsGeoJson();
|
||
const airports = fc.features.map((f) => ({
|
||
icao: f.properties.icao,
|
||
name: f.properties.name,
|
||
lat: f.properties.lat,
|
||
lon: f.properties.lon,
|
||
}));
|
||
const rng = mulberry32(randomInt(0, 2 ** 31));
|
||
const created = generateContract({ id: randomUUID(), airports, rng, nowMs: sim.clockMs, deadlineOffsetMs: HOOK_DEADLINE_MS });
|
||
await inst.db.insert(schema.contracts).values(created);
|
||
await persist(inst);
|
||
return Response.json(created, { status: 201 });
|
||
} catch (err) {
|
||
console.error('test hook failed', err);
|
||
return Response.json({ error: 'test hook failed' }, { status: 500 });
|
||
}
|
||
}
|