From c1ee1d9dd916719e709bbea0b7cb46118bafc590 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Fri, 14 Aug 2026 14:14:06 +0900 Subject: [PATCH] fix(notification): close the static-analysis findings on the merged tree Checkstyle and SpotBugs run in the module's `check` task, not in `test`, so these only surfaced once the platform was verified against the merged tree. - MissingSwitchDefault on the admin runtime-state switch. The enum is exhaustive, so the default is unreachable today; it throws rather than falling through, so a state added later fails loudly instead of silently leaving the runtime in whatever state it already had. - ConstantName on the two audit loggers: the checkstyle pattern allows `log`, `logger` or UPPER_SNAKE, and this class needs two named sinks. - DMI_RANDOM_USED_ONLY_ONCE in three Web Push fixtures. A fresh SecureRandom per call re-seeds from the OS every time, which on a constrained CI runner can block on entropy. Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/admin/NotificationAdminServiceImpl.java | 3 +++ .../platform/observation/LoggingNotificationAudit.java | 10 +++++----- .../platform/provider/webpush/WebPushCryptoTest.java | 5 ++++- .../provider/webpush/WebPushProviderAdapterTest.java | 10 +++++++--- .../platform/testkit/ContractAdapters.java | 8 ++++++-- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/admin/NotificationAdminServiceImpl.java b/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/admin/NotificationAdminServiceImpl.java index 5bfe6d37..9e072277 100644 --- a/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/admin/NotificationAdminServiceImpl.java +++ b/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/admin/NotificationAdminServiceImpl.java @@ -293,6 +293,9 @@ public final class NotificationAdminServiceImpl implements NotificationAdminServ case DEGRADED -> runtime.markDegraded(command.reason()); case THROTTLED -> runtime.markThrottled(); case AUTHENTICATION_FAILED -> runtime.markAuthenticationFailed(command.reason()); + // Unreachable while the enum is exhaustive; present so a state added later fails loudly here + // rather than silently leaving the runtime in whatever state it was already in. + default -> throw new IllegalStateException("unhandled provider runtime state"); } AdminOperationResult result = diff --git a/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java b/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java index b1926aa5..10a762d6 100644 --- a/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java +++ b/src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java @@ -22,13 +22,13 @@ import org.slf4j.LoggerFactory; public final class LoggingNotificationAudit implements NotificationAuditPort, NotificationSecurityAuditPort { - private static final Logger audit = LoggerFactory.getLogger("notification.audit"); - private static final Logger security = LoggerFactory.getLogger("notification.security"); + private static final Logger AUDIT = LoggerFactory.getLogger("notification.audit"); + private static final Logger SECURITY = LoggerFactory.getLogger("notification.security"); @Override public void record(NotificationAuditEvent event) { Objects.requireNonNull(event, "event"); - audit.info( + AUDIT.info( "action={} actor={} reason={} operationId={} occurredAt={} attributes={}", event.action(), event.actorRef(), @@ -43,7 +43,7 @@ public final class LoggingNotificationAudit Objects.requireNonNull(profileId, "profileId"); // The payload is deliberately absent: a forged callback must not get its content into the log // just by being rejected. - security.warn( + SECURITY.warn( "event=callback_signature_rejected providerProfile={} reason={}", profileId.value(), reasonCode); @@ -52,7 +52,7 @@ public final class LoggingNotificationAudit @Override public void callbackRejectedByLimit(ProviderProfileId profileId, String reasonCode) { Objects.requireNonNull(profileId, "profileId"); - security.warn( + SECURITY.warn( "event=callback_rejected_by_limit providerProfile={} reason={}", profileId.value(), reasonCode); diff --git a/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushCryptoTest.java b/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushCryptoTest.java index fa630ee7..5531598c 100644 --- a/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushCryptoTest.java +++ b/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushCryptoTest.java @@ -41,6 +41,9 @@ class WebPushCryptoTest { private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-08-14T00:00:00Z"), ZoneOffset.UTC); private static final URI ENDPOINT = URI.create("https://push.example.com/send/abc123"); + // One instance: a fresh SecureRandom per call re-seeds from the OS each time, which is slower + // and, on a constrained CI runner, can block on entropy. + private static final SecureRandom RANDOM = new SecureRandom(); @Test void payloadIsEncryptedForTheSubscriptionAndDecryptsBackToThePlaintext() throws Exception { @@ -221,7 +224,7 @@ class WebPushCryptoTest { private static byte[] authSecret() { byte[] secret = new byte[16]; - new SecureRandom().nextBytes(secret); + RANDOM.nextBytes(secret); return secret; } diff --git a/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushProviderAdapterTest.java b/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushProviderAdapterTest.java index 80a09346..2e47fe18 100644 --- a/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushProviderAdapterTest.java +++ b/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushProviderAdapterTest.java @@ -35,6 +35,10 @@ class WebPushProviderAdapterTest { private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-08-14T00:00:00Z"), ZoneOffset.UTC); + // One instance: a fresh SecureRandom per call re-seeds from the OS each time, which is slower + // and, on a constrained CI runner, can block on entropy. + private static final SecureRandom RANDOM = new SecureRandom(); + private final ProviderFaultHarness harness = new ProviderFaultHarness(); private final ContactPointProtector protector = new AesGcmContactPointProtector(SecurityFixtures.keys()); @@ -115,7 +119,7 @@ class WebPushProviderAdapterTest { @Test void payloadEncryptionRoundTripsThroughTheSubscriptionKeys() { - var encryptor = new Rfc8291Aes128GcmEncryptor(new SecureRandom()); + var encryptor = new Rfc8291Aes128GcmEncryptor(RANDOM); var subscription = subscription(); var encrypted = @@ -135,7 +139,7 @@ class WebPushProviderAdapterTest { return new WebPushNotificationProviderAdapter( new JdkNotificationHttpGateway(Duration.ofSeconds(2)), new WebPushRequestMapper( - new Rfc8291Aes128GcmEncryptor(new SecureRandom()), + new Rfc8291Aes128GcmEncryptor(RANDOM), // Signing needs a PKCS#8 EC key, which VapidJwtSignerTest covers; this keeps the // transport test about TTL, encryption and status mapping. (endpoint, signingKey, publicKey) -> "vapid t=stub-token, k=" + publicKey, @@ -163,7 +167,7 @@ class WebPushProviderAdapterTest { generator.initialize(new ECGenParameterSpec("secp256r1")); KeyPair pair = generator.generateKeyPair(); byte[] authSecret = new byte[16]; - new SecureRandom().nextBytes(authSecret); + RANDOM.nextBytes(authSecret); return new WebPushSubscriptionValue( URI.create(harness.baseUri() + "/push/subscription-1"), Rfc8291Aes128GcmEncryptor.encodePoint((ECPublicKey) pair.getPublic()), diff --git a/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/testkit/ContractAdapters.java b/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/testkit/ContractAdapters.java index 00f9118d..113a3252 100644 --- a/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/testkit/ContractAdapters.java +++ b/src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/testkit/ContractAdapters.java @@ -61,6 +61,10 @@ import java.util.Optional; */ public final class ContractAdapters { + // One instance: a fresh SecureRandom per call re-seeds from the OS each time, which is slower + // and, on a constrained CI runner, can block on entropy. + private static final SecureRandom RANDOM = new SecureRandom(); + private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-08-14T00:00:00Z"), ZoneOffset.UTC); @@ -186,7 +190,7 @@ public final class ContractAdapters { new WebPushNotificationProviderAdapter( new JdkNotificationHttpGateway(Duration.ofSeconds(2)), new WebPushRequestMapper( - new Rfc8291Aes128GcmEncryptor(new SecureRandom()), + new Rfc8291Aes128GcmEncryptor(RANDOM), // Signing needs a PKCS#8 EC key, which WebPushCryptoTest covers; the suites here // are about transport and evidence semantics. (endpoint, signingKey, publicKey) -> "vapid t=stub-token, k=" + publicKey, @@ -240,7 +244,7 @@ public final class ContractAdapters { generator.initialize(new ECGenParameterSpec("secp256r1")); KeyPair pair = generator.generateKeyPair(); byte[] authSecret = new byte[16]; - new SecureRandom().nextBytes(authSecret); + RANDOM.nextBytes(authSecret); return new WebPushSubscriptionValue( URI.create(harness.baseUri() + "/push/subscription-1"), Rfc8291Aes128GcmEncryptor.encodePoint((ECPublicKey) pair.getPublic()),