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-load-api-')); process.env.FLIGHT_DB_PATH = path.join(dir, 'load.db'); }); afterAll(async () => { delete process.env.FLIGHT_DB_PATH; await fs.rm(dir, { recursive: true, force: true }); }); const post = (id: string, body: unknown) => POST(new Request(`http://localhost/api/packages/${id}/load`, { method: 'POST', body: JSON.stringify(body) }), { params: Promise.resolve({ id }), }); describe('POST /api/packages/:id/load', () => { it('rejects a missing/non-string planeId and unknown fields with 400', async () => { for (const body of [{}, { planeId: 42 }, { bogus: 1 }]) { const res = await post('p-x', body); expect(res.status).toBe(400); expect(((await res.json()) as { error: string }).error).toBeTruthy(); } }); it('rejects an unknown package id with 404', async () => { const res = await post('nope', { planeId: 'plane-starter' }); expect(res.status).toBe(404); expect(((await res.json()) as { error: string }).error).toMatch(/not found/); }); it('rejects an unknown plane id with 404', async () => { const { db } = await getGameDb(); await db.insert(schema.packages).values({ id: 'p-ghost-plane', originIcao: 'EDDF', destIcao: 'LEMD', reward: 1, cargoType: 'Mail', cargoCount: 1, deadlineSimTime: Date.now() + 3_600_000, status: 'open', planeId: null, createdSimTime: Date.now(), createdRealMs: Date.now(), // v0.65.0 — fresh wall-clock stamp or the pre-action tick expires it }); const res = await post('p-ghost-plane', { planeId: 'ghost' }); expect(res.status).toBe(404); }); it('rejects loading a package whose origin the plane is not at with 409', async () => { const { db } = await getGameDb(); // The seeded starter sits at EDDF — a package from LEMD cannot be loaded there. await db.insert(schema.packages).values({ id: 'p-elsewhere', originIcao: 'LEMD', destIcao: 'EDDF', reward: 1, cargoType: 'Mail', cargoCount: 1, deadlineSimTime: Date.now() + 3_600_000, status: 'open', planeId: null, createdSimTime: Date.now(), createdRealMs: Date.now(), // v0.65.0 — fresh wall-clock stamp or the pre-action tick expires it }); const res = await post('p-elsewhere', { planeId: 'plane-starter' }); expect(res.status).toBe(409); expect(((await res.json()) as { error: string }).error).toMatch(/at EDDF/); }); it('rejects loading a non-open package with 409', async () => { const { db } = await getGameDb(); await db.insert(schema.packages).values({ id: 'p-busy', originIcao: 'EDDF', destIcao: 'LEMD', reward: 1, cargoType: 'Mail', cargoCount: 1, deadlineSimTime: Date.now() + 3_600_000, status: 'loaded', planeId: 'plane-starter', createdSimTime: Date.now(), }); const res = await post('p-busy', { planeId: 'plane-starter' }); expect(res.status).toBe(409); expect(((await res.json()) as { error: string }).error).toMatch(/loaded/); }); it('loads an open package at the plane\'s airport and returns the full snapshot', async () => { const { db } = await getGameDb(); // A known open package originating where the starter actually sits (EDDF). await db.insert(schema.packages).values({ id: 'p-home', originIcao: 'EDDF', destIcao: 'LEMD', reward: 1_000, cargoType: 'Mail', cargoCount: 4, deadlineSimTime: Date.now() + 24 * 3_600_000, status: 'open', planeId: null, createdSimTime: Date.now(), createdRealMs: Date.now(), // v0.65.0 — fresh wall-clock stamp or the pre-action tick expires it }); const res = await post('p-home', { planeId: 'plane-starter' }); 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 }; // ISS-117 planes: Array>; packages: Array<{ id: string; status: string; planeId: string | null }>; }; expect(Object.keys(body).sort()).toEqual([ 'account', 'catalog', 'fuelPricePerL', 'packages', 'planes', 'sellFraction', 'simState', ]); const loaded = body.packages.find((p) => p.id === 'p-home')!; expect(loaded.status).toBe('loaded'); expect(loaded.planeId).toBe('plane-starter'); // Persisted, not just in the response. const row = (await db.select().from(schema.packages).where(eq(schema.packages.id, 'p-home')))[0]; expect(row.status).toBe('loaded'); expect(row.planeId).toBe('plane-starter'); // A second load of the same package is rejected — it is no longer open. const again = await post('p-home', { planeId: 'plane-starter' }); expect(again.status).toBe(409); }); });