import { randomUUID } from "node:crypto"; import { content } from "@deadhead/content"; 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 db .update(companies) .set({ credits: options.credits }) .where(eq(companies.playerId, player.playerId)); } if (state === "tutorial") return player; const shipId = randomUUID(); await db.transaction(async (tx) => { await tx .update(players) .set({ tutorialActive: false, loanerLocationId: null }) .where(eq(players.id, player.playerId)); await tx.insert(ships).values({ id: shipId, playerId: player.playerId, modelId: content.shipModels[0]!.id, name: "Test Courier", locationId: "earth", createdAt: TEST_NOW, }); await tx .update(pilots) .set({ assignedShipId: shipId, locationId: null }) .where(eq(pilots.id, player.avatarId)); await tx.insert(bays).values({ id: randomUUID(), playerId: player.playerId, locationId: "earth", createdAt: TEST_NOW, }); }); return { ...player, shipId }; }