Repository navigation
Cross-service contract tests for the AS↔DC back-channel (lightweight shared-fixture, no Spring Cloud) #150
Description
Summary
Filed per the senior-architect review during PR D scoping (issue #148): the proper validation pattern for "do AS and DC actually agree on the back-channel JSON wire format at runtime" is a consumer-driven contract test (Spring Cloud Contract or Pact), NOT "two Spring contexts in one JVM" and NOT an in-process mock of either side.
Already-shipped coverage establishes the foundation:
DataCustodianBackchannelClientTest(PR C4) — AS as consumer, validates the wire shape against aMockRestServiceServerstubbed JSON contractSubscriptionProvisioningControllerTest(PR B2) — DC as provider, validates the controller's request/response handling
What's missing is a shared contract artifact that both sides bind to, with a CI-enforced compatibility check.
Why contract testing is the right pattern
For independently-deployed Spring Boot services that communicate over HTTP, contract tests are the industry-standard pattern because they:
- Catch breaking changes when either side updates their DTO without the other knowing
- Run fast (no Spring context startup, no Docker, no real network)
- Are honest about what they cover (the contract; not customer UX, not production wiring, not network layers)
- Scale (adding a new endpoint requires one contract, not a cross-service integration test setup)
The "two Spring contexts in one JVM" alternative considered in #148 was rejected as non-idiomatic and brittle — it pretends to validate the cross-service flow but doesn't catch the failures that actually happen in production (proxy timeouts, CORS, TLS, ingress config).
Scope
Two interacting wire contracts to formalize:
Contract 1: AS → DC /internal/backchannel/v1/subscriptions
- Request:
BackchannelRequest(correlation_id, client_id, granted_scope, retail_customer_id, selected_usage_point_ids, customer_resource_uri) - Response 201:
BackchannelResponse(authorization_id, resource_subscription_id, customer_subscription_id, resource_uri, authorization_uri, customer_resource_uri) - Response 400/500: error envelope shape
Contract 2: DC → AS /oauth2/introspect
- Request: standard RFC 7662 (token + Basic auth)
- Response 200: standard RFC 7662 + ESPI-specific claims (
resourceURI,authorizationURI,customerResourceURI,authorization_id,resource_subscription_id,customer_subscription_id) per PR C4's customizer
Recommended approach
Spring Cloud Contract is the natural fit for a Spring Boot 4 monorepo:
- Contracts live in the producer module's
src/test/resources/contracts/ - Producer module (DC for Bump commons-io:commons-io from 2.4 to 2.14.0 in /openespi-datacustodian #1, AS for Bump org.postgresql:postgresql from 42.7.4 to 42.7.7 in /openespi-datacustodian/OpenESPI-GreenButton-Workspace/OpenESPI-DataCustodian-java #2) verifies contracts via auto-generated tests during its own
mvn test - Consumer module (AS for Bump commons-io:commons-io from 2.4 to 2.14.0 in /openespi-datacustodian #1, DC for Bump org.postgresql:postgresql from 42.7.4 to 42.7.7 in /openespi-datacustodian/OpenESPI-GreenButton-Workspace/OpenESPI-DataCustodian-java #2) gets generated stubs via
@AutoConfigureStubRunnerfor its own integration tests - Contract changes can break either side's build immediately — true CI-enforced compatibility
Pact is a viable alternative but adds a Pact Broker deployment burden the project doesn't currently need.
Sub-tasks
- Add
spring-cloud-contract-verifierto AS and DC parent POM - Author Contract 1 in DC's
src/test/resources/contracts/— Groovy DSL files for the happy path + 400 + 500 cases - Author Contract 2 in AS's
src/test/resources/contracts/— happy path + non-existent-token (active=false) cases - AS consumer-side stubs — replace
MockRestServiceServerinDataCustodianBackchannelClientTestwith@AutoConfigureStubRunner(cleaner; the stub is generated from DC's contract) - DC consumer-side stubs — wire AS's introspection contract into DC's resource-server tests
- CI enforcement — both modules'
mvn verifyfails if their contracts diverge from the published wire format - Documentation — README section explaining the contract test pattern for future endpoints
Acceptance
- Both wire contracts are formalized as machine-checkable artifacts
- AS and DC builds verify their respective contracts on every CI run
- Adding a new field to either DTO requires updating the contract (which fails CI until both producer + consumer agree)
- PR D's
DataCustodianBackchannelClientTestis migrated to use the generated stubs - Documentation exists for adding new cross-service contracts
Out of scope
- Browser end-to-end testing (deferred to TP bring-up: missing JAXB Atom marshaller, narrow component scan, and broader OAuth2-client wiring gaps #146 territory)
- Production smoke tests (deferred to whenever staging exists)
- The Phase 2.0 orchestration test (PR D: Phase 2.0 integration test — auth-code flow + introspection + token-response augmentation end-to-end (closes #122 Step 6) #148) — different concern; this issue is the cross-service-contract layer
Effort estimate
2-3 working days for someone familiar with Spring Cloud Contract. First-timers should expect 4-5 days including learning curve.
Related
- Phase 2.0 — Stand up auth-server in dev with opaque tokens + subscription-bound introspection (Phase 2 precursor) #122 — Phase 2.0 umbrella; this issue + PR D: Phase 2.0 integration test — auth-code flow + introspection + token-response augmentation end-to-end (closes #122 Step 6) #148 + Repair AS integration test infrastructure (pre-existing pre-#122) #149 collectively close it once delivered
- PR D: Phase 2.0 integration test — auth-code flow + introspection + token-response augmentation end-to-end (closes #122 Step 6) #148 — PR D Phase 2.0 integration test (the in-process orchestration test)
- Repair AS integration test infrastructure (pre-existing pre-#122) #149 — Repair AS integration test infrastructure (blocks PR D: Phase 2.0 integration test — auth-code flow + introspection + token-response augmentation end-to-end (closes #122 Step 6) #148)
- PR feat(#122 PR C4): AS-side back-channel call + token-response augmentation #147 (PR C4) — back-channel client + customizer + success handler; the producer/consumer side of Contract 1
Activity
fix(#160): correct AS↔DC token/introspection wire format to the ESPI …
chore(#160): introspection-claim verification + remove vestigial cust…
feat(#158): DC→TP notification contract + TP-receiver stub (#163)
[-]Cross-service contract test between AS and DC's back-channel (Spring Cloud Contract / Pact)[/-][+]Cross-service contract tests for the AS↔DC back-channel (lightweight shared-fixture, no Spring Cloud)[/+]dfcoffin commentedon Jun 5, 2026
Scope/approach correction (supersedes the original "Spring Cloud Contract / Pact" framing).
This issue predates two later decisions, so the title and parts of the body are stale:
No Spring Cloud Contract / Pact. Per the standing architecture constraint (the AS, DC, and TP are deployed standalone on EC2 — not a Spring Cloud mesh), we use the repo's lightweight, framework-free consumer-driven contract pattern instead: a shared fixture under repo-root
contracts/that both sides bind to in their own fast unit tests (a producer test asserts the service emits the shape; a consumer test asserts it parses the shape). This is the same pattern already used by DC scope enforcement vocabulary doesn't match AS-issued ESPI FB scopes — customer tokens 403 on all resources #157, DC→TP notification contract test + TP-receiver stub #158, and AS↔DC token/introspection wire format diverges from ESPI standard (Batch URI forms + non-standard *_id claims) #160. Retitled accordingly.Contract 2 (introspection / token-response) is already delivered by AS↔DC token/introspection wire format diverges from ESPI standard (Batch URI forms + non-standard *_id claims) #160 —
IntrospectionWireContractTestpins it againstcontracts/token-response-*.json. Note theauthorization_id/resource_subscription_id/customer_subscription_idclaims this issue listed for the introspection body were removed in AS↔DC token/introspection wire format diverges from ESPI standard (Batch URI forms + non-standard *_id claims) #160 (they were non-standard); the ESPI introspection/token body carries only the three*URIfields. Those*_idfields legitimately remain on the internal back-channel response (DC→AS), which is what Contract 1 covers.Remaining work = Contract 1 only: the AS→DC back-channel
POST /internal/backchannel/v1/subscriptionsrequest + 201 response. Delivered as:contracts/backchannel-subscription-request.jsonandcontracts/backchannel-subscription-response.jsonEspiBatchUri, scope toEspiScope)DataCustodianBackchannelClientTesthappy-path migrated off its inline JSON onto the shared fixtures)Out: the Spring Cloud Contract sub-tasks (verifier dependency, Groovy DSL, stub-runner) — not applicable.
feat(#150): AS↔DC back-channel request/response wire contract (#165)
chore(#119): defer ESPI resource CRUD writes — delete write/admin con…