import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer'; import { getGameDb } from '@/lib/db/client'; import { createSession } from '@/lib/auth'; import { PLANE_TYPES } from '@/lib/game/catalog'; 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-buy-api-')); process.env.FLIGHT_DB_PATH = path.join(dir, 'buy.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 = (body: unknown) => POST( new Request('http://localhost/api/market/buy-plane', { method: 'POST', headers: { 'X-Flight-Session': sessionId }, body: JSON.stringify(body), }), ); describe('POST /api/market/buy-plane', () => { it('rejects a missing model, unknown fields and unknown models with 400', async () => { for (const body of [{}, { model: 7 }, { bogus: 1 }, { model: 'boeing747' }]) { const res = await post(body); expect(res.status).toBe(400); expect(((await res.json()) as { error: string }).error).toBeTruthy(); } }); it('rejects the purchase with 409 when cash is below the price (seeded €20k < €200k)', async () => { const res = await post({ model: 'atr72' }); expect(res.status).toBe(409); expect(((await res.json()) as { error: string }).error).toMatch(/insufficient funds/i); // Cash untouched by the failed attempt. expect((await getLocalPlayer(await getGameDb())).cash).toBe(20_000); }); it('deducts the full price and adds a full-tank plane at EDDF to the snapshot', async () => { const inst = await getGameDb(); // Local player row — top up cash so the purchase can succeed. await updateLocalPlayerEconomy(inst, { cash: 500_000 }); const res = await post({ model: 'atr72' }); expect(res.status).toBe(200); const body = (await res.json()) as { account: { cash: number }; planes: Array<{ id: string; model: string; status: string; fuel: number; locationIcao: string }>; }; expect(body.account.cash).toBe(500_000 - PLANE_TYPES.atr72.price); const bought = body.planes.filter((p) => p.model === 'atr72'); expect(bought.length).toBe(2); // starter + the new one const fresh = bought.find((p) => p.id !== 'plane-starter')!; expect(fresh.status).toBe('available'); expect(fresh.fuel).toBe(PLANE_TYPES.atr72.maxFuel); expect(fresh.locationIcao).toBe('EDDF'); // home base delivery }); });