131 lines
5.6 KiB
TypeScript
131 lines
5.6 KiB
TypeScript
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 { createSession } from '@/lib/auth';
|
|
import { POST } from './route';
|
|
|
|
let dir: string;
|
|
let sessionId: 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');
|
|
// v1.1.0 (ISS-107): action routes are session-guarded — mint a session for the seeded admin user.
|
|
sessionId = await createSession(await getGameDb(), 'admin-kamilkrzywda');
|
|
});
|
|
|
|
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',
|
|
headers: { 'X-Flight-Session': sessionId },
|
|
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<Record<string, unknown>>;
|
|
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);
|
|
});
|
|
});
|