SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
6.3 KB · 114 lines typescript
Raw Blame History
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