Files
Flight-Ops/app/api/planes/[planeId]/fly/route.test.ts
T

111 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 the plane cannot fuel with 409 (burn > fuel)', async () => {
const { db } = await getGameDb();
// EDDF → LEMD ≈ 1950 km × 1.75 L/km ≈ 3400 L — far more than 1 L.
await db.update(schema.planes).set({ fuel: 1 }).where(eq(schema.planes.id, 'plane-starter'));
const res = await post('plane-starter', { destIcao: 'LEMD' });
expect(res.status).toBe(409);
expect(((await res.json()) as { error: string }).error).toMatch(/insufficient fuel/i);
// Refill for later tests.
await db.update(schema.planes).set({ fuel: 6300 }).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);
});
});