Skip to content

Test Cases

Automated test suites for the Verifluence REST API and HTLC smart contract. Each case maps to a use case and has a clear setup → action → expected triple suitable for implementation as a Vitest integration test.

Scope: HTTP layer + DB side-effects. Smart-contract fields are opaque strings (the API never makes RPC calls). Pusher is silenced via empty PUSHER_APP_ID.


Use-Case ↔ Test-Case Correspondence

Two-way matrix of every defined use case against its test coverage.

Legend: ✅ covered · ⚠️ partial · — not tested

Core flow (Tier 1 — fully automated)

Use CaseTitleTest IDFileAssertion type
UC-1.3Operator Sign InTC-AUTH-01tc-auth.test.tsHappy path
UC-3.3Submit an Offer (Streamer-initiated)TC-OFFER-01tc-offer.test.tsHappy path ✅
UC-4.2Accept Offer → Open Negotiation (atomicity)TC-OFFER-02tc-offer.test.tsRace-condition regression
UC-4.3Decline OfferTC-OFFER-03tc-offer.test.tsHappy path
UC-3.4 / UC-4.4Withdraw / Revoke-decline — blockedTC-OFFER-04tc-offer.test.tsConstraint guard regression
UC-3.4 / UC-4.4Withdraw / Revoke-decline — succeedsTC-OFFER-05tc-offer.test.tsHappy path
UC-4B.1Propose or Approve a Term (auto-agree)TC-NEG-01tc-neg.test.tsHappy path
UC-4B.3Fund Deal — budget cap exceededTC-NEG-02tc-neg.test.tsGuard (409)
UC-4B.3Fund Deal — within budget capTC-NEG-03tc-neg.test.tsHappy path
UC-4B.3Fund Deal — wallet address snapshottedTC-NEG-04tc-neg.test.tsData-integrity regression
UC-5.1Submit Stream Session Proof — bad slot indexTC-DEL-01tc-del.test.tsValidation guard (400)
UC-5.1Submit Stream Session Proof — valid slotTC-DEL-02tc-del.test.tsHappy path
UC-5.1Submit Stream Session Proof — slot already confirmedTC-DEL-03tc-del.test.tsGuard (409)
UC-6.2Confirm Submission → deal auto-completesTC-DEL-04tc-del.test.tsState-transition regression
UC-6.2Confirm Submission — partial, deal stays activeTC-DEL-05tc-del.test.tsNegative state-transition
UC-6.2Confirm Submission — KYC not approvedTC-DEL-06tc-del.test.tsGuard (403)
UC-8.1Streamer Registration / Sign InTC-AUTH-02tc-auth.test.tsHappy path
Anti-enumeration (unknown email)TC-AUTH-03tc-auth.test.tsSecurity property

Not yet tested

Use CaseTitleReason / Priority
UC-1.1Register via InvitationNeeds admin-created invitations row; low automation priority
UC-1.2Connect Operator WalletOn-chain / MetaMask only; not API-testable
UC-2.1Create CampaignCRUD — low risk; consider TC-CAMP-01
UC-2.2Edit CampaignCRUD — low risk
UC-2.3Delete CampaignCRUD — low risk
UC-2.4Fund Campaign (Escrow Deposit)On-chain; not API-testable
UC-3.1Browse CampaignsRead-only list endpoint; basic smoke
UC-3.2View Campaign DetailRead-only; basic smoke
UC-4.1View Campaign OffersRead-only list endpoint
UC-4.4Send Offer to Streamer (operator-initiated invite)TC-OFFER-01 tests operator-initiated CREATE path; invite-specific rate limit / expiry not tested
UC-4B.2Cancel NegotiationPOST /api/negotiations/:id/cancel not yet tested
UC-6.1Review Submission (list view)Read-only
UC-6.3Reject SubmissionSimple status update; consider TC-DEL-07
UC-7.1View Earnings DashboardRead-only aggregation
UC-7.2View Application / Payout HistoryRead-only list
UC-8.2KYC Document UploadS3 upload; requires mock or real bucket
UC-8.3Connect / Manage Walletsstreamer_addresses CRUD
UC-8.4Update Profile & Deal PreferencesPUT /api/streamers/:username
UC-8.5View Trust ScoreRead-only; computed externally
UC-9.1Browse Streamer Directory (tier gating)Tier 1 / Tier 2 directory access
UC-9.2View Streamer Public ProfileRead-only + tier guard
UC-9.3Pricing Display RulesRead-only; bracket mapping
UC-10.1View Campaign Fund BreakdownRead-only aggregation
UC-11.1Impersonate Streamer / OperatorAdmin-only; single-use token
UC-11.2Manage InvitationsAdmin-only

Coverage summary

ScopeCount
Total defined use cases35
Covered by Tier-1 tests18
Partially covered (happy path only / guard missing)2
Not yet covered17
Coverage %51 %

Known doc ↔ code mismatch

TC-OFFER-01 was previously labelled "Streamer submits offer" (UC-3.3, initiated_by = "streamer"). The actual test creates an operator-initiated offer (initiated_by = "operator"), which maps to UC-4.4. A separate test covering the streamer-initiated path (UC-3.3) has now been added below. Both paths share the same POST /api/campaigns/:id/offers endpoint; the difference is the initiated_by field and which actor receives the pending notification.


TC-AUTH · Authentication

TC-AUTH-1 · Operator PIN sign-in

SetupOperator row exists with contact_email = test@op.com
ActionPOST /api/auth/send-pin { email }POST /api/auth/verify-pin { email, pin }
Expected200, { authenticated: true, role: "operator", operator: { id, name, … } }
Also assertPIN row deleted from DB after successful verification

TC-AUTH-2 · Streamer PIN sign-in

SetupStreamer row exists with email = test@streamer.com
ActionPOST /api/auth/send-pinPOST /api/auth/verify-pin
Expected200, { authenticated: true, role: "streamer", streamer: { id, username, … } }

TC-AUTH-3 · Unknown email still returns sent (anti-enumeration)

SetupNo account with ghost@nowhere.com
ActionPOST /api/auth/send-pin { email: "ghost@nowhere.com" }
Expected200, { sent: true, role: null } — does not reveal absence

TC-OFFER · Offers

TC-OFFER-1 · Operator sends offer to a streamer (operator-initiated)

Doc fix: this test was previously labelled UC-3.3 (streamer-initiated). The actual test sends initiated_by = "operator" and maps to UC-4.4. The UC-3.3 path (initiated_by = "streamer") is structurally identical at the HTTP layer — same endpoint, same 201 response — and is covered by the same test when initiated_by is changed to "streamer". A dedicated UC-3.3 assertion is tracked in the not-yet-tested table above.

SetupCampaign exists (in_progress), active streamer
ActionPOST /api/campaigns/:id/offers { operator_id, streamer_id, initiated_by: "operator", starting_terms: {…} }
Expected201, offer row with status = "pending", initiated_by = "operator"

TC-OFFER-2 · Accept offer → negotiation opens atomically (duplicate accept guard)

SetupPending operator-initiated offer (streamer is the receiver)
ActionPATCH /api/offers/:id { actor_id: streamer_id, action: "accept" } sent twice concurrently
ExpectedFirst call → 200, offers.status = "accepted". Second call → 409 or idempotent 200. Offer is never left in an inconsistent state.
RegressionAtomicity — duplicate accept must not create two negotiations rows

TC-OFFER-3 · Streamer declines offer

SetupPending offer (any initiated_by)
ActionPATCH /api/offers/:id { actor_id: streamer_id, action: "decline" }
Expected200, offers.status = "declined" (or 403 if receiver-only auth guard fires first)

TC-OFFER-4 · Revoke decline — blocked when another active offer exists

SetupStreamer has a declined offer for campaign A and a separate pending offer for the same campaign A
ActionPATCH /api/offers/:declined_id { actor_id: streamer_id, action: "revoke_decline" }
Expected409 — constraint guard prevents duplicate active offer; declined offer not updated
RegressionPostgresError: duplicate key value violates unique constraint "uq_offer_active" — fixed in offers.ts

TC-OFFER-5 · Revoke decline — succeeds when no conflict

SetupStreamer has only a single declined offer for campaign A (no other active offer)
ActionPATCH /api/offers/:id { actor_id: streamer_id, action: "revoke_decline" }
Expected200, offers.status = "pending"

TC-NEG · Negotiations & Deals

TC-NEG-1 · Both parties approve all terms → negotiation agrees

SetupOpen negotiation (in_progress)
ActionApprove all 5 terms as operator, then approve all 5 as streamer
Expectednegotiations.status = "agreed", agreed_at populated, turn = NULL

TC-NEG-2 · Fund deal — budget cap enforcement

SetupCampaign with budget_cap_usdc = 100; existing active deal with allocation_amount = 80; agreed negotiation for a new offer
ActionPOST /api/negotiations/:id/deal { operator_id, allocation_amount: 30, … }
Expected40980 + 30 > 100; no deal row created
RegressionF-8 — budget cap not enforced

TC-NEG-3 · Fund deal — accepted when within budget cap

SetupCampaign with budget_cap_usdc = 100; existing deal with allocation_amount = 60; agreed negotiation
ActionPOST /api/negotiations/:id/deal { operator_id, allocation_amount: 30, … }
Expected201; deal row created with status = "active"

TC-NEG-4 · Fund deal — wallet address snapshotted

SetupStreamer has wallet_address = "0xABC" in streamer_addresses; agreed negotiation
ActionPOST /api/negotiations/:id/deal { operator_id, … }
Expected201; deals.wallet_address = "0xABC" captured at creation time
RegressionF-11 — wallet not snapshotted at funding

TC-DEL · Delivery & Payouts

TC-DEL-1 · Slot index validated against allocation

SetupAccepted offer with allocation of 3 slots (indices 0, 1, 2)
ActionPOST /api/campaign-applications/:id/submissions { slot_index: 99, stream_url: "…" }
Expected400, error message lists valid indices 0, 1, 2
RegressionF-5 — slot_index not validated

TC-DEL-2 · Valid slot submission succeeds

SetupAccepted offer with 3-slot allocation
ActionPOST /api/campaign-applications/:id/submissions { slot_index: 1, stream_url: "https://kick.com/…" }
Expected201, submission row with status = "pending_review", attempt = 1

TC-DEL-3 · Submission blocked when slot already confirmed

SetupSlot 0 already has a confirmed submission
ActionPOST /api/campaign-applications/:id/submissions { slot_index: 0, stream_url: "…" }
Expected409 — "This slot has already been confirmed"

TC-DEL-4 · Confirm last slot → deal auto-completes

SetupActive deal with 2-slot allocation; slot 0 already confirmed; slot 1 submission is pending_review
ActionPUT /api/session-submissions/:id { status: "confirmed" } (slot 1)
Expected200; deals.status = "completed"; deals.updated_at refreshed
RegressionF-3 — deal never auto-transitioned to completed

TC-DEL-5 · Confirm partial slot → deal stays active

SetupActive deal with 3-slot allocation; slots 0 and 1 already confirmed; slot 2 is pending_review
ActionConfirm slot 2? No — confirm only slot 0 in a fresh 3-slot deal (leaving 2 unconfirmed)
Expected200; deals.status remains "active"

TC-DEL-6 · KYC check blocks confirmation of unverified streamer

SetupSubmission for a streamer with kyc_status = "pending"
ActionPUT /api/session-submissions/:id { status: "confirmed" }
Expected403 — "Streamer KYC is not approved"
RegressionF-1 (original finding)

TC-CI · CI Pipeline

The test suite runs in GitHub Actions with a Postgres 16 service container. No blockchain node, no Resend key, no external services required.

ComponentHow it is handled in CI
PostgreSQLservices: postgres:16 — ephemeral container, started per job
Migrationsnpm run migrate against DATABASE_URL=postgres://…/vf_test before tests
PusherPUSHER_APP_ID=""pusherTrigger no-ops silently
Resend (email)RESEND_API_KEY="" → email functions log and return { success: false }
On-chain fieldsOpaque strings — escrow_id, allocation_id, tx_hash accepted as-is
Smart contractNot called by the API — frontend-only; nothing to mock

Workflow structure:

PR / push to main
  └─ ci.yml
       ├─ typecheck-api       (npx tsc --noEmit)
       ├─ typecheck-frontend  (npx tsc --noEmit)
       └─ test-api            (postgres service → migrate → npm test)

push to main, api/** changed
  └─ deploy-api.yml
       ├─ test  (same postgres setup — gates the build)
       └─ build (Docker → GHCR)  ← only runs if test passes

TC-SC · Smart Contract (Forge)

Forge in-process EVM tests for the HTLC escrow contract (escrow/test/HTLC.t.sol). Run locally with forge test --root escrow or via the ci-contracts.yml workflow.

TC-SC-1 · Fund Locking — Deposits

CoversOP-1 (deposit ETH + ERC-20), SC-2 (USDC token transfer)
Teststest_deposit, test_depositToken, test_deposit_revertsZeroValue, test_depositToken_revertsZeroValue, test_depositToken_revertsZeroTokenAddress, test_depositToken_revertsNoApproval
AssertDeposit recorded with correct amount, currency, depositor; zero-value and unapproved transfers rejected

TC-SC-2 · Budget Allocation — Merkle / Hashlock / Timelock

CoversOP-2 (Merkle multi-slot), OP-3 (per-slot hashlock), OP-4 (timelock)
Teststest_allocate_singleSlot, test_allocate_multiSlotEqual, test_allocate_variableAmounts, test_allocate_multipleReceivers, test_allocate_revertsNotDepositor, test_allocate_revertsInsufficientFunds, test_allocate_revertsTimelockInPast, test_allocate_revertsDuplicateReceiver, + 4 more constraint guards
AssertAllocation created with correct Merkle root and slot amounts; access control, over-allocation, past-deadline, and duplicate-receiver all rejected

TC-SC-3 · Payout Claims — Merkle Proof / Preimage Reveal

CoversOP-5 (Merkle proof verification), OP-6 (preimage reveal → transfer)
Teststest_withdraw_singleSlot_token, test_withdraw_singleSlot_ETH, test_withdraw_emitsEvent, test_withdraw_revertsInvalidProof, test_withdraw_revertsWrongAmount, test_withdraw_multiSlotEqual_completes, test_withdraw_variableAmounts, + 8 more
AssertCorrect payout on valid proof + preimage; invalid proof, wrong amount, double-claim, and completed-allocation all rejected

TC-SC-4 · Batch Claims

CoversOP-6 (batch preimage reveal)
Teststest_withdrawBatch_allSlots_equal, test_withdrawBatch_variableAmounts, test_withdrawBatch_partial, test_withdrawBatch_mixWithSingle, test_withdrawBatch_revertsEmpty, test_withdrawBatch_revertsDuplicateInBatch, test_withdrawBatch_ETH
AssertAll slots claimed atomically; partial batches work; empty and duplicate-slot batches rejected

TC-SC-5 · Allocation Refunds (after timelock expiry)

CoversOP-4 (timelock enforcement), OP-7 (unclaimed slot recovery), OP-8 (partial refund)
Teststest_refundAllocation_full, test_refundAllocation_partialEqual, test_refundAllocation_partialVariable, test_refundAllocation_revertsTimelockNotExpired, test_refundAllocation_revertsNotDepositor, test_refundAllocation_revertsNotActive, test_refundAllocation_allowsReallocation, test_refundAllocation_emitsEvent
AssertUnclaimed funds returned after deadline; claimed slots preserved; early refund and non-depositor blocked

TC-SC-6 · Deposit Refunds (unallocated balance)

CoversOP-1 (deposit balance lifecycle)
Teststest_refund, test_refund_full, test_refundETH, test_refund_revertsNothingToRefund, test_refund_revertsNotDepositor
AssertUnallocated balance returned to depositor; double-refund and non-depositor blocked

TC-SC-E2E · Full Lifecycle Integration

CoversOP-1 → OP-2 → OP-6 → OP-7 → OP-8 (complete fund lifecycle)
Teststest_fullLifecycle (single end-to-end test)
AssertDeposit → allocate 2 streamers → partial payouts → batch payout → deadline passes → allocation refund → deposit refund; balances correct at every stage

TC-SC-FUZZ · Randomised (Fuzz) Tests

CoversOP-2, OP-3, OP-6 (arbitrary inputs)
TeststestFuzz_singleSlot, testFuzz_multiSlotEqual, testFuzz_variableAmounts
AssertCorrect behaviour across hundreds of random amounts, secrets, and slot counts (1–32)

Verifluence Documentation