Skip to main content

Smart contract tests

177 Foundry tests across 4 files, organized by domain:
cd contracts && forge test

Test structure

FileTestsWhat it covers
MouthBetPvP.t.sol641v1 PvP flow: deposit, withdraw, resolve, finalize, claim, resolverClaim, nonDisputeable
MouthBetOpenPvP.t.sol35Open PvP flow: multi-opponent deposits, partial fill, activatePartial, max opponents, withdrawAfterDeadline
MouthBetEdgeCases.t.sol32Cross-cutting scenarios: re-resolve / dispute window, mutual cancel, asymmetric odds, fee snapshot, resolver change, full lifecycle
MouthBetFactory.t.sol46Factory: bet creation with various odds, admin functions (fee, resolver, treasury), upgrades, initialization
All test contracts inherit from BaseTest.sol, which sets up a fork-like environment with a mock USDC token, deploys the factory via UUPS proxy, and provides shared helpers (_createPvPBet, _createOpenBet, _opponentDeposit, _resolve, etc.).

Coverage

FileLinesStatementsFunctions
MouthBet.sol98.09% (205/209)97.37% (222/228)100% (21/21)
MouthBetFactory.sol96.15% (50/52)95.45% (42/44)100% (10/10)
cd contracts && forge coverage --ir-minimum
Branch coverage numbers appear low (~47% / ~12%) but this is an artifact of --ir-minimum generating imprecise source mappings. The --ir-minimum flag is required because the contracts hit “stack too deep” errors with standard coverage compilation.

Backend tests

The backend uses integration tests that exercise the full HTTP request lifecycle: routing, middleware, input validation, permission checks, state transitions, and database queries. Tests run against the real PostgreSQL database — only external services that cannot be automated (Privy OAuth, Base blockchain) are mocked.
cd backend && pnpm test
56 tests across 4 files. Runs in ~60 seconds.

What the tests cover

Auth (auth.test.ts — 11 tests)

FlowWhat’s verified
POST /auth/loginFirst login creates user in DB, returns profile
POST /auth/loginMissing or invalid token returns 401
GET /auth/meAuthenticated user gets their profile
GET /auth/meNo token → 401, valid token but user deleted → 403
POST /auth/withdrawValid withdrawal returns txHash
POST /auth/withdrawInvalid address, zero amount, missing wallet, missing fields → 400

Bets (bets.test.ts — 33 tests)

FlowWhat’s verified
ListingGET /bets returns bets, supports status and category filters, pagination
DetailGET /bets/:id returns bet with challenger handle; 404 for non-existent
CreationPOST /bets creates PvP and open_pvp bets; validates required fields, odds range (1–99), minimum amount (5 USDC); 401 without auth
AcceptanceCannot accept own bet (400), non-pending bet (400), wrong opponent for PvP (403), expired deadline (400)
CancellationOnly challenger can cancel (403 for others), only pending bets (400)
ResolutionOnly resolver wallet can resolve (403), must be active bet (400), validates outcome values
FinalizationOnly resolver can finalize (403), must be resolved bet (400)
ClaimOnly winner can claim (403 for loser/non-participant), must be finalized (400), no double-claim (400)
Full lifecycleEnd-to-end: create → accept → resolve → finalize → claim

Users (users.test.ts — 8 tests)

FlowWhat’s verified
GET /users/:handleReturns public profile (no private fields like privyId); 404 for non-existent
GET /users/:handle/betsReturns bet history (as challenger and as opponent); pagination; 404
GET /users/:handle/statsReturns stats array; 404 for non-existent

Leaderboard (leaderboard.test.ts — 4 tests)

FlowWhat’s verified
GET /leaderboardReturns leaderboard array with activity data
GET /leaderboardSupports sort parameter (wins, volume, profit) and limit

What is mocked

External services that require real credentials, OAuth flows, or cost real money are replaced with mock functions:
ModuleMockWhy
Privy authverifyAccessToken, getUser, createOAuth with X cannot be automated in tests
tx-relayerrelayCreateBet, relayDeposit, relayWithdraw, relayClaim, relayTransferUSDCWould send real USDC transactions on Base mainnet
blockchainresolveBetOnChain, finalizeBetOnChain, publicClientWould interact with real smart contracts
wallet-resolverresolveXHandleToWalletDepends on Privy user lookup
Mocks are defined in src/__tests__/setup.ts and reset before each test via vi.clearAllMocks().

What is real

  • PostgreSQL database — all INSERT, SELECT, UPDATE, DELETE queries hit the real Neon DB
  • Hono app — full middleware chain (CORS, logger, auth middleware)
  • Route handlers — all validation logic, permission checks, SQL queries, state transitions
  • Drizzle ORM — real query building and execution

What the tests do NOT cover

AreaReason
Privy ↔ backend integrationPrivy is mocked — if Privy changes their SDK response format, tests won’t catch it
Smart contract interactionsBlockchain is mocked — contract bugs or ABI mismatches won’t surface
Cron jobs (bet expiration, auto-claim)These run as scheduled processes, not HTTP routes
DisputesNot yet implemented in tests
Frontend ↔ backendTests only cover the API layer, not the React frontend
activity_log populationNo route writes to activity_log (populated by crons), so stats/leaderboard tests use manually inserted data

How test data is isolated

Tests never delete existing data. Each test helper (createTestUser, createTestBet, createTestActivity) tracks the IDs of records it creates. At the end of each test file, cleanupTestData() deletes only those tracked records:
// Tracked during test execution
const createdUserIds: string[] = [];
const createdBetIds: string[] = [];

// afterAll — only deletes what tests created
await db.delete(bets).where(inArray(bets.id, createdBetIds));
await db.delete(users).where(inArray(users.id, createdUserIds));
Test files run sequentially (fileParallelism: false) to avoid race conditions on the shared database.

File structure

backend/
  vitest.config.ts              # Vitest config (sequential execution, path aliases)
  src/
    app.ts                      # Hono app (extracted from index.ts for testability)
    index.ts                    # Server startup + cron (not imported by tests)
    __tests__/
      setup.ts                  # Global mocks (Privy, blockchain, tx-relayer)
      helpers.ts                # createTestUser, createTestBet, makeRequest, cleanup
      auth.test.ts              # Auth route tests
      bets.test.ts              # Bet lifecycle tests
      users.test.ts             # User profile/stats tests
      leaderboard.test.ts       # Leaderboard tests
The app.ts / index.ts split exists specifically for testing. app.ts exports the Hono app without calling serve() or starting cron jobs, so tests can import it cleanly via app.request().