Third Party API Integration Test¶
Write Go integration tests for real third-party API calls using explicit run gates, predictable safeguards, and strong contract assertions.
Scope¶
- Validate external API integration end-to-end with real config and real client.
- Keep tests opt-in by default so normal CI/unit workflows are not blocked.
- Follow the host repository's existing style — its assertion library (
testify/require,testify/assert, or stdlibt.Fatalf), its config loader, and its test-package convention. The examples below usetestify/requireandconfig.MustLoad()as one common shape; mirror what the repo already does rather than importing a new dependency. Clear skip conditions and a bounded timeout are required regardless. - Apply to any third-party API integration.
- Treat vendor examples (MCS/USS/etc.) as templates, not scope limits.
- The NON-NEGOTIABLE parts are the safety gates (run gate, prod/host/account fail-closed checks, destructive gating, bounded budget, ID/URL redaction) — not the choice of assertion or config library.
Scope Validation Gate¶
Confirm the task targets a third-party vendor API: - Third-party HTTP or gRPC API client → proceed - Internal service/handler (own HTTP server) → redirect to $api-integration-test, STOP - Pure unit test → redirect to $unit-test, STOP - Full end-to-end browser journey → out of scope, inform user, STOP
Hard stop: If the target is not a third-party vendor API, the entire remaining workflow is skipped. Output only: (1) scope verdict, (2) recommended skill or approach, (3) reason. Do NOT generate test code, do NOT proceed to subsequent gates.
Required Pattern¶
- Keep file name as
<client>_integration_test.go, in the package the repo uses for tests (same package, or its_testexternal package — match the surrounding convention). Name each integration test function with anIntegrationmarker (e.g.TestStripe_CreateCharge_Integration): the runner refuses to report success unless at least one…Integrationtest actually PASSED, which keeps a plain unit test from masquerading as a passed integration run (override the marker viaVENDOR_TEST_NAME_MATCH). - Add both build constraints at file top (for backward compatibility):
//go:build integration(Go 1.17+)// +build integration(Go <1.17 compat)- Add explicit run gate env var (example:
THIRDPARTY_INTEGRATION=1or vendor-specific gate), otherwiset.Skip(...). - Validate required runtime env vars up front (
ENV,CONFIG_DIR, the API base URL, the vendor test account, target IDs). When the run gate is set but a required var is missing/empty,t.Fatalf— do NOTt.Skip(see §Skip vs Fail (CI Integrity)). - Block production/live vendor targets by ENV and by resolved host and account (never ENV alone):
- refuse if
ENVisprod/production, OR the base-URL host is not on the explicit sandbox allowlist (VENDOR_SANDBOX_HOSTS), OR the vendor account is not a designated test account (VENDOR_TEST_ACCOUNTS) — unlessINTEGRATION_ALLOW_PROD=1. When the gate is set, this refusal ist.Fatalf, nott.Skip. UserequireVendorIntegration(§Go Implementation Baseline). - Parse and validate env var payloads:
- Always
strings.TrimSpacebefore comparison or use - Use
strconv.ParseIntfor numeric IDs - Split comma-separated lists and validate each element
- Log only non-sensitive parsed values at
t.Logf; mask identifiers withmaskIDand never log secrets/tokens (raw account/customer/tenant/target IDs are sensitive) - Load runtime config via the project's existing config loader (e.g.
config.MustLoad()— use whatever the repo already uses; do not introduce a new config mechanism for the test). - Build real third-party client with production code path.
- Use
context.WithTimeout(...)to prevent hanging requests. - Use bounded retry policy only when justified:
- default: no retry
- if enabled: max 2 retries (3 total attempts), bounded backoff, no infinite loop. Rate-limit (
429/Retry-After) retries count toward this same budget —getHonoringRateLimitis called withmaxRetries=2. - Execute real API call and assert both:
- protocol-level contract (status/code/required response fields)
- business-level invariant (identifier consistency, semantic constraints)
- For expected failure paths, assert explicit error type/code (not only
require.Error). - Define test data lifecycle explicitly:
- setup source, idempotency key strategy, cleanup or safe reuse policy.
Skip vs Fail (CI Integrity)¶
t.Skip on a misconfigured run is a false green: go test exits 0 when every test skips, so a CI job that lost its config — or points at a live vendor — passes silently.
| Situation | Behavior |
|---|---|
Run gate unset (THIRDPARTY_INTEGRATION != 1) | t.Skip (not requested) |
| Gate set, a required var missing/empty | t.Fatalf |
Gate set, target is prod/live (ENV, host not on VENDOR_SANDBOX_HOSTS, or non-test account) without INTEGRATION_ALLOW_PROD=1 | t.Fatalf |
Gate set, destructive op without INTEGRATION_ALLOW_DESTRUCTIVE=1 | t.Skip (opt-in tier — keep in a separate CI job) |
Once THIRDPARTY_INTEGRATION=1 is set, the only acceptable t.Skip is a destructive/scaffold opt-in tier that the base gate does not depend on. Everything else becomes t.Fatalf.
An individual test calling t.Skip is fine, but a whole run in which every test skipped verified nothing — run_vendor_integration.sh treats that (and a -run pattern matching nothing) as a failure, not a green PASS. So point an all-destructive CI job at its own package/-run scope and give it INTEGRATION_ALLOW_DESTRUCTIVE=1; a destructive job that skips everything is a misconfiguration the runner surfaces.
Go Implementation Baseline¶
The canonical prod/account/destructive safety helpers — requireVendorIntegration, assertTestAccount, isProdVendorTarget, assertVendorDestructiveSafe, maskID, redactURL, unwrapURLErr, parseRetryAfter, the callBudget/budgetTransport cost guards, getHonoringRateLimit, and the gRPC counterparts grpcTargetHost / isProdGRPCTarget / redactGRPCTarget / requireVendorGRPCIntegration / assertVendorGRPCDestructiveSafe — live in: - references/go-baseline.md
Copy them into the test package as written. They fail CLOSED (a sandbox host must be on VENDOR_SANDBOX_HOSTS; a test account must be on VENDOR_TEST_ACCOUNTS) and are kept token-identical to the regression fixture, which is why they are a separate reference rather than inlined here — do not paraphrase the safety logic.
references/vendor-examples.md shows a full test that calls a real endpoint through the project's vendor client using these helpers plus a bounded call budget (newVendorBudget/ spend), real Retry-After handling (getHonoringRateLimit), and ID masking. gRPC vendors use isProdGRPCTarget for the sandbox check.
Configuration Gate (Mandatory)¶
Before generating or updating test code, apply the gate in: - references/common-integration-gate.md
Vendor-Specific Safety Additions (executable rules)¶
These are concrete, testable rules — not principles. A behavioral fixture exercises each.
- Sandbox host allowlist (fail closed). A vendor target is non-prod only if its host is on
VENDOR_SANDBOX_HOSTS. Unset or non-matching host → treated as production (refuse). - Test account allowlist (fail closed).
VENDOR_TEST_ACCOUNTSis required; the account/ project/tenant must be on it (assertTestAccount). A prod account likeacct_live_9is refused. - Production writes forbidden under ALL flags.
INTEGRATION_ALLOW_PROD=1permits READ-only prod tests, never writes. A destructive call against a prod/live target fails even withALLOW_PROD=1+ALLOW_DESTRUCTIVE=1(assertVendorDestructiveSafe). - Mutations require an idempotency key. Destructive/write calls must pass a non-empty
Idempotency-Key(also protects against duplicate charges on retry). - Bounded call budget (cost control). Cap real calls with
VENDOR_MAX_CALLS(default 20); route every call through a budget thatt.Fatalfs when exceeded — a runaway loop against a paid API must not silently rack up cost. - Retry-After / 429 handling. On
429, honor theRetry-Afterheader (do not hammer), and classify the failure asrate-limit, nevercontract. AssertX-RateLimit-Remaining/Retry-Afterwhen the vendor sends them. - Mask sensitive IDs and secrets. Never log tokens, API keys, or raw customer/tenant/account IDs. Mask IDs in
t.Logf/assertions (e.g.acct_1234→acct_…34); redactAuthorization. - gRPC specifics (when the vendor is gRPC): gate reads with
requireVendorGRPCIntegrationand writes withassertVendorGRPCDestructiveSafe(same rules as their HTTP counterparts). Assertstatus.Code(err)(not string matching),defer conn.Close(), and set a deadline viacontext.WithTimeouton every RPC. Map vendor codes to your domain errors at the boundary. - Retry policy bounded and explicit. Default no retry; if enabled, max 1-2 with bounded backoff, honoring
ctx.Done()— no hidden infinite retries.
Safety Rules¶
- Use dedicated test tenant/account identifiers only (validated against
VENDOR_TEST_ACCOUNTS). - Prefer idempotent or low-risk API operations.
- Never hardcode secrets/tokens in test source; never log tokens or raw customer/tenant IDs (mask them).
- Skip vs Fail:
t.Skiponly when the run gate is off (or for a destructive/scaffold opt-in tier). When the gate is set but a required var is missing, or the target is prod/non-sandbox,t.Fatalf— a silent skip false-greens CI. See §Skip vs Fail (CI Integrity). - Keep timeout strict (for example 10-30s) and avoid unbounded retries.
- Refuse production/live targets (by ENV, host, and account) unless explicit override is provided.
Execution¶
Use vendor-specific run commands documented in: - references/vendor-examples.md
Tool-permission note. A strict Bash(go test*) allowlist matches only a command whose FIRST token is go test — an inline env-prefixed form (THIRDPARTY_INTEGRATION=1 … go test …) starts with the variable, not go. And separate Bash invocations do not carry env vars forward, so an "export … then go test" recipe is unreliable under a strict tool runtime. Use the single-invocation wrapper scripts/run_vendor_integration.sh <env-file> <pkg> (allowlisted as Bash(bash scripts/run_vendor_integration.sh*)): it parses a gitignored env file as KEY=VALUE data (it never sources it — sourcing would execute shell hidden in the file), validates the gate/sandbox/account vars, refuses a prod target, and fixes -tags=integration -count=1 -timeout=<bounded> -p=<n> -parallel=<n> -v. Extra args are limited to a strict allowlist (-run/-skip/-shuffle + value, -v/-race/-short/-failfast), so neither -count/-timeout/-tags/-p/-parallel nor their -test.count/-test.timeout binary-flag forms, nor -args, can slip through; a package starting with - is refused; the timeout is clamped to [1s, 3600s]; suite concurrency defaults to serial (VENDOR_TEST_PARALLELISM, capped [1,4], so a large fan-out can't exceed the per-test cost budget); and a green run is reported as failure unless at least one test whose name contains the marker (VENDOR_TEST_NAME_MATCH, default Integration) actually PASSED — so an all-skip run (e.g. a destructive tier without INTEGRATION_ALLOW_DESTRUCTIVE=1) or a bad -run pattern never false-greens. Run it from the target repository root (so a relative package like ./internal/... resolves against that repo), invoking it by an absolute path to the installed skill. Calling it by absolute path won't match the relative allowlist entry above — add one for the install path (Bash(bash /path/to/skill/scripts/run_vendor_integration.sh*)). The regression suite under scripts/tests/ is maintainer tooling (python3 -m unittest), intentionally outside this skill's runtime allowlist.
Output Contract¶
Use the shared output contract in: - references/common-output-contract.md
References (Load Selectively)¶
- Always read:
references/common-integration-gate.md— gate design, resolved-host + account validation, degradation levels, skip-vs-fail. Parallel to$api-integration-test's gate (same safety bar, plus vendor rules) — it is a separate file, not shared; edits do not propagate between skills. - Always read:
references/common-output-contract.md— structured report format. Parallel to$api-integration-test's — a separate file, not shared. - Always read when writing test code:
references/go-baseline.md— the canonical Go safety helpers; copy them into the test package verbatim (kept token-identical to the regression fixture). - Read if authoring new tests or triaging failures:
references/checklists.md - Read if no vendor pattern exists in the repo:
references/vendor-examples.md— generic template and run commands