OctoWiki

Vendor Adapter Tests

The build-tagged test layer colocated with the vendor adapters (services/external_vendors/**) — the unit/contract/soak/sandbox/fuzz matrix, the per-vendor coverage table, how contract tests bind to mocky-balboa and drive chaos, the two goleak soak tests, and the concrete gaps (TRS/GH/iRewardify untested, SEAGM money-mapping fixed-but-unverified, the triple FailureInfo declaration, the untested Svix verifier).

The test layer below the orchestration suites — 26 build-tagged files colocated with the vendor adapters under services/external_vendors/ (~7,100 LOC, 8 packages). These exercise a single adapter method (CreateOrder, GetOrderStatus, mappers) rather than a full DB-backed order. Companion to the Test Catalog; the mock they hit is mocky-balboa.

Build-tag matrix

KindBuild tagRuns againstIn CI?
Unit / mappernonepure in-process / httptest — no network, no DB✅ default matrix
Contract//go:build contractreal HTTP to mocky-balboa (seeded)vendor-contract-tests job
Soak//go:build !sandbox (+ goleak TestMain)octopusfake, concurrent load + goleak✅ default build (-short skips body)
Sandbox//go:build sandboxthe real Octopus sandbox❌ manual/opt-in only
Fuzznone (FuzzXxx)pure parser fuzzingcorpus in matrix; -fuzz manual

CI's vendor-contract-tests job runs go test -race -tags=contract -timeout=10m ./services/external_vendors/... with MOCKY_BASE_URL=https://mocky-balboa.octopuscards.io, parsing per-vendor --- PASS: Test<V>_Contract lines. Sandbox tags are never run in CI. The !sandbox soak gate is deliberate — the live-sandbox test opens a keep-alive connection the test can't close, which would trip goleak.VerifyTestMain; scoping goleak to !sandbox keeps the leak check on the deterministic in-process tests only.

Per-vendor coverage

VendorAdapterTest filesKinds
Runavouchers/runa_vendor.goruna_contract_test.gocontract
NeoCurrencyvouchers/neo_vendor.goneo_contract_test.go, neo_chaos_contract_test.gocontract (deepest suite)
Wupex (voucher)vouchers/wupex_vendor.gowupex_vendor_test.go, wupex_contract_test.gounit + contract
OCTO (self-voucher)vouchers/octopus_vendor.gooctopus_vendor_test.go, octopus_sandbox_test.gounit + sandbox
EpinForcevouchers/epinforce_vendor.goepinforce_contract_test.gocontract
iRewardifyvouchers/irewardify_vendor.go (1101 L)none
TRSvouchers/trs_vendor.go (879 L)none
Grasshopper (GH)vouchers/grasshopper_vendor.go (616 L)none
DTOne (topup)direct_topup/dtone/dtone_contract_test.go, error_messages_test.gocontract + mapper
SEAGMdirect_topup/seagm/seagm_contract_test.go, error_messages_test.gocontract + mapper
Wupex (topup)direct_topup/wupex/error_messages_test.gomapper only — no contract
OCTO_TOPUP (self)direct_topup/octopus/vendor + orders + soak + sandbox + fuzzrichest self-vendor
DTOne eSIMesim/dtone/dtone_esim_vendor_test.go, error_messages_test.gofixture-unit + mapper — no contract
OCTO_ESIM (self)esim/octopus/vendor + soak + sandbox + fuzz30-test vendor file (largest)
octopuscommonshared self-vendortoken_manager_test.go, guard_test.gounit + fuzz

Highlights. Neo is the reference: neo_chaos_contract_test.go runs the full failure matrix and asserts the classification (retryable-PENDING vs terminal-FAILED via expectRetryablePending/expectTerminalFailure), plus 401 auto-refresh and the _R1 retry-suffix-stripping idempotency hash. The OCTO self-vendors carry the deepest unit coverage — self-loop guards, token cache + 401-reauth + refresh-on-expiry, composite-id variant mapping, ErrDuplicateMerchantRef, recover-by-reference → ErrOrderNotFound, HMAC webhook verification, and concurrent-idempotent create (-race, exactly one wins). octopuscommon/token_manager_test.go proves single-flight login (50 concurrent callers → 1 login).

Contract tests, specifically

Each contract file is gated //go:build contract and points the adapter at a per-vendor mock URL via testhelpers.MockyVendorURL(vendor) (resolves $MOCKY_BASE_URL, default localhost:8788). Credentials/SKUs are hardcoded constants that must stay in lockstep with frontend/mocky-balboa/seed.sql. There's no shared per-package harness — each file has its own newXxxContractVendor(t) + a referenceCode(t) uniqueness helper (nanos suffix so reruns don't collide on the idempotency cache).

The pattern is auth → catalog → create → status/vouchers at the adapter-method level. EpinForce is the most elaborate, driving mocky's /_mock/complete|fail|auto-fulfill seams to make create→poll→complete deterministic and asserting ErrShouldReCreate on Failed/404. Chaos here uses the mockychaos client (ResetForTest then ArmForTest one-shots keyed by vendor+method+path); delay_then_fail tests assert elapsed ≥1.5s to prove the rule fired. Only the Neo suite asserts the full classification — the thinner Runa/Wupex/SEAGM/DTOne contract files mostly assert "error propagated."

Soak tests

Two goleak soak tests, self-vendor only: direct_topup/octopus/octopus_soak_test.go and esim/octopus/octopus_esim_soak_test.go — 32 workers each doing create+status over one shared client/TokenManager against octopusfake, arming one-shot 401s to force re-auth under concurrency, asserting every created order eventually delivers (created == delivered). Each TestMain calls goleak.VerifyTestMain(m) so a surviving goroutine (e.g. a TokenManager refresh timer that never stops) fails the package. They skip under -short. No third-party adapter has a soak test.

Gaps

Third-party voucher adapters with zero adapter-level tests

iRewardify (1101 L — its source even carries a // You'll need to add this to types package TODO), TRS (879 L), and Grasshopper/GH (616 L) have no unit, contract, sandbox, or mapper test. GH is exercised only indirectly as the mint stub inside the G2A orchestration suite.

  • Missing contract tests: Wupex topup (mapper only — no request-building/parsing test at all) and DTOne eSIM (fixture-unit + mapper, but no contract test — unlike DTOne topup).
  • SEAGM money-mapping — fixed in source, untested. The adapter now populates MinAmount/MaxAmount/VendorDiscount from unitPrice (seagm_vendor.go:837, with a comment explaining the earlier ~60× overcharge from using par_value). But no test asserts it — the contract GetProducts subtest only checks len(Products) != 0. The exact regression that caused the overcharge is uncovered at the adapter level.
  • The FailureInfo type is declared three timestypes/failure_info.go:13 (canonical) plus independent copies in direct_topup/dtone/error_messages.go:26 and esim/dtone/error_messages.go:23. A change to the canonical contract won't be caught by the two DTOne copies' compilers (see error-codes).
  • The types/ package has no test of its ownfailure_codes/failure_info/common and the sentinels (ErrShouldReCreate, ErrDuplicateMerchantRef, ErrOrderNotFound) are exercised only indirectly through the per-vendor error_messages_test.go files.
  • manager.go and the three factories have no unit tests — the factory is exercised only via the manual sandbox-tagged OCTO test.
  • payouts/, webhooks/ (incl. svix_verifier.go), and vendorhttp/ have no test files at all — the Svix webhook verifier is untested; only the OCTO self-vendors' inline HMAC-SHA256 verification is covered.
  • No testdata/ anywhere under external_vendors/ — all fixtures are inline string literals; contract fixtures live remotely in mocky's seed.sql.

Key files

  • Contract: services/external_vendors/{vouchers,direct_topup/{dtone,seagm},esim/dtone}/*_contract_test.go
  • Self-vendor: services/external_vendors/{direct_topup,esim}/octopus/*_test.go, octopuscommon/*_test.go
  • Mappers: .../{dtone,seagm,wupex}/error_messages_test.go; types services/external_vendors/types/
  • Harness: test/testhelpers/mocky.go, test/testhelpers/mockychaos/client.go

On this page