import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { randomUUID } from "node:crypto"; import { closeDb, creditTransactions, db, eq, gameRounds, sql, users, wallets } from "@spinza/database"; import { syncGames } from "@spinza/database/sync-games"; import { buildApp } from "../src/app"; import { closeRedis, redis } from "../src/lib/redis"; import type { FastifyInstance } from "fastify"; let app: FastifyInstance; let cookie = ""; let userId = ""; const username = `a_${randomUUID().slice(0, 8)}`; const origin = "http://localhost:8230"; const headers = () => ({ origin, cookie: `spinza_session=${cookie}` }); beforeAll(async () => { process.env.NODE_ENV = "test"; app = await buildApp(); await app.ready(); const keys = await redis().keys("rl:*"); if (keys.length) await redis().del(...keys); await syncGames(db); await db.execute(sql`update games set lifecycle = 'published' where slug in ('dropzone','grid-break','orbit','the-vault','escape-99')`); const res = await app.inject({ method: "POST", url: "/api/auth/register", headers: { origin }, payload: { username, password: "password123", confirmPassword: "password123", ageConfirmed: true } }); cookie = res.cookies.find((c) => c.name === "spinza_session")!.value; userId = res.json().user.id; }); afterAll(async () => { if (userId) await db.delete(users).where(eq(users.id, userId)); await app.close(); await closeDb(); await closeRedis(); }); describe("arcade instant games", () => { it("dropzone resolves a drop and is idempotent", async () => { const id = randomUUID(); const a = await app.inject({ method: "POST", url: "/api/arcade/dropzone/play", headers: headers(), payload: { bet: 100, clientRoundId: id, input: { risk: "high", lane: 0 } } }); expect(a.statusCode).toBe(200); const body = a.json(); expect(body.outcome.steps).toHaveLength(12); expect(body.outcome.summary.startLane).toBe(0); expect(body.outcome.summary.table).toHaveLength(13); const b = await app.inject({ method: "POST", url: "/api/arcade/dropzone/play", headers: headers(), payload: { bet: 100, clientRoundId: id, input: { risk: "high", lane: 0 } } }); expect(b.json().roundId).toBe(body.roundId); expect(b.json().replayed).toBe(true); }); it("grid-break and orbit resolve with valid inputs and reject bad bets", async () => { const g = await app.inject({ method: "POST", url: "/api/arcade/grid-break/play", headers: headers(), payload: { bet: 20, clientRoundId: randomUUID(), input: { column: 3 } } }); expect(g.statusCode).toBe(200); expect(g.json().outcome.steps.length).toBeGreaterThanOrEqual(1); const o = await app.inject({ method: "POST", url: "/api/arcade/orbit/play", headers: headers(), payload: { bet: 20, clientRoundId: randomUUID(), input: { angle: 90 } } }); expect(o.statusCode).toBe(200); expect(o.json().outcome.summary.orbits.length).toBeGreaterThanOrEqual(3); const bad = await app.inject({ method: "POST", url: "/api/arcade/orbit/play", headers: headers(), payload: { bet: 33, clientRoundId: randomUUID(), input: {} } }); expect(bad.statusCode).toBe(400); }); }); describe("arcade ladder games", () => { it("the vault: start resolves the first layer, then secure or bust; ledger stays consistent", async () => { const start = await app.inject({ method: "POST", url: "/api/arcade/the-vault/start", headers: headers(), payload: { bet: 100, clientRoundId: randomUUID() } }); expect(start.statusCode).toBe(200); let s = start.json().session; expect(["running", "busted"]).toContain(s.status); if (s.status === "running") { expect(s.stage).toBe(1); expect(s.current).toBeCloseTo(1.2, 2); expect(s.offers).toHaveLength(1); const act = await app.inject({ method: "POST", url: "/api/arcade/the-vault/act", headers: headers(), payload: { roundId: s.roundId, action: { type: "cashout" } } }); expect(act.statusCode).toBe(200); s = act.json().session; expect(s.status).toBe("cashed"); expect(s.win).toBe(120); expect(act.json().balance).toBeGreaterThan(0); // Acting again on a settled run is a no-op. const again = await app.inject({ method: "POST", url: "/api/arcade/the-vault/act", headers: headers(), payload: { roundId: s.roundId, action: { type: "continue" } } }); expect(again.json().session.status).toBe("cashed"); } const [{ total }] = await db.select({ total: sql`coalesce(sum(amount),0)::bigint` }).from(creditTransactions).where(eq(creditTransactions.userId, userId)); const w = await db.query.wallets.findFirst({ where: eq(wallets.userId, userId) }); expect(Number(total)).toBe(w!.balance); const rounds = await db.select().from(gameRounds).where(eq(gameRounds.userId, userId)); expect(rounds.length).toBeGreaterThanOrEqual(4); }); it("escape 99: only one run at a time; climbing advances floors", async () => { const start = await app.inject({ method: "POST", url: "/api/arcade/escape-99/start", headers: headers(), payload: { bet: 10, clientRoundId: randomUUID() } }); expect(start.statusCode).toBe(200); let s = start.json().session; if (s.status === "running") { const dup = await app.inject({ method: "POST", url: "/api/arcade/escape-99/start", headers: headers(), payload: { bet: 10, clientRoundId: randomUUID() } }); expect(dup.statusCode).toBe(409); const cur = await app.inject({ method: "GET", url: "/api/arcade/escape-99/current", headers: headers() }); expect(cur.json().session.roundId).toBe(s.roundId); for (let i = 0; i < 5 && s.status === "running"; i++) { const offer = s.offers[0]; const act = await app.inject({ method: "POST", url: "/api/arcade/escape-99/act", headers: headers(), payload: { roundId: s.roundId, action: { type: "continue", offerId: offer.id } } }); expect(act.statusCode).toBe(200); const next = act.json().session; if (next.status === "running") expect(next.stage).toBe(s.stage + offer.advance); s = next; } if (s.status === "running") { const out = await app.inject({ method: "POST", url: "/api/arcade/escape-99/act", headers: headers(), payload: { roundId: s.roundId, action: { type: "cashout" } } }); expect(out.json().session.status).toBe("cashed"); expect(out.json().session.win).toBeGreaterThan(0); } } }); });