Closes the Group L "integration test — voucher issuance via API — issue → redeem → Org created path" ticket. Per docs/INVIOLABLE-PRINCIPLES.md principle #2 (no mocks where the test would otherwise verify real behavior), this test runs against a real PostgreSQL — not sqlmock. The voucher mechanic lives in store.RedeemPromoCode which runs a transaction with SELECT FOR UPDATE on promo_codes, COUNT lookup on promo_redemptions, and inserts into credit_ledger. Mocking SQL strings doesn't verify whether the transactional invariants actually hold under concurrent contention; this codebase has been bitten by exactly that gap before (#93: counter incremented before order was committed). The test is gated on BILLING_TEST_PG_URL — when unset, it skips (NOT mocks). CI populates it via the new postgres service container in .github/workflows/test-billing-integration.yaml. Each test gets its own Postgres schema (via CREATE SCHEMA + libpq's options=-c search_path) so parallel runs don't cross-contaminate, and so goroutine concurrency tests reliably hit the same schema regardless of which pooled connection they pick up. Coverage: - Issue → Redeem → Credit applied (the canonical happy path) - Per-customer double-redemption blocked - Redemption cap enforced under concurrency (12 goroutines fighting for a 5-cap voucher → exactly 5 successful redemptions, no more) - Soft-deleted codes rejected as "not found" (no tombstone leak per #91) - Inactive codes rejected with distinct "not active" error - Two different customers can each redeem the same voucher - Org-creation prerequisites: customer.tenant_id non-empty, balance > 0 (these are the inputs the downstream tenant.created event consumer feeds into CreateTenant — covered by tenant-service consumer_test.go) CI workflow added: .github/workflows/test-billing-integration.yaml runs the tests against a postgres:16-alpine service container with -race. Refs #147 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.0 KiB
YAML
67 lines
2.0 KiB
YAML
name: Test — Billing Integration (real Postgres)
|
|
|
|
# Runs the integration tests in core/services/billing/store/ that require a
|
|
# real PostgreSQL instance (e.g. voucher_integration_test.go for #147).
|
|
#
|
|
# Per docs/INVIOLABLE-PRINCIPLES.md principle #2 ("no mocks where the test
|
|
# would otherwise verify real behavior"), the voucher transactional path —
|
|
# SELECT FOR UPDATE on promo_codes, the redemption-cap concurrency guard,
|
|
# the soft-delete rejection — must be verified against a real database.
|
|
# We bring up Postgres as a service container and pass its URL through
|
|
# BILLING_TEST_PG_URL.
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- 'core/services/billing/**'
|
|
- 'core/services/shared/**'
|
|
- '.github/workflows/test-billing-integration.yaml'
|
|
branches: [main]
|
|
pull_request:
|
|
paths:
|
|
- 'core/services/billing/**'
|
|
- 'core/services/shared/**'
|
|
- '.github/workflows/test-billing-integration.yaml'
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
integration:
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: billing
|
|
POSTGRES_PASSWORD: billing
|
|
POSTGRES_DB: billing_test
|
|
options: >-
|
|
--health-cmd "pg_isready -U billing"
|
|
--health-interval 5s
|
|
--health-timeout 3s
|
|
--health-retries 10
|
|
ports:
|
|
- 5432:5432
|
|
defaults:
|
|
run:
|
|
working-directory: core/services/billing
|
|
env:
|
|
BILLING_TEST_PG_URL: postgres://billing:billing@localhost:5432/billing_test?sslmode=disable
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@v5
|
|
with:
|
|
go-version: '1.22'
|
|
cache-dependency-path: core/services/billing/go.sum
|
|
|
|
- name: Download modules
|
|
run: go mod download
|
|
|
|
- name: Vet
|
|
run: go vet ./...
|
|
|
|
- name: Run integration tests
|
|
run: go test -race -count=1 -v ./store/ -run "Integration|Voucher"
|