From 47948025c5652603421817b74960679f7ac08081 Mon Sep 17 00:00:00 2001 From: "@tanya_r" Date: Fri, 19 Jun 2026 09:32:41 -0300 Subject: [PATCH] test(compliance): add the end-to-end compliance lifecycle integration test Prove the compliance web layer end to end: a full-context Spring Boot test drives a case from open through claim to resolved over HTTP against real Postgres, checks that re-resolving a terminal case returns 409, that opening with only the read scope is forbidden, that the OpenAPI document lists both the KYC and case endpoints, and that the health endpoint is reachable without a token. The web layer itself (security, RFC 7807, observability, OpenAPI) shipped in earlier slices; this closes the consolidation by exercising it as a whole. Closes #270 --- .../compliance/api/ComplianceLifecycleIT.kt | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 services/compliance/src/integrationTest/kotlin/com/fincore/compliance/api/ComplianceLifecycleIT.kt diff --git a/services/compliance/src/integrationTest/kotlin/com/fincore/compliance/api/ComplianceLifecycleIT.kt b/services/compliance/src/integrationTest/kotlin/com/fincore/compliance/api/ComplianceLifecycleIT.kt new file mode 100644 index 0000000..516d8a8 --- /dev/null +++ b/services/compliance/src/integrationTest/kotlin/com/fincore/compliance/api/ComplianceLifecycleIT.kt @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: BUSL-1.1 +// SPDX-FileCopyrightText: 2026 FinCore Engine Authors + +package com.fincore.compliance.api + +import com.fincore.compliance.application.kyc.KycCheckRequest +import com.fincore.compliance.application.kyc.KycCheckResult +import com.fincore.compliance.application.kyc.KycProvider +import com.fincore.compliance.infrastructure.persistence.ComplianceCaseRepository +import com.fincore.test.containers.PostgresContainerExtension +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Import +import org.springframework.http.MediaType +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.jwt.JwtDecoder +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@SpringBootTest +@AutoConfigureMockMvc +@ExtendWith(PostgresContainerExtension::class) +@Import(ComplianceLifecycleIT.TestBeans::class) +class ComplianceLifecycleIT( + @Autowired private val mockMvc: MockMvc, + @Autowired private val cases: ComplianceCaseRepository, +) { + @TestConfiguration + class TestBeans { + @Bean fun jwtDecoder(): JwtDecoder = mockk() + + @Bean fun kycProvider(): KycProvider = + object : KycProvider { + override fun check(request: KycCheckRequest): KycCheckResult = KycCheckResult.Pending("ref-test") + } + } + + @AfterEach + fun cleanUp() { + cases.deleteAll() + } + + @Test + fun `should drive a case from open through claim to resolved over http`() { + val location = openCase() + + mockMvc + .perform(post("$location/claim").with(jwt().authorities(SimpleGrantedAuthority(WRITE)))) + .andExpect(status().isOk) + .andExpect(jsonPath("$.status").value("CLAIMED")) + + mockMvc + .perform(post("$location/resolve").with(jwt().authorities(SimpleGrantedAuthority(WRITE)))) + .andExpect(status().isOk) + .andExpect(jsonPath("$.status").value("RESOLVED")) + + mockMvc + .perform(get(location).with(jwt().authorities(SimpleGrantedAuthority(READ)))) + .andExpect(status().isOk) + .andExpect(jsonPath("$.status").value("RESOLVED")) + } + + @Test + fun `should return 409 when resolving a case twice`() { + val location = openCase() + mockMvc + .perform(post("$location/claim").with(jwt().authorities(SimpleGrantedAuthority(WRITE)))) + .andExpect(status().isOk) + mockMvc + .perform(post("$location/resolve").with(jwt().authorities(SimpleGrantedAuthority(WRITE)))) + .andExpect(status().isOk) + + mockMvc + .perform(post("$location/resolve").with(jwt().authorities(SimpleGrantedAuthority(WRITE)))) + .andExpect(status().isConflict) + } + + @Test + fun `should return 403 when opening a case with only the read scope`() { + mockMvc + .perform( + post("/v1/compliance/cases") + .with(jwt().authorities(SimpleGrantedAuthority(READ))) + .contentType(MediaType.APPLICATION_JSON) + .content(VALID_BODY), + ).andExpect(status().isForbidden) + } + + @Test + fun `should publish both kyc and compliance case paths in the openapi document`() { + mockMvc + .perform(get("/v3/api-docs")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.paths['/v1/kyc/sessions']").exists()) + .andExpect(jsonPath("$.paths['/v1/compliance/cases']").exists()) + } + + @Test + fun `should expose the health endpoint without a token`() { + mockMvc.perform(get("/actuator/health")).andExpect(status().isOk) + } + + private fun openCase(): String = + mockMvc + .perform( + post("/v1/compliance/cases") + .with(jwt().authorities(SimpleGrantedAuthority(WRITE))) + .contentType(MediaType.APPLICATION_JSON) + .content(VALID_BODY), + ).andExpect(status().isCreated) + .andReturn() + .response + .getHeader("Location")!! + + companion object { + private const val READ = "SCOPE_compliance:read" + private const val WRITE = "SCOPE_compliance:write" + private const val VALID_BODY = """{"reference":"case-ref-1"}""" + + @JvmStatic + @DynamicPropertySource + fun properties(registry: DynamicPropertyRegistry) { + registry.add("spring.datasource.url") { PostgresContainerExtension.jdbcUrl } + registry.add("spring.datasource.username") { PostgresContainerExtension.username } + registry.add("spring.datasource.password") { PostgresContainerExtension.password } + registry.add("spring.jpa.hibernate.ddl-auto") { "none" } + registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri") { "https://issuer.test" } + // This context does not exercise the AML consumer; keep the Kafka listener from polling an absent broker. + registry.add("spring.kafka.listener.auto-startup") { "false" } + } + } +}