import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { eq } from 'drizzle-orm'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import * as schema from '@/lib/db/schema'; import { getGameDb } from '@/lib/db/client'; import { POST } from './route'; let dir: string; beforeAll(async () => { // Same pattern as app/api/state/route.test.ts: point the DB singleton at a // throwaway file (fresh module registry per test file → fresh singleton). dir = await fs.mkdtemp(path.join(os.tmpdir(), 'flight-fly-api-')); process.env.FLIGHT_DB_PATH = path.join(dir, 'fly.db'); }); afterAll(async () => { delete process.env.FLIGHT_DB_PATH; await fs.rm(dir, { recursive: true, force: true }); }); const post = (planeId: string, body: unknown) => POST(new Request(`http://localhost/api/planes/${planeId}/fly`, { method: 'POST', body: JSON.stringify(body) }), { params: Promise.resolve({ planeId }), }); describe('POST /api/planes/:planeId/fly', () => { it('rejects a missing/non-string destIcao and unknown fields with 400', async () => { for (const body of [{}, { destIcao: 42 }, { bogus: 1 }]) { const res = await post('plane-starter', body); expect(res.status).toBe(400); expect(((await res.json()) as { error: string }).error).toBeTruthy(); } }); it('rejects an ICAO not in the airport dataset with 400', async () => { const res = await post('plane-starter', { destIcao: 'ZZZZ' }); expect(res.status).toBe(400); expect(((await res.json()) as { error: string }).error).toMatch(/unknown airport/); }); it('rejects an unknown plane id with 404', async () => { const res = await post('ghost-plane', { destIcao: 'LEMD' }); expect(res.status).toBe(404); expect(((await res.json()) as { error: string }).error).toMatch(/not found/); }); it('rejects a flying plane with 409', async () => { const { db } = await getGameDb(); await db.update(schema.planes) .set({ status: 'flying', departureSimTime: Date.now(), destIcao: 'LEMD' }) .where(eq(schema.planes.id, 'plane-starter')); const res = await post('plane-starter', { destIcao: 'EDDM' }); expect(res.status).toBe(409); expect(((await res.json()) as { error: string }).error).toMatch(/flying/); // Ground it again for later tests. await db.update(schema.planes) .set({ status: 'available', departureSimTime: null, destIcao: null }) .where(eq(schema.planes.id, 'plane-starter')); }); it('rejects a leg that does not fit even a full tank with 409 (ISS-082 backstop)', async () => { const { db } = await getGameDb(); // UNNT → LPPD ≈ 7670 km needs ~10k L — beyond the ATR's 6300 L tank, so no auto-refuel can make it flyable. await db.update(schema.planes).set({ locationIcao: 'UNNT' }).where(eq(schema.planes.id, 'plane-starter')); const res = await post('plane-starter', { destIcao: 'LPPD' }); expect(res.status).toBe(409); expect(((await res.json()) as { error: string }).error).toMatch(/insufficient fuel/i); // Put the starter back at the hub for later tests. await db.update(schema.planes).set({ locationIcao: 'EDDF' }).where(eq(schema.planes.id, 'plane-starter')); }); it('flies the plane and returns the full snapshot with departure + destination set', async () => { const res = await post('plane-starter', { destIcao: 'LEMD' }); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toContain('application/json'); // Response convention (v0.8.0): the FULL snapshot, same shape as GET /api/state. const body = (await res.json()) as { account: { cash: number }; simState: { clockMs: number; timeScale: number; paused: boolean }; planes: Array<{ id: string; status: string; destIcao: string | null; departureSimTime: number | null; locationIcao: string }>; }; expect(Object.keys(body).sort()).toEqual([ 'account', 'catalog', 'fuelPricePerL', 'packages', 'planes', 'sellFraction', 'simState', ]); const plane = body.planes.find((p) => p.id === 'plane-starter')!; expect(plane.status).toBe('flying'); expect(plane.destIcao).toBe('LEMD'); // destination set at fly time (v0.12.0) expect(typeof plane.departureSimTime).toBe('number'); // departure set at fly time expect(plane.locationIcao).toBe('EDDF'); // origin unchanged until arrival // A second flight of the same (now flying) plane is rejected — it is no longer available. const again = await post('plane-starter', { destIcao: 'EDDM' }); expect(again.status).toBe(409); }); });