TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1import { afterAll, beforeAll, describe, expect, it } from "vitest";2import { randomUUID } from "node:crypto";3import { closeDb, creditTransactions, db, eq, gameRounds, sql, users, wallets } from "@spinza/database";4import { syncGames } from "@spinza/database/sync-games";5import { buildApp } from "../src/app";6import { closeRedis, redis } from "../src/lib/redis";7import type { FastifyInstance } from "fastify";89let app: FastifyInstance;10let cookie = "";11let userId = "";12const username = `a_${randomUUID().slice(0, 8)}`;13const origin = "http://localhost:8230";14const headers = () => ({ origin, cookie: `spinza_session=${cookie}` });1516beforeAll(async () => {17 process.env.NODE_ENV = "test";18 app = await buildApp();19 await app.ready();20 const keys = await redis().keys("rl:*");21 if (keys.length) await redis().del(...keys);22 await syncGames(db);23 await db.execute(sql`update games set lifecycle = 'published' where slug in ('dropzone','grid-break','orbit','the-vault','escape-99')`);24 const res = await app.inject({ method: "POST", url: "/api/auth/register", headers: { origin }, payload: { username, password: "password123", confirmPassword: "password123", ageConfirmed: true } });25 cookie = res.cookies.find((c) => c.name === "spinza_session")!.value;26 userId = res.json().user.id;27});2829afterAll(async () => {30 if (userId) await db.delete(users).where(eq(users.id, userId));31 await app.close();32 await closeDb();33 await closeRedis();34});3536describe("arcade instant games", () => {37 it("dropzone resolves a drop and is idempotent", async () => {38 const id = randomUUID();39 const a = await app.inject({ method: "POST", url: "/api/arcade/dropzone/play", headers: headers(), payload: { bet: 100, clientRoundId: id, input: { risk: "high", lane: 0 } } });40 expect(a.statusCode).toBe(200);41 const body = a.json();42 expect(body.outcome.steps).toHaveLength(12);43 expect(body.outcome.summary.startLane).toBe(0);44 expect(body.outcome.summary.table).toHaveLength(13);45 const b = await app.inject({ method: "POST", url: "/api/arcade/dropzone/play", headers: headers(), payload: { bet: 100, clientRoundId: id, input: { risk: "high", lane: 0 } } });46 expect(b.json().roundId).toBe(body.roundId);47 expect(b.json().replayed).toBe(true);48 });4950 it("grid-break and orbit resolve with valid inputs and reject bad bets", async () => {51 const g = await app.inject({ method: "POST", url: "/api/arcade/grid-break/play", headers: headers(), payload: { bet: 20, clientRoundId: randomUUID(), input: { column: 3 } } });52 expect(g.statusCode).toBe(200);53 expect(g.json().outcome.steps.length).toBeGreaterThanOrEqual(1);54 const o = await app.inject({ method: "POST", url: "/api/arcade/orbit/play", headers: headers(), payload: { bet: 20, clientRoundId: randomUUID(), input: { angle: 90 } } });55 expect(o.statusCode).toBe(200);56 expect(o.json().outcome.summary.orbits.length).toBeGreaterThanOrEqual(3);57 const bad = await app.inject({ method: "POST", url: "/api/arcade/orbit/play", headers: headers(), payload: { bet: 33, clientRoundId: randomUUID(), input: {} } });58 expect(bad.statusCode).toBe(400);59 });60});6162describe("arcade ladder games", () => {63 it("the vault: start resolves the first layer, then secure or bust; ledger stays consistent", async () => {64 const start = await app.inject({ method: "POST", url: "/api/arcade/the-vault/start", headers: headers(), payload: { bet: 100, clientRoundId: randomUUID() } });65 expect(start.statusCode).toBe(200);66 let s = start.json().session;67 expect(["running", "busted"]).toContain(s.status);68 if (s.status === "running") {69 expect(s.stage).toBe(1);70 expect(s.current).toBeCloseTo(1.2, 2);71 expect(s.offers).toHaveLength(1);72 const act = await app.inject({ method: "POST", url: "/api/arcade/the-vault/act", headers: headers(), payload: { roundId: s.roundId, action: { type: "cashout" } } });73 expect(act.statusCode).toBe(200);74 s = act.json().session;75 expect(s.status).toBe("cashed");76 expect(s.win).toBe(120);77 expect(act.json().balance).toBeGreaterThan(0);78 // Acting again on a settled run is a no-op.79 const again = await app.inject({ method: "POST", url: "/api/arcade/the-vault/act", headers: headers(), payload: { roundId: s.roundId, action: { type: "continue" } } });80 expect(again.json().session.status).toBe("cashed");81 }82 const [{ total }] = await db.select({ total: sql<number>`coalesce(sum(amount),0)::bigint` }).from(creditTransactions).where(eq(creditTransactions.userId, userId));83 const w = await db.query.wallets.findFirst({ where: eq(wallets.userId, userId) });84 expect(Number(total)).toBe(w!.balance);85 const rounds = await db.select().from(gameRounds).where(eq(gameRounds.userId, userId));86 expect(rounds.length).toBeGreaterThanOrEqual(4);87 });8889 it("escape 99: only one run at a time; climbing advances floors", async () => {90 const start = await app.inject({ method: "POST", url: "/api/arcade/escape-99/start", headers: headers(), payload: { bet: 10, clientRoundId: randomUUID() } });91 expect(start.statusCode).toBe(200);92 let s = start.json().session;93 if (s.status === "running") {94 const dup = await app.inject({ method: "POST", url: "/api/arcade/escape-99/start", headers: headers(), payload: { bet: 10, clientRoundId: randomUUID() } });95 expect(dup.statusCode).toBe(409);96 const cur = await app.inject({ method: "GET", url: "/api/arcade/escape-99/current", headers: headers() });97 expect(cur.json().session.roundId).toBe(s.roundId);98 for (let i = 0; i < 5 && s.status === "running"; i++) {99 const offer = s.offers[0];100 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 } } });101 expect(act.statusCode).toBe(200);102 const next = act.json().session;103 if (next.status === "running") expect(next.stage).toBe(s.stage + offer.advance);104 s = next;105 }106 if (s.status === "running") {107 const out = await app.inject({ method: "POST", url: "/api/arcade/escape-99/act", headers: headers(), payload: { roundId: s.roundId, action: { type: "cashout" } } });108 expect(out.json().session.status).toBe("cashed");109 expect(out.json().session.win).toBeGreaterThan(0);110 }111 }112 });113});114