Repository navigation
PR D: Phase 2.0 integration test — auth-code flow + introspection + token-response augmentation end-to-end (closes #122 Step 6) #148
Description
Summary
The literal acceptance criterion of #122 Step 6 ("one integration test that boots both modules, mints a real token via the authorization code flow, calls a data-custodian endpoint, and asserts the principal carries subscription_id") has not been written. The work shipped across PRs B1 → B2 → C1 → C2a → C2b → C3 → C4 enabled this test; PR D writes it. Closes #122 cleanly and gives Phase 2 (#119) a concrete fence-post to build on.
Filed as a tracking issue per project lead. Implementation queued until DC API completion work is in flight or AS attention is available.
Goal
One automated integration test that boots AS + DC in a real Spring context, programmatically completes the OAuth2 authorization-code flow (bypassing customer UI), exchanges the code at /oauth2/token, asserts the response and introspection both carry C4's canonical URI claims, and uses the access token against a DC resource endpoint.
What "real" means
- AS and DC both fully bean-wired (not mocked).
- The back-channel call (AS → DC
/internal/backchannel/v1/subscriptions) actually goes over HTTP. - The opaque-token introspection call (DC → AS
/oauth2/introspect) actually goes over HTTP. - Customer UI (login form, Authorization Screen) is bypassed by pre-seeding
OAuth2AuthorizationConsent+ the C4.1GrantContext— equivalent to what the customer would have produced if walked through the flow.
Sub-tasks
D.1 — Test harness scaffolding (2-3h)
- New test class:
openespi-datacustodian/src/test/java/.../integration/AuthCodeFlowEndToEndIntegrationTest.java - Single-JVM, dual-Spring-context approach:
@SpringBootTest(classes = DataCustodianApplication.class, webEnvironment = RANDOM_PORT)- AS booted programmatically in
@BeforeAllvianew SpringApplicationBuilder(AuthorizationServerApplication.class).run(...)on a separate random port @DynamicPropertySourceinjects AS's chosen port into DC'sspring.security.oauth2.resourceserver.opaquetoken.introspection-uri, and vice versa for AS'sespi.backchannel.endpoint
- TestContainers MySQL: single container with
datacustodian+oauth2_authserverschemas pre-created via init SQL
D.2 — Auth-flow simulation (2-3h)
Pre-seed via direct repository / service calls (not HTTP):
- DC:
RetailCustomerEntitywith known username and numeric id - AS:
OAuth2AuthorizationConsentfor(third_party_client_id, customer_principal_name, "FB_1;FB_4_5") - AS:
GrantContextfor that customer's session via the C4.1 session store
Then exercise the flow:
- Build MockMvc / TestRestTemplate for AS, manually inject
SecurityContextwith the customer'sAuthentication - Trigger
GET /oauth2/authorize?response_type=code&client_id=third_party&redirect_uri=...&scope=FB_1;FB_4_5&state=test - Capture the
codefrom the redirect'sLocationheader
D.3 — Token mint + assertions (1-2h)
POST /oauth2/tokenwithgrant_type=authorization_code+ the code +third_partyBasic auth → assert 200 + response JSON containsaccess_token,token_type=Bearer,scope,resourceURI,authorizationURI,customerResourceURI(C4's additionalParameters)POST /oauth2/introspectwith the access token + Basic auth (resource-server client) → assert claims include the same three URIs +authorization_id+resource_subscription_id+customer_subscription_id(or whichever subset matches the granted scope)GET /espi/1_1/resource/Authorization/{id}on DC withAuthorization: Bearer <token>→ assert 200 (or whatever the canonical "use the token" smoke is)
D.4 — Negative paths (optional, defer if scope tightens)
- Back-channel down at token mint → asserts
error=server_error - Code replay (second
/tokencall with same code) → assertserror=invalid_grant - Token used after revocation → asserts
error=invalid_tokenon resource endpoint
D.5 — CI integration (1h)
- Verify the test class is picked up by Failsafe (likely needs
*IntegrationTestnaming) - Verify TestContainers Docker is available in CI runner (already is for other modules)
- Add to
mvn verify -Pintegration-testsinvocation inci.ymlif not already covered
Effort estimate
1 working day end-to-end, possibly stretching to 1.5 if dual-context coexistence in one JVM is finicky. D.4 is optional polish.
Open design questions
-
subscription_idsemantics. Phase 2.0 — Stand up auth-server in dev with opaque tokens + subscription-bound introspection (Phase 2 precursor) #122 Step 6 says "asserts the principal carriessubscription_id." PR C4 producesresource_subscription_idandcustomer_subscription_id(granular per ESPI Function Block grant). Do we add a single combinedsubscription_idclaim for the original intent, or is the granular form acceptable per Phase 2's consumer (Remediation: ESPI REST controller modernization (post-#116) #119)? -
Test location. New test in
openespi-datacustodian/src/test/java/.../integration/(lives with DC where the resource server is) or a new top-level test module (overkill for one class)? -
Dual-context strategy. Single JVM with both Spring contexts (faster, simpler to write) vs. two JVMs (closer to production, more reliable but harder to set up). Recommendation: single JVM unless we hit context-conflict pain.
-
Database isolation. Single TestContainers MySQL with both schemas vs. two containers. Recommendation: single.
-
Which DC resource endpoint does the test exercise after token mint? Need a pre-existing canonical "happy path" endpoint that works on the current DC. Candidate:
/espi/1_1/resource/Authorization/{id}since C4 surfaces the canonical Authorization URI.
Risks
- Dual-context port juggling could be flaky. Mitigation: use Spring's
@DynamicPropertySourcecarefully, validate both apps are listening before issuing requests. OAuth2AuthorizationConsentpre-seeding doesn't exercise all of C4.1's enricher logic since we're skipping the C3/continuepath. Acceptable tradeoff — that's covered by unit tests inGrantContextEnrichingAuthorizationServiceTest.- MockMvc vs. real HTTP for AS calls. The back-channel + introspection MUST be real HTTP (those are the inter-module calls). The customer-side auth-code flow CAN be MockMvc since it's all within AS's process — simpler and faster.
- The pre-existing H2 DATETIME issue doesn't affect this test (using MySQL via TestContainers).
Out of scope for this issue
- Customer-UI walkthrough — C2a/C2b/C3 are validated by their own unit tests + the deferred browser E2E in TP bring-up: missing JAXB Atom marshaller, narrow component scan, and broader OAuth2-client wiring gaps #146.
- Refresh-token flow.
- Token revocation flow.
- Multiple-customer / multiple-grant scenarios.
- The
Phase 2subscription-scoping work in Remediation: ESPI REST controller modernization (post-#116) #119 — that's the next consumer of this test's contract.
Acceptance
- Test passes locally and in CI
-
/oauth2/tokenresponse JSON hasresourceURI,authorizationURI,customerResourceURI -
/oauth2/introspectresponse includes the same URI claims + subscription IDs - DC accepts the access token on a resource endpoint
- Test runs in <60s wall-clock (TestContainers spin-up acceptable)
- Test is robust enough that flaky-runs don't get green-merged accidentally
- Phase 2.0 — Stand up auth-server in dev with opaque tokens + subscription-bound introspection (Phase 2 precursor) #122 is closeable (this is its missing acceptance test)
Dependencies
- PR feat(#122 PR C4): AS-side back-channel call + token-response augmentation #147 (PR C4) merged — provides the token-response augmentation this test asserts on
- DC has a working resource endpoint to call — see open question Complete Spring Boot 3.5 Migration with Legacy Code Removal and Data Security #5
- Optional: design alignment on
subscription_idclaim shape — see open question Bump commons-io:commons-io from 2.4 to 2.14.0 in /openespi-datacustodian #1
Related
- Phase 2.0 — Stand up auth-server in dev with opaque tokens + subscription-bound introspection (Phase 2 precursor) #122 — the umbrella Phase 2.0 issue this closes
- Remediation: ESPI REST controller modernization (post-#116) #119 — Phase 2 subscription scoping, depends on this test's contract
- Complete #123 schema-repair sync: align AS V1 schema with application code column names #145 — AS V1 schema repair, independent
- TP bring-up: missing JAXB Atom marshaller, narrow component scan, and broader OAuth2-client wiring gaps #146 — TP bring-up, independent (browser E2E is TP's territory)
Activity
dfcoffin commentedon Jun 3, 2026
Update — implementation attempted, blocked on pre-existing test-infra issue
Attempted Option A (single-context AS test with
@MockitoBean DataCustodianBackchannelClient+ MockMvc-driven auth-code flow). Encountered a pre-existing blocker: the AS integration test infrastructure is broken at the base level —OAuth2FlowIntegrationTest(23 tests) errors out 23/23, and any new@SpringBootTest-based AS integration test fails identically (placeholder driver class inapplication-testcontainers.yml, H2 DATETIME column-type rejections inapplication-test.yml, Postgres-specificschemas: publicsetting incompatible with MySQL TestContainers, possibly more).Filed #149 to repair the AS integration test base. PR D (this issue) blocks on #149.
Filed #150 — cross-service contract test as the senior-architect-recommended pattern for the AS↔DC wire contract validation that "two contexts in one JVM" was trying (badly) to accomplish.
Revised order of operations to close #122
What's NOT in this PR
The investigation attempt in this session did NOT produce shippable code. The test class was drafted but reverted because it can't be made to pass against the broken integration test base. No changes committed to the C4 branch / main.
Effort revision
Original estimate: ~1 working day. Revised: 1 day for the test itself (#148) on a working foundation, but blocked by 2-3 days of test-infrastructure repair work (#149) that must come first.
fix(#149): repair AS testcontainers integration test infrastructure (#…
feat(#148): Phase 2.0 auth-code-flow orchestration integration test (c…
fix(#154): reconcile default ESPI clients to code definition on start…
fix(#160): correct AS↔DC token/introspection wire format to the ESPI …