import { randomUUID } from "node:crypto"; import { eq, sql } from "drizzle-orm"; import { afterAll, beforeAll, beforeEach } from "vitest"; import { createDatabase, type Database, type Db } from "../src/db/client"; import { runMigrations } from "../src/db/migrate"; import { user } from "../src/db/auth-schema"; import { bays, companies, pilots, players, ships } from "../src/db/schema"; import { createPlayer, type NewPlayer } from "../src/game/new-player"; import { testConfig } from "./helpers"; export const TEST_NOW = Date.parse("2100-01-01T00:00:00Z"); /** * Connects a test file to the test database: migrates once, empties every * table before each test, and closes the pool at the end. Use `database()` * inside tests. */ export function useTestDatabase(): () => Database { let database: Database | undefined; const url = testConfig().databaseUrl; beforeAll(async () => { await runMigrations(url); database = createDatabase(url); }); beforeEach(async () => { await resetDatabase(database!.db); }); afterAll(async () => { await database?.close(); }); return () => { if (!database) throw new Error("useTestDatabase: database not ready (use it inside tests)"); return database; }; } /** Empties every table in the public schema. */ export async function resetDatabase(db: Db): Promise { const result = await db.execute<{ tablename: string }>( sql`select tablename from pg_tables where schemaname = 'public'`, ); const names = result.rows.map((r) => `"${r.tablename}"`); if (names.length > 0) await db.execute(sql.raw(`truncate ${names.join(", ")} cascade`)); } export type SeedState = "tutorial" | "owner"; export interface SeededPlayer extends NewPlayer { /** The Courier owned in the "owner" state. */ shipId?: string; } /** * Creates a player in a named state: * - `tutorial`: brand new player (what guest sign-up creates). * - `owner`: tutorial over, owns one Courier at Earth crewed by the avatar, has an Earth bay. */ export async function seedPlayer( db: Db, state: SeedState, options: { worldSeed?: number; credits?: number } = {}, ): Promise { const userId = randomUUID(); await db.insert(user).values({ id: userId, name: "Guest", email: `${userId}@guest.test`, isAnonymous: true, }); const player = await createPlayer(db, { userId, worldSeed: options.worldSeed ?? 12345, now: TEST_NOW, }); if (options.credits !== undefined) await setCredits(db, player.playerId, options.credits); if (state === "tutorial") return player; return { ...player, shipId: await makeOwner(db, player.playerId, player.avatarId) }; } export async function setCredits(db: Db, playerId: string, credits: number): Promise { await db.update(companies).set({ credits }).where(eq(companies.playerId, playerId)); } /** * Turns a tutorial player into an owner: tutorial over, one Courier at Earth * crewed by the avatar, an Earth bay. Returns the ship id. */ export async function makeOwner(db: Db, playerId: string, avatarId: string): Promise { const shipId = randomUUID(); await db.transaction(async (tx) => { await tx .update(players) .set({ tutorialActive: false, loanerLocationId: null }) .where(eq(players.id, playerId)); await tx.insert(ships).values({ id: shipId, playerId, modelId: "courier", name: "Test Courier", locationId: "earth", createdAt: TEST_NOW, }); await tx .update(pilots) .set({ assignedShipId: shipId, locationId: null }) .where(eq(pilots.id, avatarId)); await tx .insert(bays) .values({ id: randomUUID(), playerId, locationId: "earth", createdAt: TEST_NOW }); }); return shipId; }