veza/veza-backend-api/tests/integration
senke 3cd82ba5be fix(hyperswitch): idempotency-key on create-payment and create-refund — v1.0.7 item D
Every outbound POST /payments and POST /refunds from the Hyperswitch
client now carries an Idempotency-Key HTTP header. Key values are
explicit parameters at every call site — no context-carrier magic,
no auto-generation. An empty key is a loud error from the client
(not silent header omission) so a future new call site that forgets
to supply one fails immediately, not months later under an obscure
replay scenario.

Key choices, both stable across HTTP retries of the same logical
call:
  * CreatePayment → order.ID.String() (GORM BeforeCreate populates
    order.ID before the PSP call in ConfirmOrder).
  * CreateRefund → pendingRefund.ID.String() (populated by the
    Phase 1 tx.Create in RefundOrder, available for the Phase 2 PSP
    call).

Scope note (reproduced here for the next reader who grep-s the
commit log for "Idempotency-Key"):

  Idempotency-Key covers HTTP-transport retry (TLS reconnect,
  proxy retry, DNS flap) within a single CreatePayment /
  CreateRefund invocation. It does NOT cover application-level
  replay (user double-click, form double-submit, retry after crash
  before DB write). That class of bug requires state-machine
  preconditions on VEZA side — already addressed by the order
  state machine + the handler-level guards on POST
  /api/v1/payments (for payments) and the partial UNIQUE on
  `refunds.hyperswitch_refund_id` landed in v1.0.6.1 (for refunds).

  Hyperswitch TTL on Idempotency-Key: typically 24h-7d server-side
  (verify against current PSP docs). Beyond TTL, a retry with the
  same key is treated as a new request. Not a concern at current
  volumes; document if retry logic ever extends beyond 1 hour.

Explicitly out of scope: item D does NOT add application-level
retry logic. The current "try once, fail loudly" behavior on PSP
errors is preserved. Adding retries is a separate design exercise
(backoff, max attempts, circuit breaker) not part of this commit.

Interfaces changed:
  * hyperswitch.Client.CreatePayment(ctx, idempotencyKey, ...)
  * hyperswitch.Client.CreatePaymentSimple(...) convenience wrapper
  * hyperswitch.Client.CreateRefund(ctx, idempotencyKey, ...)
  * hyperswitch.Provider.CreatePayment threads through
  * hyperswitch.Provider.CreateRefund threads through
  * marketplace.PaymentProvider interface — first param after ctx
  * marketplace.refundProvider interface — first param after ctx

Removed:
  * hyperswitch.Provider.Refund (zero callers, superseded by
    CreateRefund which returns (refund_id, status, err) and is the
    only method marketplace's refundProvider cares about).

Tests:
  * Two new httptest.Server-backed tests (client_test.go) pin the
    Idempotency-Key header value for CreatePayment and CreateRefund.
  * Two new empty-key tests confirm the client errors rather than
    silently sending no header.
  * TestRefundOrder_OpensPendingRefund gains an assertion that
    f.provider.lastIdempotencyKey == refund.ID.String() — if a
    future refactor threads the key from somewhere else (paymentID,
    uuid.New() per call, etc.) the test fails loudly.
  * Four pre-existing test mocks updated for the new signature
    (mockRefundPaymentProvider in marketplace, mockPaymentProvider
    in tests/integration and tests/contract, mockRefundPayment
    Provider in tests/integration/refund_flow).

Subscription's CreateSubscriptionPayment interface declares its own
shape and has no live Hyperswitch-backed implementation today —
v1.0.6.2 noted this as the payment-gate bypass surface, v1.0.7
item G will ship the real provider. When that lands, item G's
implementation threads the idempotency key through in the same
pattern (documented in v107-plan.md item G acceptance).

CHANGELOG v1.0.7-rc1 entry updated with the full item D scope note
and the "out of scope: retries" caveat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 02:30:02 +02:00
..
api_health_test.go stabilizing veza-backend-api: phase 1 2025-12-16 11:23:49 -05:00
gdpr_flow_test.go style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
logout_blacklist_test.go v0.9.1 2026-03-05 19:22:31 +01:00
main_test.go test(backend): gate testcontainers tests behind VEZA_SKIP_INTEGRATION 2026-04-14 11:45:19 +02:00
oauth_github_test.go style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
oauth_google_test.go style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
payment_flow_test.go fix(hyperswitch): idempotency-key on create-payment and create-refund — v1.0.7 item D 2026-04-18 02:30:02 +02:00
payout_flow_test.go feat(v0.13.5): polish marketplace & compliance — KYC, support, payout E2E 2026-03-13 14:57:19 +01:00
QUARANTINE.md stabilizing veza-backend-api: phase 1 2025-12-16 11:23:49 -05:00
README.md chore(v0.102): consolidate remaining changes — docs, frontend, backend 2026-02-20 13:02:12 +01:00
refund_flow_test.go fix(hyperswitch): idempotency-key on create-payment and create-refund — v1.0.7 item D 2026-04-18 02:30:02 +02:00
resume_upload_test.go feat(auth): v0.911 Keystone - OAuth and auth integration tests 2026-02-27 09:58:53 +01:00
token_refresh_test.go v0.9.1 2026-03-05 19:22:31 +01:00
track_quota_test.go style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
upload_async_polling_test.go stabilizing veza-backend-api: phase 1 2025-12-16 11:23:49 -05:00
upload_flow_test.go stabilizing veza-backend-api: phase 1 2025-12-16 11:23:49 -05:00
webhook_idempotency_test.go style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
webhook_security_test.go style(backend): gofmt -w on 85 files (whitespace only) 2026-04-14 12:22:14 +02:00
webhook_test_helpers.go feat(v0.912): Cashflow - payment E2E integration tests 2026-02-27 20:00:51 +01:00

Tests d'Intégration - veza-backend-api

Objectif: Tests d'intégration reproductibles validant le comportement réel du système avec dépendances externes.


Contrat d'Environnement Minimal

Services Requis

Les tests d'intégration nécessitent les services suivants:

  1. PostgreSQL (obligatoire)

    • Version: 15+
    • Base de données: veza_test
    • Utilisateur: veza / Mot de passe: veza
    • Migrations: Appliquées automatiquement via testcontainers
  2. Redis (obligatoire pour certains tests)

    • Version: 7+
    • Port: 6379 (par défaut)
    • Aucune base de données spécifique requise
  3. RabbitMQ (optionnel)

    • Utilisé uniquement pour tests spécifiques
    • Peut être skippé si non disponible

Méthodes de Setup

Option 1: Testcontainers (Recommandé)

Avantages:

  • Reproductible (même environnement partout)
  • Isolation complète (pas de pollution entre tests)
  • Pas de configuration manuelle requise
  • Fonctionne en CI/CD

Prérequis:

  • Docker installé et en cours d'exécution
  • Go 1.21+

Utilisation:

# Les tests utilisent automatiquement testcontainers
go test ./tests/integration/... -tags integration -v

Configuration:

  • PostgreSQL: Démarre automatiquement via internal/testutils/setup.go
  • Redis: Démarre automatiquement via internal/testutils/setup_redis.go (à créer)
  • Migrations: Appliquées automatiquement depuis migrations/

Option 2: Services Locaux (Alternative)

Quand utiliser:

  • Docker non disponible
  • Tests de développement rapide
  • Debugging local

Configuration (choisir selon la stack démarrée):

  • Avec make infra-up (docker-compose.yml dev) :
export DATABASE_URL="postgresql://veza:password@localhost:15432/veza?sslmode=disable"
export REDIS_ADDR="localhost:16379"
export REDIS_TEST_URL="redis://localhost:16379/15"
export RABBITMQ_URL="amqp://veza:password@localhost:15672/"
  • Avec docker-compose.test.yml (stack dédiée tests) :
export DATABASE_URL="postgresql://veza_test:veza_test@localhost:5434/veza_test?sslmode=disable"
export REDIS_ADDR="localhost:6380"
export REDIS_TEST_URL="redis://localhost:6380/15"
export RABBITMQ_URL="amqp://veza_test:veza_test@localhost:5673/"

Setup manuel:

# PostgreSQL
createdb veza_test
psql veza_test < migrations/*.sql

# Redis
redis-server

# RabbitMQ (optionnel)
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management

Exécution des Tests

Tests d'Intégration (Tous)

# Avec testcontainers (recommandé)
go test ./tests/integration/... -tags integration -v

# Avec services locaux (ports selon make infra-up)
export DATABASE_URL="postgresql://veza:password@localhost:15432/veza?sslmode=disable"
export REDIS_ADDR="localhost:16379"
go test ./tests/integration/... -tags integration -v

Tests Spécifiques

# Test upload async polling
go test ./tests/integration -tags integration -run TestUploadAsyncPollingStatus -v

# Test upload scalability
go test ./tests/integration -tags integration -run TestUploadScalability -v

Via Makefile

# Tous les tests d'intégration
make test-integration

# Tests avec quarantaine (validation manuelle)
make test-quarantine

Structure des Tests

Tests Non-Quarantinés

Ces tests doivent toujours passer avant production:

  • TestUploadAsyncPollingStatus - Upload async avec polling status
  • TestUploadScalability - Upload distribué avec Redis

Tests Quarantinés

Voir QUARANTINE.md pour la classification complète.

Classification:

  • 🔴 Doit passer avant prod - Bloquant pour release
  • 🟡 CI nightly - Exécuté en CI séparé, non-bloquant
  • 🟢 Manual only - Exécution manuelle uniquement

Dépannage

Erreur: "Docker not running"

Solution: Démarrer Docker

# Linux
sudo systemctl start docker

# macOS
open -a Docker

Erreur: "PostgreSQL container failed to start"

Solution: Vérifier logs testcontainers

docker ps -a | grep postgres
docker logs <container_id>

Cause commune: Port 5432 déjà utilisé

# Vérifier processus utilisant le port
lsof -i :5432

# Arrêter processus ou changer port dans test

Erreur: "Redis not available"

Solution 1: Utiliser testcontainers (recommandé)

  • Les tests utilisent automatiquement testcontainers si disponible

Solution 2: Démarrer Redis localement

redis-server
# ou
docker run -d -p 6379:6379 redis:7-alpine

Test Flaky (intermittent)

Actions:

  1. Vérifier logs testcontainers pour timeouts
  2. Augmenter timeouts dans setup si nécessaire
  3. Vérifier ressources système (CPU, mémoire)
  4. Documenter dans QUARANTINE.md si non-résolvable

Tests internal/testutils et SetupTestDB()

Les tests dans internal/testutils qui utilisent SetupTestDB() (db_cleanup_test, db_test, fixtures_test) nécessitent Docker et testcontainers (PostgreSQL). En l'absence de Docker ou avec -short:

  • Ces tests sont automatiquement skippés via SkipIfDockerUnavailable(t)
  • Pour exécuter tous les tests sans Docker: go test ./... -short (les tests testutils seront skippés)
  • Pour exécuter les tests testutils: s'assurer que Docker est démarré et ne pas utiliser -short

Variables d'Environnement

Pour Tests avec Services Locaux

Variable Description Défaut
DATABASE_URL PostgreSQL connection string postgresql://veza:veza@localhost:5432/veza_test?sslmode=disable
REDIS_ADDR Redis address localhost:6379
RABBITMQ_URL RabbitMQ connection string amqp://guest:guest@localhost:5672/
SKIP_TESTCONTAINERS Forcer services locaux (si true) false

Pour Tests avec Testcontainers

Aucune variable requise - testcontainers démarre automatiquement les services.


CI/CD

Pipeline Normal

- name: Run unit tests
  run: go test ./internal/... -short -tags '!integration'

Pipeline Intégration (Séparé)

- name: Run integration tests
  run: go test ./tests/integration/... -tags integration -v
  services:
    docker:
      image: docker:latest

Note: Les tests d'intégration peuvent être exécutés:

  • En CI nightly (tous les tests)
  • En CI sur demande (workflow_dispatch)
  • En CI sur PR (tests non-quarantinés uniquement)

Bonnes Pratiques

1. Isolation des Tests

  • Chaque test utilise sa propre base de données (via testcontainers)
  • Nettoyage automatique après chaque test
  • Pas de dépendances entre tests

2. Reproductibilité

  • Utiliser testcontainers pour environnement identique
  • Éviter les timeouts courts (< 1s)
  • Éviter les sleeps fixes (utiliser polling avec timeout)

3. Performance

  • Tests parallèles quand possible (t.Parallel())
  • Timeouts raisonnables (5-10s max par test)
  • Nettoyer ressources (containers, fichiers) après test

4. Fiabilité

  • Tests non-flaky (passent 100% du temps)
  • Messages d'erreur clairs
  • Skip si dépendances manquantes (avec message)

Exemples

Exemple 1: Test avec Testcontainers

func TestMyIntegration(t *testing.T) {
    ctx := context.Background()
    
    // PostgreSQL via testcontainers
    dsn, err := testutils.GetTestContainerDB(ctx)
    require.NoError(t, err)
    
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    require.NoError(t, err)
    
    // Test...
}

Exemple 2: Test avec Services Locaux

func TestMyIntegration(t *testing.T) {
    dsn := os.Getenv("DATABASE_URL")
    if dsn == "" {
        t.Skip("DATABASE_URL not set, skipping integration test")
    }
    
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    require.NoError(t, err)
    
    // Test...
}

Références


Dernière mise à jour: 2025-12-15
Maintenu par: Veza Backend Team