Skip to content

Commit 64e679b

Browse files
refactor(infra): enforce coding and testing standards across all services (STA-243) (#269)
* fix(infra): resolve bean conflicts, healthchecks, and build issues (STA-243) - Add @ConditionalOnMissingBean to fallback adapters in S2, S3, S4, S5, S6 so real provider beans take precedence in sandbox profile - Fix USDC decimal scaling in DevCustodyAdapter (truncate, not throw) - Replace wget Docker healthchecks with TCP checks, fix Temporal command - Disable JaCoCo (incompatible with Java 25) - Exclude @tag(sandbox) tests from default test runs - Widen recipient_account_hash column for full SHA-256 digest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(infra): enforce coding and testing standards across all services (STA-243) - Remove all Javadoc and TODO comments from 300+ production files - Move 11 config classes to correct packages (application/config or infrastructure/config) per hexagonal architecture standard - Extract IsolatedTransactionExecutor port to remove Spring transaction infrastructure imports from domain layer (S4, S5) - Move TASK_QUEUE constant from application to domain layer (S1) - Replace DataIntegrityViolationException with RuntimeException in domain handler (S1) - Convert non-BDD Mockito (when/verify) to BDD style (given/then) across 8 test files - Replace generic argument matchers (any, anyString, eq) with actual values in test stubs and verifications Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(infra): remove remaining section divider comments (STA-243) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(s13): restore permitted auth/public paths in SecurityConfig (STA-243) The comment-removal pass in 35a3a18 accidentally collapsed six requestMatchers(...).permitAll() lines into a single nonsense path /actuatorauth/**, causing /v1/auth/**, /v1/invitations/**, /actuator/**, Swagger, and /.well-known/** to all require JWT auth. Four merchant-iam business tests failed as a result because they could no longer log in. Restore the original permit list so business tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d6bc7c commit 64e679b

335 files changed

Lines changed: 281 additions & 3444 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api-gateway-iam/api-gateway-iam/src/main/java/com/stablecoin/payments/gateway/iam/application/config/IdempotencyKeyFilter.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,6 @@
3131
import java.util.HexFormat;
3232
import java.util.Set;
3333

34-
/**
35-
* Enforces presence of {@code Idempotency-Key} header on state-mutating endpoints
36-
* (POST, PATCH, DELETE) -- excluding auth, JWKS, and actuator endpoints.
37-
* Uses INSERT-first reservation pattern to prevent TOCTOU races:
38-
* 1. Try INSERT with status_code=0 (reservation)
39-
* 2. If INSERT succeeds, proceed with request and UPDATE with real response
40-
* 3. If INSERT fails (duplicate), re-read stored record for replay or conflict
41-
*/
4234
@Slf4j
4335
@Component
4436
@Order(2)
@@ -242,10 +234,6 @@ private String computeSha256(byte[] body) {
242234

243235
record IdempotencyRecord(String idempotencyKey, String requestHash, String responseBody, int statusCode) {}
244236

245-
/**
246-
* Request wrapper that replays a cached body so downstream filters and
247-
* controllers can read the request body after it has already been consumed.
248-
*/
249237
private static class CachedBodyRequestWrapper extends HttpServletRequestWrapper {
250238

251239
private final byte[] body;

api-gateway-iam/api-gateway-iam/src/main/java/com/stablecoin/payments/gateway/iam/application/security/MerchantScopeEnforcer.java

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,13 @@
1212

1313
import java.util.UUID;
1414

15-
/**
16-
* Extracts the authenticated principal's merchant ID from the SecurityContext
17-
* and enforces that it matches the target merchant ID.
18-
* Used via {@code @PreAuthorize("@merchantScopeEnforcer.hasAccess(#merchantId)")}.
19-
*/
2015
@Component
2116
@RequiredArgsConstructor
2217
public class MerchantScopeEnforcer {
2318

2419
private final ApiKeyRepository apiKeyRepository;
2520
private final AccessTokenRepository accessTokenRepository;
2621

27-
/**
28-
* Checks whether the authenticated principal has access to the given merchant.
29-
*
30-
* @return true if the principal's merchant ID matches the target
31-
* @throws MerchantAccessDeniedException if no merchant-scoped authentication is present or IDs don't match
32-
*/
3322
public boolean hasAccess(UUID targetMerchantId) {
3423
var principalMerchantId = authenticatedMerchantId();
3524
if (!principalMerchantId.equals(targetMerchantId)) {
@@ -38,37 +27,18 @@ public boolean hasAccess(UUID targetMerchantId) {
3827
return true;
3928
}
4029

41-
/**
42-
* Checks whether the authenticated principal owns the API key.
43-
*
44-
* @return true if the key's merchant ID matches the principal's
45-
* @throws ApiKeyNotFoundException if the key does not exist
46-
* @throws MerchantAccessDeniedException if the principal does not own the key
47-
*/
4830
public boolean hasAccessToApiKey(UUID keyId) {
4931
var apiKey = apiKeyRepository.findById(keyId)
5032
.orElseThrow(() -> ApiKeyNotFoundException.byId(keyId));
5133
return hasAccess(apiKey.getMerchantId());
5234
}
5335

54-
/**
55-
* Checks whether the authenticated principal owns the access token.
56-
*
57-
* @return true if the token's merchant ID matches the principal's
58-
* @throws TokenRevokedException if the token does not exist
59-
* @throws MerchantAccessDeniedException if the principal does not own the token
60-
*/
6136
public boolean hasAccessToToken(UUID jti) {
6237
var token = accessTokenRepository.findByJti(jti)
6338
.orElseThrow(() -> TokenRevokedException.of(jti));
6439
return hasAccess(token.getMerchantId());
6540
}
6641

67-
/**
68-
* Returns the authenticated principal's merchant ID.
69-
*
70-
* @throws MerchantAccessDeniedException if no merchant-scoped authentication is present
71-
*/
7242
public UUID authenticatedMerchantId() {
7343
var auth = SecurityContextHolder.getContext().getAuthentication();
7444
return extractMerchantId(auth);

api-gateway-iam/api-gateway-iam/src/main/java/com/stablecoin/payments/gateway/iam/application/security/UserAuthentication.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
import java.util.Objects;
88
import java.util.UUID;
99

10-
/**
11-
* Spring Security principal for S13-authenticated users accessing S10 gateway.
12-
* Distinct from {@link MerchantAuthentication} which represents merchant/client-level auth.
13-
*/
1410
public class UserAuthentication extends AbstractAuthenticationToken {
1511

1612
private final UUID userId;

api-gateway-iam/api-gateway-iam/src/main/java/com/stablecoin/payments/gateway/iam/application/security/UserJwtAuthenticationFilter.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,6 @@
2121
import java.util.List;
2222
import java.util.UUID;
2323

24-
/**
25-
* Validates S13-issued user JWTs at the S10 gateway.
26-
* Fetches S13's JWKS to verify ES256 signatures, then extracts user claims
27-
* into a {@link UserAuthentication} principal.
28-
*
29-
* <p>Runs after the S10 merchant JWT filter. If the token's issuer doesn't match
30-
* the S13 issuer, this filter passes through (the token may be an S10 merchant JWT
31-
* that was already handled or will be rejected upstream).</p>
32-
*/
3324
@Slf4j
3425
@RequiredArgsConstructor
3526
public class UserJwtAuthenticationFilter extends OncePerRequestFilter {
Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
package com.stablecoin.payments.gateway.iam.domain.port;
22

3-
import com.stablecoin.payments.gateway.iam.domain.exception.UserJwksUnavailableException;
43

5-
/**
6-
* Outbound port for fetching the JWKS (JSON Web Key Set) from the Merchant IAM service (S13).
7-
* Infrastructure decides caching strategy and failover behaviour.
8-
*/
94
public interface UserJwksProvider {
105

11-
/**
12-
* Returns the JWKS JSON from S13.
13-
*
14-
* @throws UserJwksUnavailableException if S13 is unreachable and no cached value is available
15-
*/
166
String fetchJwks();
177
}

api-gateway-iam/api-gateway-iam/src/test/java/com/stablecoin/payments/gateway/iam/application/security/ApiKeyAuthenticationFilterTest.java

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@
2626
import java.util.UUID;
2727

2828
import static org.assertj.core.api.Assertions.assertThat;
29-
import static org.mockito.ArgumentMatchers.anyString;
29+
import static org.mockito.BDDMockito.given;
30+
import static org.mockito.BDDMockito.then;
3031
import static org.mockito.Mockito.never;
31-
import static org.mockito.Mockito.verify;
32-
import static org.mockito.Mockito.when;
3332

3433
@ExtendWith(MockitoExtension.class)
3534
class ApiKeyAuthenticationFilterTest {
@@ -64,7 +63,7 @@ class WhenNoApiKeyHeader {
6463
void shouldPassThroughWithoutHeader() throws ServletException, IOException {
6564
filter.doFilterInternal(request, response, filterChain);
6665

67-
verify(filterChain).doFilter(request, response);
66+
then(filterChain).should().doFilter(request, response);
6867
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
6968
}
7069

@@ -74,8 +73,8 @@ void shouldPassThroughWithBlankHeader() throws ServletException, IOException {
7473

7574
filter.doFilterInternal(request, response, filterChain);
7675

77-
verify(filterChain).doFilter(request, response);
78-
verify(apiKeyService, never()).validate(anyString(), anyString());
76+
then(filterChain).should().doFilter(request, response);
77+
then(apiKeyService).shouldHaveNoInteractions();
7978
}
8079
}
8180

@@ -106,11 +105,11 @@ void shouldAuthenticateWithValidKey() throws ServletException, IOException {
106105
.version(0)
107106
.build();
108107

109-
when(apiKeyService.validate(rawKey, "10.0.0.1")).thenReturn(apiKey);
108+
given(apiKeyService.validate(rawKey, "10.0.0.1")).willReturn(apiKey);
110109

111110
filter.doFilterInternal(request, response, filterChain);
112111

113-
verify(filterChain).doFilter(request, response);
112+
then(filterChain).should().doFilter(request, response);
114113
var auth = SecurityContextHolder.getContext().getAuthentication();
115114
assertThat(auth).isInstanceOf(MerchantAuthentication.class);
116115
var merchantAuth = (MerchantAuthentication) auth;
@@ -127,51 +126,51 @@ class WhenInvalidApiKey {
127126
@Test
128127
void shouldRejectNotFoundKey() throws ServletException, IOException {
129128
request.addHeader("X-API-Key", "invalid_key");
130-
when(apiKeyService.validate("invalid_key", "127.0.0.1"))
131-
.thenThrow(ApiKeyNotFoundException.byHash());
129+
given(apiKeyService.validate("invalid_key", "127.0.0.1"))
130+
.willThrow(ApiKeyNotFoundException.byHash());
132131

133132
filter.doFilterInternal(request, response, filterChain);
134133

135-
verify(filterChain, never()).doFilter(request, response);
134+
then(filterChain).should(never()).doFilter(request, response);
136135
assertThat(response.getStatus()).isEqualTo(401);
137136
}
138137

139138
@Test
140139
void shouldRejectRevokedKey() throws ServletException, IOException {
141140
var keyId = UUID.randomUUID();
142141
request.addHeader("X-API-Key", "revoked_key");
143-
when(apiKeyService.validate("revoked_key", "127.0.0.1"))
144-
.thenThrow(ApiKeyRevokedException.of(keyId));
142+
given(apiKeyService.validate("revoked_key", "127.0.0.1"))
143+
.willThrow(ApiKeyRevokedException.of(keyId));
145144

146145
filter.doFilterInternal(request, response, filterChain);
147146

148-
verify(filterChain, never()).doFilter(request, response);
147+
then(filterChain).should(never()).doFilter(request, response);
149148
assertThat(response.getStatus()).isEqualTo(401);
150149
}
151150

152151
@Test
153152
void shouldRejectExpiredKey() throws ServletException, IOException {
154153
var keyId = UUID.randomUUID();
155154
request.addHeader("X-API-Key", "expired_key");
156-
when(apiKeyService.validate("expired_key", "127.0.0.1"))
157-
.thenThrow(ApiKeyExpiredException.of(keyId));
155+
given(apiKeyService.validate("expired_key", "127.0.0.1"))
156+
.willThrow(ApiKeyExpiredException.of(keyId));
158157

159158
filter.doFilterInternal(request, response, filterChain);
160159

161-
verify(filterChain, never()).doFilter(request, response);
160+
then(filterChain).should(never()).doFilter(request, response);
162161
assertThat(response.getStatus()).isEqualTo(401);
163162
}
164163

165164
@Test
166165
void shouldRejectDisallowedIp() throws ServletException, IOException {
167166
request.addHeader("X-API-Key", "valid_key");
168167
request.setRemoteAddr("192.168.1.1");
169-
when(apiKeyService.validate("valid_key", "192.168.1.1"))
170-
.thenThrow(IpNotAllowedException.of("192.168.1.1"));
168+
given(apiKeyService.validate("valid_key", "192.168.1.1"))
169+
.willThrow(IpNotAllowedException.of("192.168.1.1"));
171170

172171
filter.doFilterInternal(request, response, filterChain);
173172

174-
verify(filterChain, never()).doFilter(request, response);
173+
then(filterChain).should(never()).doFilter(request, response);
175174
assertThat(response.getStatus()).isEqualTo(401);
176175
}
177176
}
@@ -185,7 +184,7 @@ void shouldSkipWhenAlreadyAuthenticated() throws ServletException, IOException {
185184

186185
filter.doFilterInternal(request, response, filterChain);
187186

188-
verify(filterChain).doFilter(request, response);
189-
verify(apiKeyService, never()).validate(anyString(), anyString());
187+
then(filterChain).should().doFilter(request, response);
188+
then(apiKeyService).shouldHaveNoInteractions();
190189
}
191190
}

api-gateway-iam/api-gateway-iam/src/test/java/com/stablecoin/payments/gateway/iam/application/security/AuditLogFilterTest.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
import static org.assertj.core.api.Assertions.assertThat;
2424
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2525
import static org.mockito.ArgumentMatchers.any;
26-
import static org.mockito.Mockito.doThrow;
26+
import static org.mockito.BDDMockito.then;
27+
import static org.mockito.BDDMockito.willThrow;
2728
import static org.mockito.Mockito.never;
28-
import static org.mockito.Mockito.verify;
2929

3030
@ExtendWith(MockitoExtension.class)
3131
class AuditLogFilterTest {
@@ -58,14 +58,14 @@ void tearDown() {
5858
void shouldAlwaysCallFilterChain() throws ServletException, IOException {
5959
filter.doFilterInternal(request, response, filterChain);
6060

61-
verify(filterChain).doFilter(request, response);
61+
then(filterChain).should().doFilter(request, response);
6262
}
6363

6464
@Test
6565
void shouldSkipAuditWhenNotAuthenticated() throws ServletException, IOException {
6666
filter.doFilterInternal(request, response, filterChain);
6767

68-
verify(auditLogRepository, never()).save(any());
68+
then(auditLogRepository).should(never()).save(any());
6969
}
7070

7171
@Nested
@@ -88,7 +88,7 @@ void shouldPersistAuditLogEntry() throws ServletException, IOException {
8888
filter.doFilterInternal(request, response, filterChain);
8989

9090
var captor = ArgumentCaptor.forClass(AuditLogEntry.class);
91-
verify(auditLogRepository).save(captor.capture());
91+
then(auditLogRepository).should().save(captor.capture());
9292

9393
var entry = captor.getValue();
9494
assertThat(entry.getMerchantId()).isEqualTo(merchantId);
@@ -110,30 +110,30 @@ void shouldRecordJwtAuthMethod() throws ServletException, IOException {
110110
filter.doFilterInternal(request, response, filterChain);
111111

112112
var captor = ArgumentCaptor.forClass(AuditLogEntry.class);
113-
verify(auditLogRepository).save(captor.capture());
113+
then(auditLogRepository).should().save(captor.capture());
114114
assertThat(captor.getValue().getDetail()).containsEntry("auth_method", "JWT");
115115
}
116116

117117
@Test
118118
void shouldNotFailWhenRepositoryThrows() throws ServletException, IOException {
119-
doThrow(new RuntimeException("DB down"))
120-
.when(auditLogRepository).save(any());
119+
willThrow(new RuntimeException("DB down"))
120+
.given(auditLogRepository).save(any());
121121

122122
filter.doFilterInternal(request, response, filterChain);
123123

124-
verify(filterChain).doFilter(request, response);
124+
then(filterChain).should().doFilter(request, response);
125125
// No exception propagated — filter swallows it
126126
}
127127

128128
@Test
129129
void shouldAuditEvenWhenFilterChainThrows() throws ServletException, IOException {
130-
doThrow(new ServletException("downstream error"))
131-
.when(filterChain).doFilter(request, response);
130+
willThrow(new ServletException("downstream error"))
131+
.given(filterChain).doFilter(request, response);
132132

133133
assertThatThrownBy(() -> filter.doFilterInternal(request, response, filterChain))
134134
.isInstanceOf(ServletException.class);
135135

136-
verify(auditLogRepository).save(any());
136+
then(auditLogRepository).should().save(any());
137137
}
138138
}
139139
}

0 commit comments

Comments
 (0)