openova/.github/workflows/test-billing-integration.yaml
hatiyildiz 3e956b7d81 test: voucher issuance integration test — real Postgres (#147)
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>
2026-04-28 13:53:43 +02:00

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"