v1.7.0: multiplayer test coverage — player-scoped fixtures + N-player clock verification (ISS-116, ISS-113)
CI / build + unit + e2e (pull_request) Successful in 13m33s
CI / build + unit + e2e (pull_request) Successful in 13m33s
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getGameDb, persist } from '@/lib/db/client';
|
||||
import { LOCAL_PLAYER } from '@/lib/db/localPlayer';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { getStateSnapshot } from '@/lib/game/state';
|
||||
import { tickNow } from '@/lib/sim/engine';
|
||||
|
||||
// TEST-ONLY hook (v1.7.0, ISS-116) — deletes every plane owned by a player so the e2e suite can
|
||||
// remove planes no API path can get rid of: a minted second account's onboarding starter is its
|
||||
// LAST plane, and the per-owner last-plane guard (v1.2.0, ISS-109) makes it unsellable. Leftover
|
||||
// foreign planes would break later specs' single-fleet self-heal (two-planes.spec.ts sells every
|
||||
// non-starter as local → 403). Disabled unless FLIGHT_TEST_HOOKS=1; sibling of /api/test/set-fuel —
|
||||
// same gate, same conventions.
|
||||
//
|
||||
// POST /api/test/delete-plane
|
||||
// body { username: string } — required. Deletes all planes owned by that player's row (any bound
|
||||
// packages are unbound first) and returns the full state snapshot after ticking, like siblings.
|
||||
// Idempotent: an unknown username deletes nothing and still 200s, so cleanup can run unconditionally.
|
||||
|
||||
export async function POST(req: Request) {
|
||||
if (process.env.FLIGHT_TEST_HOOKS !== '1') {
|
||||
return Response.json({ error: 'test hooks disabled' }, { status: 404 });
|
||||
}
|
||||
|
||||
let body: unknown = {};
|
||||
try {
|
||||
const text = await req.text();
|
||||
body = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
return Response.json({ error: 'invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
||||
return Response.json({ error: 'body must be a JSON object' }, { status: 400 });
|
||||
}
|
||||
const input = body as Record<string, unknown>;
|
||||
for (const key of Object.keys(input)) {
|
||||
if (key !== 'username') {
|
||||
return Response.json({ error: `unknown field "${key}"` }, { status: 400 });
|
||||
}
|
||||
}
|
||||
const username = input.username;
|
||||
if (typeof username !== 'string' || username.length === 0) {
|
||||
return Response.json({ error: 'username is required and must be a non-empty string' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const inst = await getGameDb();
|
||||
await tickNow(); // clock is current before the snapshot we return (same as siblings)
|
||||
const players = await inst.db.select().from(schema.players).where(eq(schema.players.username, username));
|
||||
let deleted = 0;
|
||||
if (players[0]) {
|
||||
const owned = await inst.db.select().from(schema.planes).where(eq(schema.planes.ownerId, players[0].id));
|
||||
for (const plane of owned) {
|
||||
// Free any bound packages' slots — a deleted plane must never leave dangling planeIds.
|
||||
await inst.db.update(schema.packages).set({ planeId: null }).where(eq(schema.packages.planeId, plane.id));
|
||||
await inst.db.delete(schema.planes).where(eq(schema.planes.id, plane.id));
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
if (deleted > 0) {
|
||||
await persist(inst);
|
||||
}
|
||||
// v1.3.0 (ISS-110): admin-style hook — no session; the snapshot reports the seeded local account.
|
||||
return Response.json({ deleted, ...(await getStateSnapshot(LOCAL_PLAYER.id)) });
|
||||
} catch (err) {
|
||||
console.error('test hook failed', err);
|
||||
return Response.json({ error: 'test hook failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user