deadhead-space-freight-tycoon/apps/server/test/db.ts

101 lines
3.2 KiB
TypeScript

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 { 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<void> {
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<SeededPlayer> {
const player = await createPlayer(db, { 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 };
}