feat: add production capability foundations

This commit is contained in:
donghyeon-ka
2026-07-31 23:50:44 +09:00
parent b3add0162d
commit 567422f2e5
757 changed files with 132385 additions and 2146 deletions
+11 -2
View File
@@ -42,6 +42,15 @@ MongoDB, file server, object storage 같은 optional leaf는 독립적으로 빌
잘못 설정된 채로 기동돼 트래픽을 받는 것보다, 기동 시점에 명확한 이유와 함께 멈추는 편이 안전하다.
이 패키지는 그 "빨리·명확하게 실패시키기(fail-fast)"를 담당한다.
### AuthenticationModeCompositionConfig
`ca-skeleton.security.auth-mode`의 기본값은 `jwt`이며 `jwt|redis-session` 중 정확히 하나만 허용한다.
JWT mode는 `jwtDecoder`가 있어야 하고 Redis session repository/filter가 있으면 기동을 거부한다.
Redis session mode는 반대로 `jwtDecoder`를 거부하고 `redisVersionedSessionRepository`
`springSessionRepositoryFilter`가 모두 있어야 한다. 이 검증은 bean name만 확인하므로 bootstrap이
Spring Session/Redis 구현 타입을 직접 의존하지 않으며, composition 누락과 이중 활성화를 context
refresh 완료 전에 실패시킨다.
### FlywayProdSafetyValidator
- **`prod` 프로파일에서 Flyway 안전장치가 꺼지지 못하도록 런타임에서 강제한다.** Flyway 옵션은
`application.yml`에 안전값으로 고정돼 있지만(`baseline-on-migrate=false`, `out-of-order=false`,
@@ -144,7 +153,7 @@ MongoDB, file server, object storage 같은 optional leaf는 독립적으로 빌
### SecretSource
- **시크릿 해석을 인터페이스 한 겹 뒤로 숨긴 backend seam 이다.** 시크릿이 필요한 코드는 이
인터페이스에만 의존하고, 실제 백엔드(env / Vault / AWS Secrets Manager / GCP Secret Manager)는
`SecretSourceFactory`가 설정값으로 고른다. `RateLimiter` / `RateLimiterFactory`와 같은 패턴이다.
`SecretSourceFactory`가 설정값으로 고른다. 설정과 구현 선택을 한 factory에 모으는 패턴이다.
백엔드를 추가하는 비용이 "새 `SecretSource` 구현 1개 + `SecretSourceStrategy` enum 값 1개 +
factory case 1개"로 고정되고, 소비자(`SecretSourceValidator`, 향후 어댑터)는 전혀 손대지 않는다.
- **빈 문자열은 "없음"으로 취급한다.** `resolve`가 blank 값을 `Optional.empty()`로 돌려주지 않으면,
@@ -174,7 +183,7 @@ MongoDB, file server, object storage 같은 optional leaf는 독립적으로 빌
### SecretSourceFactory
- **유일한 확장 지점을 `switch` 하나로 모았다.** 새 백엔드는 `SecretSourceStrategy` 값 +
`SecretSource` 구현 + 이 `switch`의 case 추가로 끝나고 소비자는 바뀌지 않는다.
`RateLimiterFactory`와 같은 형태로, "확장 비용이 어디에 있는가"를 한곳에서 보이게 했다.
factory 한곳에 "확장 비용이 어디에 있는가"를 보이게 했다.
### EnvironmentSecretSource
- **기본 백엔드는 Spring `Environment`에서 읽는 것이다.** `ENVIRONMENT` 전략은 시크릿이 env var /
+21
View File
@@ -28,6 +28,12 @@ sourceSets {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
}
redisCompositionTest {
java.srcDir 'src/redisCompositionTest/java'
resources.srcDir 'src/redisCompositionTest/resources'
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
}
}
configurations {
@@ -37,6 +43,9 @@ configurations {
sampleOffTestCompileOnly.extendsFrom testCompileOnly
sampleOffTestRuntimeOnly.extendsFrom testRuntimeOnly
sampleOffTestAnnotationProcessor.extendsFrom testAnnotationProcessor
redisCompositionTestImplementation.extendsFrom testImplementation
redisCompositionTestCompileOnly.extendsFrom testCompileOnly
redisCompositionTestRuntimeOnly.extendsFrom testRuntimeOnly
}
dependencies {
@@ -47,6 +56,7 @@ dependencies {
implementation project(':adapter:outbound:messaging')
implementation project(':adapter:outbound:cache-redis')
implementation project(':adapter:outbound:notification')
implementation project(':adapter:outbound:fileserver')
implementation project(':adapter:outbound:httpclient')
implementation project(':adapter:outbound:identifier')
implementation project(':adapter:inbound:web')
@@ -140,6 +150,17 @@ tasks.register('sampleOffTest', Test) {
jvmArgs '-Duser.timezone=UTC'
}
tasks.register('redisCompositionTest', Test) {
group = 'redis verification'
description = 'Runs Redis provider/role/security-mode composition and zero-side-effect contracts.'
testClassesDirs = sourceSets.redisCompositionTest.output.classesDirs
classpath = sourceSets.redisCompositionTest.runtimeClasspath
useJUnitPlatform()
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
// The custom source set compiles the same test corpus, so it follows the repository-wide
// warning-only policy already applied to checkstyleTest and spotbugsTest in the root build.
tasks.named('checkstyleSampleOffTest') {
+276 -270
View File
@@ -1,443 +1,449 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
aopalliance:aopalliance:1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
ch.qos.logback:logback-classic:1.5.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
aopalliance:aopalliance:1.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
ch.qos.logback:logback-classic:1.5.21=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-classic:1.5.34=sampleFixture
ch.qos.logback:logback-core:1.5.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.21=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.34=sampleFixture
com.approvaltests:approvaltests-util:31.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.approvaltests:approvaltests:31.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.approvaltests:approvaltests-util:31.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.approvaltests:approvaltests:31.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.21=sampleFixture
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.21.4=sampleFixture
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.21.4=sampleFixture
com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.21.4=sampleFixture
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.4=sampleFixture
com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.4=sampleFixture
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.4=sampleFixture
com.fasterxml.jackson.module:jackson-module-parameter-names:2.21.4=sampleFixture
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.21.4=sampleFixture
com.fasterxml:classmate:1.7.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml:classmate:1.7.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml:classmate:1.7.3=sampleFixture
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.f4b6a3:uuid-creator:6.1.1=sampleFixture,testRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
com.github.spotbugs:spotbugs:4.10.2=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=sampleFixture,spotbugs
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,sampleOffTestCompileClasspath,spotbugs,testCompileClasspath
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,spotbugs,testCompileClasspath
com.google.code.gson:gson:2.13.2=spotbugs
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
com.google.errorprone:error_prone_annotations:2.47.0=checkstyle
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.6.0-jre=checkstyle
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.9.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.jayway.jsonpath:json-path:2.9.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:9.37.4=sampleFixture
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
com.squareup.okhttp3:okhttp-jvm:5.2.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okhttp3:okhttp-jvm:5.2.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okhttp3:okhttp:4.12.0=sampleFixture
com.squareup.okhttp3:okhttp:5.2.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio-jvm:3.16.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okhttp3:okhttp:5.2.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio-jvm:3.16.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio-jvm:3.6.0=sampleFixture
com.squareup.okio:okio:3.16.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio:3.16.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio:3.6.0=sampleFixture
com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-api:1.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-api:1.3.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=redisCompositionTestRuntimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=redisCompositionTestRuntimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.zaxxer:HikariCP:6.3.3=sampleFixture
com.zaxxer:HikariCP:7.0.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.zaxxer:HikariCP:7.0.2=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-beanutils:commons-beanutils:1.11.0=checkstyle
commons-codec:commons-codec:1.19.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-codec:commons-codec:1.19.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-collections:commons-collections:3.2.2=checkstyle
commons-io:commons-io:2.20.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-io:commons-io:2.20.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-io:commons-io:2.21.0=spotbugs
commons-logging:commons-logging:1.3.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-logging:commons-logging:1.3.5=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
info.picocli:picocli:4.7.7=checkstyle
io.github.cdimascio:dotenv-java:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
io.github.resilience4j:resilience4j-bulkhead:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-circuitbreaker:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-core:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-micrometer:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-ratelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-retry:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-timelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.lettuce:lettuce-core:6.8.1.RELEASE=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.cdimascio:dotenv-java:3.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
io.github.resilience4j:resilience4j-bulkhead:2.2.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-circuitbreaker:2.2.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-core:2.2.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-micrometer:2.2.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-ratelimiter:2.2.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-retry:2.2.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-timelimiter:2.2.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.lettuce:lettuce-core:6.8.1.RELEASE=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.micrometer:context-propagation:1.1.4=sampleFixture
io.micrometer:context-propagation:1.2.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:context-propagation:1.2.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-commons:1.15.12=sampleFixture
io.micrometer:micrometer-commons:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-commons:1.16.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-core:1.15.12=sampleFixture
io.micrometer:micrometer-core:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-core:1.16.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-jakarta9:1.15.12=sampleFixture
io.micrometer:micrometer-jakarta9:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-jakarta9:1.16.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-observation:1.15.12=sampleFixture
io.micrometer:micrometer-observation:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-observation:1.16.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-registry-prometheus:1.15.12=sampleFixture
io.micrometer:micrometer-registry-prometheus:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-registry-prometheus:1.16.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing-bridge-otel:1.5.12=sampleFixture
io.micrometer:micrometer-tracing-bridge-otel:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing-bridge-otel:1.6.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing:1.5.12=sampleFixture
io.micrometer:micrometer-tracing:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-base:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing:1.6.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-base:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-compression:4.2.7.Final=testRuntimeClasspath
io.netty:netty-codec-dns:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-dns:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http2:4.2.7.Final=testRuntimeClasspath
io.netty:netty-codec-http:4.2.7.Final=testRuntimeClasspath
io.netty:netty-codec-marshalling:4.2.7.Final=testRuntimeClasspath
io.netty:netty-codec-protobuf:4.2.7.Final=testRuntimeClasspath
io.netty:netty-codec:4.2.7.Final=testRuntimeClasspath
io.netty:netty-common:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-resolver-dns:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-common:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-resolver-dns:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-transport-classes-epoll:4.2.7.Final=testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.7.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.7.Final=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry.semconv:opentelemetry-semconv:1.32.0=sampleFixture
io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-api:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-api:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-common:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-api:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-common:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-context:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-context:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-context:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-common:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-common:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-common:1.55.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp-common:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-otlp-common:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp-common:1.55.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-otlp:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-extension-trace-propagators:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-extension-trace-propagators:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-extension-trace-propagators:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-common:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-common:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-common:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.55.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-logs:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-logs:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-logs:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-metrics:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-metrics:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-metrics:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-trace:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-trace:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-trace:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk:1.55.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor:reactor-core:3.7.19=sampleFixture
io.projectreactor:reactor-core:3.8.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor:reactor-core:3.8.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-config:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-config:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-config:1.4.3=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-core:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-core:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-core:1.4.3=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-formats:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-exposition-formats:1.4.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-formats:1.4.3=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-textformats:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-exposition-textformats:1.4.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-textformats:1.4.3=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-model:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-model:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-model:1.4.3=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-tracer-common:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-tracer-common:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-tracer-common:1.4.3=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.smallrye:jandex:3.2.0=sampleFixture
io.swagger.core.v3:swagger-annotations-jakarta:2.2.29=sampleFixture
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-core-jakarta:2.2.29=sampleFixture
io.swagger.core.v3:swagger-core-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-core-jakarta:2.2.38=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-models-jakarta:2.2.29=sampleFixture
io.swagger.core.v3:swagger-models-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-models-jakarta:2.2.38=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:2.1.1=sampleFixture
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
jakarta.persistence:jakarta.persistence-api:3.1.0=sampleFixture
jakarta.persistence:jakarta.persistence-api:3.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.transaction:jakarta.transaction-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.persistence:jakarta.persistence-api:3.2.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.transaction:jakarta.transaction-api:2.0.1=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.0.2=sampleFixture
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.websocket:jakarta.websocket-api:2.2.0=sampleOffTestCompileClasspath,testCompileClasspath
jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=sampleOffTestCompileClasspath,testCompileClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.websocket:jakarta.websocket-api:2.2.0=redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.5=sampleFixture
javax.inject:javax.inject:1=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
javax.inject:javax.inject:1=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
jaxen:jaxen:2.0.0=spotbugs
me.paulschwarz:spring-dotenv:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.17.8=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.java.dev.jna:jna:5.18.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.logstash.logback:logstash-logback-encoder:8.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:accessors-smart:2.6.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:json-smart:2.6.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
me.paulschwarz:spring-dotenv:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.17.8=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.java.dev.jna:jna:5.18.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.logstash.logback:logstash-logback-encoder:8.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:accessors-smart:2.6.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:json-smart:2.6.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
org.antlr:antlr4-runtime:4.13.2=checkstyle,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.antlr:antlr4-runtime:4.13.2=checkstyle,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.bcel:bcel:6.12.0=spotbugs
org.apache.commons:commons-compress:1.28.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-compress:1.28.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-text:1.15.0=spotbugs
org.apache.commons:commons-text:1.3=checkstyle
org.apache.httpcomponents:httpclient:4.5.13=checkstyle,testRuntimeClasspath
org.apache.httpcomponents:httpcore:4.4.16=checkstyle,testRuntimeClasspath
org.apache.kafka:kafka-clients:4.1.1=sampleOffTestCompileClasspath,testCompileClasspath
org.apache.kafka:kafka-clients:4.1.1=redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.apache.logging.log4j:log4j-api:2.24.3=sampleFixture
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
org.apache.logging.log4j:log4j-to-slf4j:2.24.3=sampleFixture
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
org.apache.tomcat.embed:tomcat-embed-core:10.1.55=sampleFixture
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:10.1.55=sampleFixture
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:10.1.55=sampleFixture
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.xbean:xbean-reflect:3.7=checkstyle
org.apiguardian:apiguardian-api:1.1.2=sampleOffTestCompileClasspath,testCompileClasspath
org.aspectj:aspectjweaver:1.9.25=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apiguardian:apiguardian-api:1.1.2=redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.aspectj:aspectjweaver:1.9.25=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.aspectj:aspectjweaver:1.9.25.1=sampleFixture
org.assertj:assertj-core:3.27.6=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.awaitility:awaitility:4.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.checkerframework:checker-qual:3.49.5=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.assertj:assertj-core:3.27.6=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.awaitility:awaitility:4.3.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.checkerframework:checker-qual:3.49.5=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
org.dom4j:dom4j:2.2.0=spotbugs
org.eclipse.angus:angus-activation:2.0.3=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.14.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.eclipse.angus:angus-activation:2.0.3=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.14.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.7.2=sampleFixture
org.flywaydb:flyway-database-postgresql:11.14.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:11.14.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:11.7.2=sampleFixture
org.glassfish.jaxb:jaxb-core:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-core:4.0.6=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-core:4.0.9=sampleFixture
org.glassfish.jaxb:jaxb-runtime:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-runtime:4.0.6=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-runtime:4.0.9=sampleFixture
org.glassfish.jaxb:txw2:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:txw2:4.0.6=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:txw2:4.0.9=sampleFixture
org.hamcrest:hamcrest:3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.hamcrest:hamcrest:3.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.hibernate.common:hibernate-commons-annotations:7.0.3.Final=sampleFixture
org.hibernate.models:hibernate-models:1.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.hibernate.models:hibernate-models:1.0.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.hibernate.orm:hibernate-core:6.6.53.Final=sampleFixture
org.hibernate.orm:hibernate-core:7.1.8.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hibernate.orm:hibernate-core:7.1.8.Final=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hibernate.validator:hibernate-validator:8.0.3.Final=sampleFixture
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.javassist:javassist:3.28.0-GA=checkstyle
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jboss.logging:jboss-logging:3.6.3.Final=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.jetbrains:annotations:13.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture
org.jetbrains:annotations:17.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestAnnotationProcessor,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.1=sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-testkit:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains:annotations:17.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,productionRuntimeClasspath,redisCompositionTestAnnotationProcessor,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestAnnotationProcessor,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.1=redisCompositionTestRuntimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-testkit:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:6.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:6.1.0=spotbugs
org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.20.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.openapitools:jackson-databind-nullable:0.2.6=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.resource:1.0.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.20.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=redisCompositionTestRuntimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.openapitools:jackson-databind-nullable:0.2.6=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.resource:1.0.0=compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.ow2.asm:asm-analysis:9.10.1=spotbugs
org.ow2.asm:asm-commons:9.10.1=spotbugs
org.ow2.asm:asm-tree:9.10.1=spotbugs
org.ow2.asm:asm-util:9.10.1=spotbugs
org.ow2.asm:asm:9.10.1=spotbugs
org.ow2.asm:asm:9.7.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
org.ow2.asm:asm:9.7.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,redisCompositionTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
org.postgresql:postgresql:42.7.11=sampleFixture
org.postgresql:postgresql:42.7.8=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.reactivestreams:reactive-streams:1.0.4=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.postgresql:postgresql:42.7.8=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.reactivestreams:reactive-streams:1.0.4=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle
org.rnorth.duct-tape:duct-tape:1.0.8=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.rnorth.duct-tape:duct-tape:1.0.8=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.18=sampleFixture
org.slf4j:slf4j-api:2.0.17=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.17=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.18=sampleFixture
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
org.springdoc:springdoc-openapi-starter-common:2.8.6=sampleFixture
org.springdoc:springdoc-openapi-starter-common:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springdoc:springdoc-openapi-starter-common:3.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.6=sampleFixture
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.16=sampleFixture
org.springframework.boot:spring-boot-actuator-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator:3.5.16=sampleFixture
org.springframework.boot:spring-boot-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-autoconfigure:3.5.16=sampleFixture
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-configuration-processor:4.0.0=annotationProcessor
org.springframework.boot:spring-boot-data-commons:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-data-jpa-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-data-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-flyway:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-health:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-hibernate:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-client:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-converter:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jdbc-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jdbc:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jpa-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-micrometer-metrics:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-micrometer-observation:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-persistence:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-restclient:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-security:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-sql:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-data-commons:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-data-jpa-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-data-jpa:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-flyway:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-health:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-hibernate:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-client:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-converter:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jdbc-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jdbc:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jpa-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jpa:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-micrometer-metrics:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-micrometer-observation:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-persistence:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-restclient:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-security:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-sql:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-actuator:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-data-jpa:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-data-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-flyway:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-data-jpa:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-flyway:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jdbc:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-jdbc:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jdbc:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-json:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-json:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-json:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-micrometer-metrics:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-micrometer-metrics:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-security:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-security:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-security:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-validation:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-web:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-web:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-web:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-transaction:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-validation:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-transaction:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-validation:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot:3.5.16=sampleFixture
org.springframework.boot:spring-boot:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.cloud:spring-cloud-context:4.1.4=sampleOffTestCompileClasspath,testCompileClasspath
org.springframework.boot:spring-boot:4.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.cloud:spring-cloud-context:4.1.4=redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.springframework.data:spring-data-commons:3.5.13=sampleFixture
org.springframework.data:spring-data-commons:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.data:spring-data-commons:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.data:spring-data-jpa:3.5.13=sampleFixture
org.springframework.data:spring-data-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.data:spring-data-jpa:4.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.data:spring-data-keyvalue:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.data:spring-data-redis:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.integration:spring-integration-core:6.5.10=sampleFixture
org.springframework.integration:spring-integration-core:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.integration:spring-integration-core:7.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.integration:spring-integration-jdbc:6.5.10=sampleFixture
org.springframework.integration:spring-integration-jdbc:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.integration:spring-integration-jdbc:7.0.0=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.retry:spring-retry:2.0.13=sampleFixture
org.springframework.security:spring-security-config:6.5.11=sampleFixture
org.springframework.security:spring-security-config:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-config:7.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-core:6.5.11=sampleFixture
org.springframework.security:spring-security-core:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-core:7.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-crypto:6.5.11=sampleFixture
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-core:6.5.11=sampleFixture
org.springframework.security:spring-security-oauth2-core:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-core:7.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-jose:6.5.11=sampleFixture
org.springframework.security:spring-security-oauth2-jose:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-jose:7.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-resource-server:6.5.11=sampleFixture
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-test:7.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-test:7.0.0=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-web:6.5.11=sampleFixture
org.springframework.security:spring-security-web:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-web:7.0.0=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.session:spring-session-core:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.session:spring-session-data-redis:4.0.0=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-aop:6.2.19=sampleFixture
org.springframework:spring-aop:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-aop:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-aspects:6.2.19=sampleFixture
org.springframework:spring-aspects:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-aspects:7.0.1=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-beans:6.2.19=sampleFixture
org.springframework:spring-beans:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-beans:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-context-support:7.0.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-context:6.2.19=sampleFixture
org.springframework:spring-context:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-context:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-core:6.2.19=sampleFixture
org.springframework:spring-core:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-core:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-expression:6.2.19=sampleFixture
org.springframework:spring-expression:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-expression:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-jcl:6.2.19=sampleFixture
org.springframework:spring-jdbc:6.2.19=sampleFixture
org.springframework:spring-jdbc:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-jdbc:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-messaging:6.2.19=sampleFixture
org.springframework:spring-messaging:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-messaging:7.0.1=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-orm:6.2.19=sampleFixture
org.springframework:spring-orm:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-test:7.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-orm:7.0.1=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-oxm:7.0.1=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-test:7.0.1=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-tx:6.2.19=sampleFixture
org.springframework:spring-tx:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-tx:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-web:6.2.19=sampleFixture
org.springframework:spring-web:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-web:7.0.1=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-webmvc:6.2.19=sampleFixture
org.springframework:spring-webmvc:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-websocket:7.0.1=sampleOffTestCompileClasspath,testCompileClasspath
org.testcontainers:testcontainers-database-commons:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-jdbc:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-postgresql:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-webmvc:7.0.1=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-websocket:7.0.1=redisCompositionTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.testcontainers:testcontainers-database-commons:2.0.2=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-jdbc:2.0.2=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.2=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-postgresql:2.0.2=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers:2.0.2=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
org.xmlunit:xmlunit-core:2.10.4=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.xmlunit:xmlunit-core:2.10.4=redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.4=sampleFixture
org.yaml:snakeyaml:2.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
redis.clients.authentication:redis-authx-core:0.1.1-beta2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.5=compileClasspath,productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
redis.clients.authentication:redis-authx-core:0.1.1-beta2=productionRuntimeClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
software.amazon.awssdk:annotations:2.30.0=testRuntimeClasspath
software.amazon.awssdk:apache-client:2.30.0=testRuntimeClasspath
software.amazon.awssdk:arns:2.30.0=testRuntimeClasspath
@@ -468,7 +474,7 @@ software.amazon.awssdk:sdk-core:2.30.0=testRuntimeClasspath
software.amazon.awssdk:third-party-jackson-core:2.30.0=testRuntimeClasspath
software.amazon.awssdk:utils:2.30.0=testRuntimeClasspath
software.amazon.eventstream:eventstream:1.0.1=testRuntimeClasspath
tools.jackson.core:jackson-core:3.0.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.0.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.0.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.0.2=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.0.2=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.0.2=productionRuntimeClasspath,redisCompositionTestCompileClasspath,redisCompositionTestRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=developmentOnly,testAndDevelopmentOnly
@@ -0,0 +1,56 @@
package dev.caskeleton.bootstrap.httpclient;
import dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientActivationResolver;
import dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration;
import dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfigurationBinder;
import dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientReadinessCardRegistry;
import dev.caskeleton.adapter.outbound.httpclient.activation.HttpOperationCatalogRegistry;
import dev.caskeleton.adapter.outbound.httpclient.activation.ResolvedHttpClientCapability;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* HTTP capability composition root.
*
* <p>Only inert configuration, registries, and a sanitized descriptor are registered in the current
* zero-binding implementation. No transport/provider configuration is imported.
*/
@Configuration(proxyBeanMethods = false)
public class HttpClientCompositionConfig {
@Bean
@ConditionalOnMissingBean
HttpClientCanonicalConfiguration httpClientCanonicalConfiguration(Environment environment) {
return new HttpClientCanonicalConfigurationBinder(Binder.get(environment)).bind();
}
@Bean
@ConditionalOnMissingBean
HttpOperationCatalogRegistry httpOperationCatalogRegistry() {
return HttpOperationCatalogRegistry.empty();
}
@Bean
@ConditionalOnMissingBean
HttpClientReadinessCardRegistry httpClientReadinessCardRegistry() {
return HttpClientReadinessCardRegistry.current();
}
@Bean
@ConditionalOnMissingBean
HttpClientActivationResolver httpClientActivationResolver() {
return new HttpClientActivationResolver();
}
@Bean
ResolvedHttpClientCapability resolvedHttpClientCapability(
HttpClientCanonicalConfiguration configuration,
HttpOperationCatalogRegistry catalogs,
HttpClientReadinessCardRegistry readinessCards,
HttpClientActivationResolver resolver) {
return resolver.resolve(configuration, catalogs, readinessCards);
}
}
@@ -3,6 +3,7 @@ package dev.caskeleton.bootstrap.idempotency;
import dev.caskeleton.application.idempotency.IdempotencyExecutor;
import dev.caskeleton.application.idempotency.IdempotencyStorePort;
import java.time.Clock;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@@ -21,6 +22,10 @@ public class IdempotencyConfig {
}
@Bean
@ConditionalOnProperty(
name = "ca-skeleton.capabilities.idempotency.provider",
havingValue = "jdbc",
matchIfMissing = true)
public IdempotencyExecutor idempotencyExecutor(
IdempotencyStorePort store, Clock clock, IdempotencySettings properties) {
return new IdempotencyExecutor(store, clock, properties.ttl());
@@ -0,0 +1,69 @@
package dev.caskeleton.bootstrap.idempotency;
import dev.caskeleton.application.idempotency.IdempotencyExecutor;
import dev.caskeleton.application.idempotency.IdempotencyExecutorV2;
import dev.caskeleton.application.idempotency.IdempotencyStorePort;
import dev.caskeleton.application.idempotency.IdempotencyStorePortV2;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** Fail-fast exclusivity guard for the JDBC V1 and Redis V2 request-replay providers. */
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(IdempotencyProviderSettings.class)
public class IdempotencyProviderSelectionConfig {
@Bean
SmartInitializingSingleton idempotencyProviderExclusivity(
IdempotencyProviderSettings settings,
ObjectProvider<IdempotencyStorePort> jdbcStores,
ObjectProvider<IdempotencyExecutor> jdbcExecutors,
ObjectProvider<IdempotencyStorePortV2> redisStores,
ObjectProvider<IdempotencyExecutorV2> redisExecutors) {
return () -> {
int jdbcStoreCount = count(jdbcStores);
int jdbcExecutorCount = count(jdbcExecutors);
int redisStoreCount = count(redisStores);
int redisExecutorCount = count(redisExecutors);
switch (settings.provider()) {
case DISABLED ->
requireCounts(
jdbcStoreCount, jdbcExecutorCount, redisStoreCount, redisExecutorCount, 0, 0, 0, 0);
case JDBC ->
requireCounts(
jdbcStoreCount, jdbcExecutorCount, redisStoreCount, redisExecutorCount, 1, 1, 0, 0);
case REDIS ->
requireCounts(
jdbcStoreCount, jdbcExecutorCount, redisStoreCount, redisExecutorCount, 0, 0, 1, 1);
default ->
throw new IllegalStateException(
"Unsupported idempotency provider: " + settings.provider());
}
};
}
private static int count(ObjectProvider<?> beans) {
return Math.toIntExact(beans.stream().count());
}
private static void requireCounts(
int jdbcStores,
int jdbcExecutors,
int redisStores,
int redisExecutors,
int expectedJdbcStores,
int expectedJdbcExecutors,
int expectedRedisStores,
int expectedRedisExecutors) {
if (jdbcStores != expectedJdbcStores
|| jdbcExecutors != expectedJdbcExecutors
|| redisStores != expectedRedisStores
|| redisExecutors != expectedRedisExecutors) {
throw new IllegalStateException(
"Idempotency provider selection is ambiguous or incomplete: exactly the selected "
+ "JDBC V1 or Redis V2 store/executor pair must be active");
}
}
}
@@ -0,0 +1,20 @@
package dev.caskeleton.bootstrap.idempotency;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
/** Exact provider selector; provider precedence is never inferred from the classpath. */
@ConfigurationProperties(prefix = "ca-skeleton.capabilities.idempotency")
public record IdempotencyProviderSettings(Provider provider) {
@ConstructorBinding
public IdempotencyProviderSettings {
provider = provider == null ? Provider.JDBC : provider;
}
public enum Provider {
DISABLED,
JDBC,
REDIS
}
}
@@ -0,0 +1,42 @@
package dev.caskeleton.bootstrap.redis;
import dev.caskeleton.adapter.outbound.cache.redis.security.DestroyableRedisSecret;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisCredentialMaterialProvider;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisSecretReference;
import dev.caskeleton.adapter.outbound.cache.redis.security.VersionedRedisCredentialMaterial;
import dev.caskeleton.bootstrap.runtime.SecretSource;
import java.time.Instant;
/**
* Strict {@code secret://environment/ENV_KEY} bridge for canonical Redis material.
*
* <p>The environment backend has no change-event stream. Rotation therefore requires process
* restart or an explicit runtime recomposition initiated by an operator.
*/
public final class RedisEnvironmentCredentialMaterialProvider
implements RedisCredentialMaterialProvider {
private static final int MAXIMUM_CREDENTIAL_CHARS = 16_384;
private static final String VERSION = "environment-restart-v1";
private final RedisEnvironmentMaterialResolver resolver;
public RedisEnvironmentCredentialMaterialProvider(SecretSource secretSource) {
this.resolver = new RedisEnvironmentMaterialResolver(secretSource);
}
@Override
public VersionedRedisCredentialMaterial resolve(RedisSecretReference reference) {
String value = resolver.resolve(reference, MAXIMUM_CREDENTIAL_CHARS);
char[] mutable = value.toCharArray();
try {
return new VersionedRedisCredentialMaterial(
VERSION, Instant.MAX, DestroyableRedisSecret.from(mutable));
} finally {
java.util.Arrays.fill(mutable, '\0');
}
}
public RedisEnvironmentMaterialProviderDescriptor descriptor() {
return RedisEnvironmentMaterialProviderDescriptor.restartOrExplicitRecomposition();
}
}
@@ -0,0 +1,22 @@
package dev.caskeleton.bootstrap.redis;
import dev.caskeleton.bootstrap.runtime.SecretSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** Registers the environment-backed material bridge without resolving any secret eagerly. */
@Configuration(proxyBeanMethods = false)
public class RedisEnvironmentMaterialConfig {
@Bean
RedisEnvironmentCredentialMaterialProvider redisEnvironmentCredentialMaterialProvider(
SecretSource secretSource) {
return new RedisEnvironmentCredentialMaterialProvider(secretSource);
}
@Bean
RedisEnvironmentTrustMaterialProvider redisEnvironmentTrustMaterialProvider(
SecretSource secretSource) {
return new RedisEnvironmentTrustMaterialProvider(secretSource);
}
}
@@ -0,0 +1,11 @@
package dev.caskeleton.bootstrap.redis;
/** Honest operational capability descriptor for the environment-backed Redis material bridge. */
public record RedisEnvironmentMaterialProviderDescriptor(
String provider, boolean changeEventsSupported, String refreshMode) {
static RedisEnvironmentMaterialProviderDescriptor restartOrExplicitRecomposition() {
return new RedisEnvironmentMaterialProviderDescriptor(
"environment", false, "restart-or-explicit-runtime-recomposition");
}
}
@@ -0,0 +1,73 @@
package dev.caskeleton.bootstrap.redis;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisSecretReference;
import dev.caskeleton.bootstrap.runtime.SecretSource;
import java.util.Objects;
import java.util.Set;
/** Shared strict parser/resolver for the two environment material provider interfaces. */
final class RedisEnvironmentMaterialResolver {
private static final String PREFIX = "secret://environment/";
private static final Set<String> ALLOWED_KEYS =
Set.of(
"APP_CACHE_REDIS_PASSWORD",
"APP_CACHE_REDIS_KEY_HMAC_SECRET",
"APP_CACHE_REDIS_TRUST_PEM",
"APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET",
"APP_LEASE_REDIS_KEY_HMAC_SECRET",
"APP_RATE_LIMIT_REDIS_PASSWORD",
"APP_RATE_LIMIT_REDIS_KEY_HMAC_SECRET",
"APP_RATE_LIMIT_REDIS_TRUST_PEM",
"APP_SESSION_REDIS_PASSWORD",
"APP_SESSION_REDIS_KEY_HMAC_SECRET",
"APP_SESSION_REDIS_TRUST_PEM");
private final SecretSource secretSource;
RedisEnvironmentMaterialResolver(SecretSource secretSource) {
this.secretSource = Objects.requireNonNull(secretSource, "secretSource must be non-null");
}
String resolve(RedisSecretReference reference, int maximumLength) {
Objects.requireNonNull(reference, "reference must be non-null");
String key = parseAllowedKey(reference.valueForResolution());
try {
String value =
secretSource.resolve(key).orElseThrow(RedisEnvironmentMaterialResolver::materialFailure);
if (value.isBlank() || value.length() > maximumLength) {
throw materialFailure();
}
return value;
} catch (RedisEnvironmentMaterialException exception) {
throw exception;
} catch (RuntimeException ignored) {
throw materialFailure();
}
}
private static String parseAllowedKey(String reference) {
if (reference == null
|| !reference.startsWith(PREFIX)
|| reference.length() <= PREFIX.length()) {
throw materialFailure();
}
String key = reference.substring(PREFIX.length());
if (!key.matches("[A-Z][A-Z0-9_]{0,127}") || !ALLOWED_KEYS.contains(key)) {
throw materialFailure();
}
return key;
}
static RedisEnvironmentMaterialException materialFailure() {
return new RedisEnvironmentMaterialException(
"Canonical Redis environment material resolution failed");
}
static final class RedisEnvironmentMaterialException extends IllegalStateException {
private RedisEnvironmentMaterialException(String message) {
super(message);
}
}
}
@@ -0,0 +1,42 @@
package dev.caskeleton.bootstrap.redis;
import dev.caskeleton.adapter.outbound.cache.redis.security.DestroyableRedisPem;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisSecretReference;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisTrustMaterialProvider;
import dev.caskeleton.adapter.outbound.cache.redis.security.VersionedRedisTrustMaterial;
import dev.caskeleton.bootstrap.runtime.SecretSource;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
/** Environment-backed, restart-only Redis trust material provider. */
public final class RedisEnvironmentTrustMaterialProvider implements RedisTrustMaterialProvider {
private static final int MAXIMUM_TRUST_BYTES = 1_048_576;
private static final String VERSION = "environment-restart-v1";
private final RedisEnvironmentMaterialResolver resolver;
public RedisEnvironmentTrustMaterialProvider(SecretSource secretSource) {
this.resolver = new RedisEnvironmentMaterialResolver(secretSource);
}
@Override
public VersionedRedisTrustMaterial resolve(RedisSecretReference reference) {
String value = resolver.resolve(reference, MAXIMUM_TRUST_BYTES);
byte[] encoded = value.getBytes(StandardCharsets.UTF_8);
if (encoded.length > MAXIMUM_TRUST_BYTES) {
Arrays.fill(encoded, (byte) 0);
throw RedisEnvironmentMaterialResolver.materialFailure();
}
try {
return new VersionedRedisTrustMaterial(
VERSION, Instant.MAX, DestroyableRedisPem.from(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
}
public RedisEnvironmentMaterialProviderDescriptor descriptor() {
return RedisEnvironmentMaterialProviderDescriptor.restartOrExplicitRecomposition();
}
}
@@ -32,6 +32,12 @@ public class SecretSourceValidator implements SmartInitializingSingleton {
"APP_EXTERNAL_API_KEY",
"APP_CACHE_REDIS_PASSWORD",
"APP_CACHE_REDIS_KEY_HMAC_SECRET",
"APP_RATE_LIMIT_REDIS_PASSWORD",
"APP_RATE_LIMIT_REDIS_KEY_HMAC_SECRET",
"APP_SESSION_REDIS_PASSWORD",
"APP_SESSION_REDIS_KEY_HMAC_SECRET",
"APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET",
"APP_LEASE_REDIS_KEY_HMAC_SECRET",
"APP_PRIVACY_PSEUDONYMIZATION_SALT");
private final ConfigurableEnvironment environment;
@@ -82,6 +88,18 @@ public class SecretSourceValidator implements SmartInitializingSingleton {
}
List<String> missing = new ArrayList<>();
for (String key : REQUIRED_PROD_SECRETS) {
if (isRateLimitSecret(key) && !isRedisRateLimitProviderSelected()) {
continue;
}
if (isSessionRedisMaterial(key) && !isRedisSessionSelected()) {
continue;
}
if (isIdempotencyRedisMaterial(key) && !isRedisIdempotencyProviderSelected()) {
continue;
}
if (isLeaseRedisMaterial(key) && !isRedisLeaseProviderSelected()) {
continue;
}
if (secretSource.resolve(key).isEmpty()) {
missing.add(key);
}
@@ -95,6 +113,45 @@ public class SecretSourceValidator implements SmartInitializingSingleton {
}
}
private static boolean isRateLimitSecret(String key) {
return key.startsWith("APP_RATE_LIMIT_REDIS_");
}
private boolean isRedisRateLimitProviderSelected() {
return "redis"
.equalsIgnoreCase(
environment.getProperty("ca-skeleton.capabilities.rate-limit.provider", "disabled"));
}
private static boolean isSessionRedisMaterial(String key) {
return key.startsWith("APP_SESSION_REDIS_");
}
private boolean isRedisSessionSelected() {
return "redis-session"
.equalsIgnoreCase(environment.getProperty("ca-skeleton.security.auth-mode", "jwt"));
}
private static boolean isIdempotencyRedisMaterial(String key) {
return key.startsWith("APP_IDEMPOTENCY_REDIS_");
}
private boolean isRedisIdempotencyProviderSelected() {
return "redis"
.equalsIgnoreCase(
environment.getProperty("ca-skeleton.capabilities.idempotency.provider", "jdbc"));
}
private static boolean isLeaseRedisMaterial(String key) {
return key.startsWith("APP_LEASE_REDIS_");
}
private boolean isRedisLeaseProviderSelected() {
return "redis"
.equalsIgnoreCase(
environment.getProperty("ca-skeleton.capabilities.lease.provider", "disabled"));
}
private boolean isProdActive() {
// Case-insensitive: a typo such as SPRING_PROFILES_ACTIVE=PROD must still match.
for (String profile : environment.getActiveProfiles()) {
@@ -0,0 +1,151 @@
package dev.caskeleton.bootstrap.runtime.redis;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCanonicalConfig;
import dev.caskeleton.adapter.outbound.cache.redis.config.RedisRole;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.Role;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.RoleHealth;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.State;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/** Maps the framework-neutral Redis role snapshot into the bootstrap-owned health framework. */
@Configuration(proxyBeanMethods = false)
public class RedisHealthContributorConfig {
@Bean("redisRequired")
@Conditional(RequiredRedisRoleCondition.class)
HealthIndicator redisRequired(RedisHealthSnapshotProvider snapshots, Environment environment) {
Set<Role> expected = expectedRequiredRoles(environment);
return () -> requiredHealth(snapshots, expected);
}
@Bean("redisOptional")
@Conditional(CacheRedisRoleCondition.class)
HealthIndicator redisOptional(RedisHealthSnapshotProvider snapshots) {
return () -> optionalCacheHealth(snapshots);
}
private static Health requiredHealth(RedisHealthSnapshotProvider snapshots, Set<Role> expected) {
try {
List<RoleHealth> roles =
snapshots.snapshot().roles().stream()
.filter(role -> expected.contains(role.role()) && role.required())
.sorted(Comparator.comparing(RoleHealth::role))
.toList();
boolean complete =
roles.stream()
.map(RoleHealth::role)
.collect(java.util.stream.Collectors.toSet())
.containsAll(expected);
boolean available =
complete && roles.stream().allMatch(role -> role.state() == State.AVAILABLE);
Health.Builder health = available ? Health.up() : Health.down();
return health
.withDetail("state", available ? "AVAILABLE" : "UNAVAILABLE")
.withDetail("roles", details(roles))
.withDetail("missingRoles", missing(expected, roles))
.build();
} catch (RuntimeException exception) {
return Health.down()
.withDetail("state", "UNAVAILABLE")
.withDetail("reason", "SNAPSHOT_UNAVAILABLE")
.build();
}
}
private static Health optionalCacheHealth(RedisHealthSnapshotProvider snapshots) {
try {
List<RoleHealth> roles =
snapshots.snapshot().roles().stream()
.filter(role -> role.role() == Role.CACHE && !role.required())
.toList();
boolean available = roles.size() == 1 && roles.getFirst().state() == State.AVAILABLE;
return Health.up()
.withDetail("state", available ? "AVAILABLE" : "DEGRADED")
.withDetail("roles", details(roles))
.withDetail("missingRoles", roles.isEmpty() ? List.of(Role.CACHE.name()) : List.of())
.build();
} catch (RuntimeException exception) {
return Health.up()
.withDetail("state", "DEGRADED")
.withDetail("reason", "SNAPSHOT_UNAVAILABLE")
.build();
}
}
private static List<Map<String, Object>> details(List<RoleHealth> roles) {
List<Map<String, Object>> result = new ArrayList<>(roles.size());
for (RoleHealth role : roles) {
Map<String, Object> detail = new LinkedHashMap<>();
detail.put("role", role.role().name());
detail.put("capabilities", role.capabilities().stream().map(Enum::name).sorted().toList());
detail.put("state", role.state().name());
detail.put("reason", role.reason().name());
detail.put("semanticObservedAt", role.semanticObservedAt().toString());
detail.put("semanticAgeMillis", role.semanticAgeMillis());
detail.put("semanticStale", role.semanticStale());
detail.put("expectedEviction", role.expectedEviction().name());
detail.put("evictionAttestation", role.evictionAttestation().name());
detail.put("externalEvictionAttestation", "INCOMPLETE");
result.add(Map.copyOf(detail));
}
return List.copyOf(result);
}
private static List<String> missing(Set<Role> expected, List<RoleHealth> actual) {
EnumSet<Role> missing = EnumSet.noneOf(Role.class);
missing.addAll(expected);
actual.forEach(role -> missing.remove(role.role()));
return missing.stream().map(Enum::name).toList();
}
private static Set<Role> expectedRequiredRoles(Environment environment) {
EnumSet<Role> roles = EnumSet.noneOf(Role.class);
if (selected(environment, RedisRole.COORDINATION)) {
roles.add(Role.COORDINATION);
}
if (selected(environment, RedisRole.SESSION)) {
roles.add(Role.SESSION);
}
return Set.copyOf(roles);
}
private static boolean selected(Environment environment, RedisRole role) {
return !RedisCanonicalConfig.selectedCapabilities(environment)
.getOrDefault(role, Set.of())
.isEmpty();
}
static final class RequiredRedisRoleCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment environment = context.getEnvironment();
return selected(environment, RedisRole.COORDINATION)
|| selected(environment, RedisRole.SESSION);
}
}
static final class CacheRedisRoleCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return selected(context.getEnvironment(), RedisRole.CACHE);
}
}
}
@@ -0,0 +1,48 @@
package dev.caskeleton.bootstrap.security;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** Fails startup when JWT and Redis Session infrastructure are both present or both absent. */
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(AuthenticationModeSettings.class)
public class AuthenticationModeCompositionConfig {
@Bean
SmartInitializingSingleton authenticationModeCompositionValidator(
AuthenticationModeSettings settings, ListableBeanFactory beans) {
return () -> validate(settings.authMode(), beans);
}
static void validate(
AuthenticationModeSettings.AuthenticationMode mode, ListableBeanFactory beans) {
List<String> contradictions = new ArrayList<>();
boolean jwtDecoder = beans.containsBean("jwtDecoder");
boolean sessionRepository = beans.containsBean("redisVersionedSessionRepository");
boolean sessionFilter = beans.containsBean("springSessionRepositoryFilter");
if (mode == AuthenticationModeSettings.AuthenticationMode.JWT) {
if (!jwtDecoder) {
contradictions.add("jwtDecoder is absent");
}
if (sessionRepository || sessionFilter) {
contradictions.add("Redis Session repository/filter is active");
}
} else {
if (jwtDecoder) {
contradictions.add("jwtDecoder is active");
}
if (!sessionRepository || !sessionFilter) {
contradictions.add("Redis Session repository/filter is incomplete");
}
}
if (!contradictions.isEmpty()) {
throw new IllegalStateException(
"Authentication mode composition is not exclusive for " + mode + ": " + contradictions);
}
}
}
@@ -0,0 +1,19 @@
package dev.caskeleton.bootstrap.security;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
/** Composition-root authority for the exclusive application authentication mode. */
@ConfigurationProperties(prefix = "ca-skeleton.security")
public record AuthenticationModeSettings(AuthenticationMode authMode) {
@ConstructorBinding
public AuthenticationModeSettings(AuthenticationMode authMode) {
this.authMode = authMode == null ? AuthenticationMode.JWT : authMode;
}
public enum AuthenticationMode {
JWT,
REDIS_SESSION
}
}
@@ -233,6 +233,9 @@ management:
health:
# D8: never expose health details to unauthenticated callers.
show-details: when-authorized
# redisRequired exists only when a correctness role is bound. Keep the static group
# fail-closed for known names while allowing an absent conditional contributor.
validate-group-membership: false
# feature-runtime-health-lifecycle-contract: expose the Kubernetes-ready
# liveness/readiness/startup probe paths.
probes:
@@ -244,11 +247,11 @@ management:
liveness:
include: livenessState
# Readiness: ready to serve traffic AND all REQUIRED dependencies up.
# Required: db (primary DB — auto-contributed by Spring Boot DataSource).
# Optional: cache / broker / notification adapters are NOT in this group
# (they are conditional or optional per the dependency taxonomy).
# Required: db and redisRequired (only COORDINATION/SESSION role bindings).
# redisOptional is deliberately excluded: a CACHE outage is reported as degraded detail
# but never turns a healthy JVM or an otherwise-ready pod unavailable.
readiness:
include: readinessState,db
include: readinessState,db,redisRequired
# Startup: startup/migration validation complete.
# readinessState acts as the startup completion gate — it flips UP only
# after the context is fully initialized (Flyway migration included).
@@ -299,6 +302,126 @@ logging:
# Module-scoped knobs. Each block is bound into a *Settings @ConfigurationProperties
# record in the corresponding module, which is where allowed-value validation lives.
ca-skeleton:
# Canonical HTTP capability activation. Bindings are the sole activation SSOT: provider
# definitions alone are inert, and the current NOT_IMPLEMENTED readiness card rejects ACTIVE
# before any client/executor/pool resource can be created.
capabilities:
cache:
# Canonical semantic cache activation. Disabled by default; "redis" requires an active
# ca-skeleton.providers.redis.roles.cache binding and resolves HMAC material by reference.
bindings:
default: ${APP_CACHE_CANONICAL_DEFAULT_PROVIDER:disabled}
regions:
default:
key-hmac-secret-reference: secret://environment/APP_CACHE_REDIS_KEY_HMAC_SECRET
namespace-application: ${APP_NAME:ca-skeleton}
namespace-environment: ${APP_CACHE_REDIS_NAMESPACE_ENVIRONMENT:local}
semantic-region: ${APP_CACHE_REDIS_SEMANTIC_REGION:default}
hash-key-version: 1
key-version: 1
policy-revision: canonical-default-r1
positive-soft-ttl: ${APP_CACHE_REDIS_POSITIVE_SOFT_TTL:240s}
positive-hard-ttl: ${APP_CACHE_DEFAULT_TTL:300s}
negative-ttl: ${APP_CACHE_NEGATIVE_TTL:60s}
ttl-jitter: ${APP_CACHE_REDIS_TTL_JITTER:0.10}
maximum-value-bytes: 61440
l1:
enabled: ${APP_CACHE_REDIS_L1_ENABLED:false}
maximum-entries: ${APP_CACHE_REDIS_L1_MAXIMUM_ENTRIES:10000}
maximum-weight-bytes: ${APP_CACHE_REDIS_L1_MAXIMUM_WEIGHT_BYTES:67108864}
maximum-entry-weight-bytes: ${APP_CACHE_REDIS_L1_MAXIMUM_ENTRY_WEIGHT_BYTES:1048576}
time-to-live: ${APP_CACHE_REDIS_L1_TTL:30s}
generation-recheck-interval: ${APP_CACHE_REDIS_L1_GENERATION_RECHECK_INTERVAL:5s}
invalidation-queue-capacity: ${APP_CACHE_REDIS_L1_INVALIDATION_QUEUE_CAPACITY:1024}
idempotency:
# disabled | jdbc | redis. JDBC is the existing V1 provider; Redis is the owner-safe V2
# provider. They are mutually exclusive and no V1-to-V2 facade is inferred.
provider: ${APP_IDEMPOTENCY_PROVIDER:jdbc}
key-hmac-secret-reference: secret://environment/APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET
namespace-application: ${APP_NAME:ca-skeleton}
namespace-environment: ${APP_IDEMPOTENCY_REDIS_NAMESPACE_ENVIRONMENT:local}
hash-key-version: 1
key-version: 1
processing-lease: ${APP_IDEMPOTENCY_PROCESSING_LEASE:30s}
replay-ttl: ${APP_IDEMPOTENCY_TTL:24h}
failure-retention: ${APP_IDEMPOTENCY_FAILURE_RETENTION:24h}
response-codec-id: json-v2
policy-revision: request-replay-v2
lease:
# disabled | redis. This is EFFICIENCY_ONLY and never supplies fencing.
provider: ${APP_LEASE_PROVIDER:disabled}
key-hmac-secret-reference: secret://environment/APP_LEASE_REDIS_KEY_HMAC_SECRET
namespace-application: ${APP_NAME:ca-skeleton}
namespace-environment: ${APP_LEASE_REDIS_NAMESPACE_ENVIRONMENT:local}
hash-key-version: 1
key-version: 1
drift-budget: ${APP_LEASE_REDIS_DRIFT_BUDGET:10ms}
rate-limit:
# disabled | redis. This is the sole outbound provider activation selector.
provider: ${APP_RATE_LIMIT_PROVIDER:disabled}
failure-policy: ${APP_RATE_LIMIT_FAILURE_POLICY:fail-closed}
default-policy-id: ${APP_RATE_LIMIT_DEFAULT_POLICY_ID:api-default}
failure-retry-after: ${APP_RATE_LIMIT_FAILURE_RETRY_AFTER:100ms}
hash-key-version: ${APP_RATE_LIMIT_HASH_KEY_VERSION:1}
key-version: ${APP_RATE_LIMIT_KEY_VERSION:1}
key-hmac-secret-reference: secret://environment/APP_RATE_LIMIT_REDIS_KEY_HMAC_SECRET
namespace-application: ${APP_NAME:ca-skeleton}
namespace-environment: ${APP_RATE_LIMIT_REDIS_NAMESPACE_ENVIRONMENT:local}
policies:
api-default:
revision: ${APP_RATE_LIMIT_POLICY_REVISION:v1}
algorithm: ${APP_RATE_LIMIT_ALGORITHM:sliding-counter}
limit: ${APP_RATE_LIMIT_LIMIT:100}
window: ${APP_RATE_LIMIT_WINDOW:1s}
capacity: ${APP_RATE_LIMIT_CAPACITY:100}
refill-tokens: ${APP_RATE_LIMIT_REFILL_TOKENS:100}
refill-period: ${APP_RATE_LIMIT_REFILL_PERIOD:1s}
maximum-cost: ${APP_RATE_LIMIT_MAXIMUM_COST:10}
cleanup-grace: ${APP_RATE_LIMIT_CLEANUP_GRACE:5s}
maximum-clock-regression: ${APP_RATE_LIMIT_MAXIMUM_CLOCK_REGRESSION:250ms}
security:
redis-session:
key-hmac-secret-reference: secret://environment/APP_SESSION_REDIS_KEY_HMAC_SECRET
namespace-application: ${APP_NAME:ca-skeleton}
namespace-environment: ${APP_SESSION_REDIS_NAMESPACE_ENVIRONMENT:local}
hash-key-version: 1
key-version: 1
idle-timeout: ${APP_SESSION_IDLE_TIMEOUT:30m}
absolute-lifetime: ${APP_SESSION_ABSOLUTE_LIFETIME:8h}
touch-interval: ${APP_SESSION_TOUCH_INTERVAL:1m}
tombstone-time-to-live: ${APP_SESSION_TOMBSTONE_TTL:5m}
maximum-envelope-bytes: ${APP_SESSION_MAXIMUM_ENVELOPE_BYTES:32768}
maximum-attributes: ${APP_SESSION_MAXIMUM_ATTRIBUTES:64}
maximum-scalar-bytes: ${APP_SESSION_MAXIMUM_SCALAR_BYTES:8192}
http-client:
expected-state: DISABLED
bindings: {}
providers:
http-client: {}
redis:
# Definitions alone are inert. Environment-specific configuration must bind roles.
legacy-migration-enabled: false
deployments: {}
roles: {}
runtime:
client-name: canonical-redis
connect-timeout: 2s
tls-handshake-timeout: 3s
acquire-timeout: 2s
command-timeout: 2s
overall-timeout: 5s
shutdown-timeout: 3s
maximum-queued-commands: 64
cluster-maximum-redirects: 5
cluster-topology-refresh-period: 30s
maximum-in-flight-commands: 64
maximum-command-bytes: 65536
maximum-in-flight-bytes: 4194304
route-drain-timeout: 6s
default-write-ttl: 5m
sentinel-discovery-refresh-period: ${APP_REDIS_SENTINEL_DISCOVERY_REFRESH_PERIOD:30s}
semantic-probe-minimum-interval: ${APP_REDIS_SEMANTIC_PROBE_MINIMUM_INTERVAL:5s}
semantic-probe-maximum-staleness: ${APP_REDIS_SEMANTIC_PROBE_MAXIMUM_STALENESS:15s}
bootstrap:
# required, non-blank — startup fails if blank (see BootstrapSettings)
app-name: ${APP_NAME}
@@ -326,18 +449,6 @@ ca-skeleton:
# prefix "/v1" (major-version path, AIP-185); override via env, or set "" for
# no prefix. The supplemental "X-Api-Version" header never overrides the path.
api-base-path: ${PRESENTATION_API_BASE_PATH:/v1}
rate-limit:
# feature-rate-limit-idempotency-contract D1/§G. enabled is env-driven
# (restart-only); limit/window are the fixed-window mechanism's literal tuning
# parameters (no env key — UNSUPPORTED_IMPL per-key counter, single-node D5).
enabled: ${APP_RATE_LIMIT_ENABLED}
limit: 100
window: 1s
# RateLimiter strategy: fixed-window (default) | (extend: sliding-window | token-bucket)
algorithm: fixed-window
# Client IP source for unauthenticated rate-limit keys:
# remote-addr-only (safe default) | forwarded-headers-trusted (only behind trusted ingress/LB)
client-ip-mode: ${APP_RATE_LIMIT_CLIENT_IP_MODE:remote-addr-only}
idempotency:
# feature-rate-limit-idempotency-contract D6/§E. ttl is env-driven (<=72h,
# validated in IdempotencyProperties); reaper-interval is literal operational tuning.
@@ -372,12 +483,22 @@ ca-skeleton:
# read also by adapter-persistence OutboxReaper via ${ca-skeleton.outbox.published-retention:P7D}
published-retention: P7D
security:
# REQUIRED; startup fails if blank
# jwt | redis-session; the bootstrap composition validator rejects mixed infrastructure.
auth-mode: ${APP_SECURITY_AUTH_MODE:jwt}
# Required only in jwt mode.
issuer-uri: ${APP_SECURITY_JWT_ISSUER}
# blank to skip audience check
audience: ${APP_SECURITY_JWT_AUDIENCE}
# comma-separated list (Spring binds to List<String>)
public-paths: ${SECURITY_PUBLIC_PATHS}
session:
cookie-name: ${APP_SESSION_COOKIE_NAME:CA_SESSION}
secure: ${APP_SESSION_COOKIE_SECURE:true}
http-only: ${APP_SESSION_COOKIE_HTTP_ONLY:true}
same-site: ${APP_SESSION_COOKIE_SAME_SITE:Lax}
path: ${APP_SESSION_COOKIE_PATH:/}
csrf-cookie-name: ${APP_SESSION_CSRF_COOKIE_NAME:XSRF-TOKEN}
csrf-header-name: ${APP_SESSION_CSRF_HEADER_NAME:X-XSRF-TOKEN}
authz:
# feature-authentication-authorization-contract D2/D3/D8: app-side role→permission
# mapping (the default source; IdP-issued permission claims are an out-of-scope
@@ -466,6 +587,42 @@ ca-skeleton:
# (which needs its project-supplied integration client bean). Domain namespace, NOT a
# generic `app.adapter.*` prefix (branch-note §Audit A1). Env keys are the registry SSOT.
app:
# Fileserver R2 exact destination/provider composition. Disabled by default: while false,
# these blank attestation placeholders do not create directories, probe a filesystem, or
# contribute FilePublicationPort. Enabling fails closed unless every local-persistent
# attestation value matches the pre-provisioned root. No implicit local fallback exists.
fileserver:
enabled: ${APP_FILESERVER_ENABLED:false}
destinations:
local-export:
provider-ref: local-primary
required-publication: unique-atomic-create
required-durability: file-and-directory-sync
maximum-rows: 1000000
maximum-encoded-bytes: 1073741824
providers:
local-primary:
# local-persistent is the only implemented/qualified R2 provider.
# shared-mounted/NFS and SFTP settings must not be added before their providers exist.
type: local-persistent
root-directory: ${APP_FILESERVER_LOCAL_ROOT:}
auto-create: false
strict-path-security: true
expected-file-store-name: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_NAME:}
expected-file-store-type: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_TYPE:}
mount-sentinel-name: .ca-fileserver-volume
mount-sentinel-sha256: ${APP_FILESERVER_LOCAL_MOUNT_SENTINEL_SHA256:}
expected-owner: ${APP_FILESERVER_LOCAL_EXPECTED_OWNER:}
maximum-root-mode: "0750"
rate-limit:
# Inbound HTTP enforcement is a separate axis from outbound provider activation.
# enabled=true with no exact EdgeRateLimitPort fails fast; it never installs a local fallback.
enabled: ${APP_RATE_LIMIT_ENABLED:false}
default-policy-id: ${APP_RATE_LIMIT_DEFAULT_POLICY_ID:api-default}
hash-key-version: ${APP_RATE_LIMIT_HASH_KEY_VERSION:1}
caller-deadline-budget: 2s
# remote-addr-only | forwarded-headers-trusted (trusted ingress only)
client-ip-mode: ${APP_RATE_LIMIT_CLIENT_IP_MODE:remote-addr-only}
cache:
redis:
# true | false (boolean_strict). Redis cache adapter on/off.
@@ -481,11 +638,24 @@ app:
maximum-queued-commands: ${APP_CACHE_REDIS_MAXIMUM_QUEUED_COMMANDS:8}
maximum-in-flight-bytes: ${APP_CACHE_REDIS_MAXIMUM_IN_FLIGHT_BYTES:16777216}
positive-ttl: ${APP_CACHE_DEFAULT_TTL:300s}
# Blank derives 80% of positive-ttl in typed settings.
positive-soft-ttl: ${APP_CACHE_REDIS_POSITIVE_SOFT_TTL:}
negative-ttl: ${APP_CACHE_NEGATIVE_TTL:60s}
ttl-jitter: ${APP_CACHE_REDIS_TTL_JITTER:0.10}
minimum-hard-ttl: ${APP_CACHE_REDIS_MINIMUM_HARD_TTL:1s}
namespace-application: ${APP_NAME:ca-skeleton}
namespace-environment: ${APP_CACHE_REDIS_NAMESPACE_ENVIRONMENT:local}
semantic-region: ${APP_CACHE_REDIS_SEMANTIC_REGION:default}
maximum-value-bytes: ${APP_CACHE_REDIS_MAXIMUM_VALUE_BYTES:1048576}
# Optional cache-only L1. Never reuse for session, idempotency or strict rate-limit state.
l1:
enabled: ${APP_CACHE_REDIS_L1_ENABLED:false}
maximum-entries: ${APP_CACHE_REDIS_L1_MAXIMUM_ENTRIES:10000}
maximum-weight-bytes: ${APP_CACHE_REDIS_L1_MAXIMUM_WEIGHT_BYTES:67108864}
maximum-entry-weight-bytes: ${APP_CACHE_REDIS_L1_MAXIMUM_ENTRY_WEIGHT_BYTES:1048576}
time-to-live: ${APP_CACHE_REDIS_L1_TTL:30s}
generation-recheck-interval: ${APP_CACHE_REDIS_L1_GENERATION_RECHECK_INTERVAL:5s}
invalidation-queue-capacity: ${APP_CACHE_REDIS_L1_INVALIDATION_QUEUE_CAPACITY:1024}
# Logical-cache-name → backendId routing (CacheStoreRouter). No keys by default —
# forks add e.g. `bindings: { worklog: redis }` or env APP_CACHE_BINDINGS_WORKLOG=redis.
# A binding to a backend that is not enabled fails startup (Layer 3 moved to router).
@@ -504,44 +674,3 @@ app:
provider: ${APP_NOTIFICATION_SLACK_PROVIDER}
email:
provider: ${APP_NOTIFICATION_EMAIL_PROVIDER}
# ---------------------------------------------------------------------------
# feature-outbound-http-client-baseline D5/D3/D7
# Registry SSOT: docs/registries/env-keys.yaml (APP_OUTBOUND_HTTP_* rows 489570)
# Bound into OutboundHttpSettings @ConfigurationProperties(prefix = "app.outbound.http").
# ---------------------------------------------------------------------------
outbound:
http:
# duration (e.g. 2s). REQUIRED — non-zero (spring_duration_shorthand_non_zero).
connect-timeout: ${APP_OUTBOUND_HTTP_CONNECT_TIMEOUT}
# duration (e.g. 5s). REQUIRED — non-zero.
read-timeout: ${APP_OUTBOUND_HTTP_READ_TIMEOUT}
# duration (e.g. 10s). REQUIRED — non-zero (deadline budget for the whole call incl. retries).
global-call-timeout: ${APP_OUTBOUND_HTTP_GLOBAL_CALL_TIMEOUT}
# Per-client live worker bound; timed-out non-cooperative workers retain a slot until exit.
maximum-in-flight-calls: ${APP_OUTBOUND_HTTP_MAXIMUM_IN_FLIGHT_CALLS:128}
# true | false (boolean_strict). Resilience4j retry — default disabled (D3).
retry-enabled: ${APP_OUTBOUND_HTTP_RETRY_ENABLED:false}
# retry 튜닝 (retry-enabled=true 일 때 적용). 기본값 = 기존 하드코딩 동작 보존.
retry:
# int >= 1 (positive_int). 총 시도 횟수(최초 시도 포함).
max-attempts: ${APP_OUTBOUND_HTTP_RETRY_MAX_ATTEMPTS:3}
# duration (spring_duration_shorthand_non_zero). exponential backoff 시작 간격.
initial-backoff: ${APP_OUTBOUND_HTTP_RETRY_INITIAL_BACKOFF:100ms}
# double >= 1.0 (double_ge_1). exponential backoff 배수.
backoff-multiplier: ${APP_OUTBOUND_HTTP_RETRY_BACKOFF_MULTIPLIER:2.0}
# true | false (boolean_strict). Resilience4j circuit breaker — default disabled.
circuit-breaker-enabled: ${APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED:false}
# circuit-breaker 튜닝 (circuit-breaker-enabled=true 일 때 적용). 기본값 = Resilience4j ofDefaults().
circuit-breaker:
# float in (0, 100] (float_in_0_exclusive_to_100). open 전환 실패율 임계치(%).
failure-rate-threshold: ${APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_FAILURE_RATE_THRESHOLD:50}
# int >= 1 (positive_int). COUNT_BASED sliding window 크기.
sliding-window-size: ${APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_SLIDING_WINDOW_SIZE:100}
# int >= 1 (positive_int). 실패율 계산을 시작하는 최소 호출 수.
minimum-number-of-calls: ${APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_MINIMUM_NUMBER_OF_CALLS:100}
# duration (spring_duration_shorthand_non_zero). open 상태 유지 시간.
wait-duration-in-open-state: ${APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_WAIT_DURATION_IN_OPEN_STATE:60s}
# int >= 1 (positive_int). half-open 상태에서 허용하는 시험 호출 수.
permitted-calls-in-half-open: ${APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_PERMITTED_CALLS_IN_HALF_OPEN:10}
# data size (e.g. 10MB). Streaming threshold — buffered reads above this fail (D7).
response-size-limit: ${APP_OUTBOUND_HTTP_RESPONSE_SIZE_LIMIT:10MB}
@@ -0,0 +1,167 @@
package dev.caskeleton.adapter.outbound.cache.redis;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettingsFactory;
import dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings;
import dev.caskeleton.adapter.outbound.cache.redis.config.RedisRole;
import dev.caskeleton.adapter.outbound.cache.redis.config.RedisRoleBinding;
import dev.caskeleton.adapter.outbound.cache.redis.runtime.RedisClientRuntimeSettings;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.Capability;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.springframework.mock.env.MockEnvironment;
class RedisCoordinationRuntimeCompositionContractTest {
private static final RedisClientRuntimeSettings CLIENT_SETTINGS =
new RedisClientRuntimeSettings(
"composition-test",
Duration.ofMillis(100),
Duration.ofMillis(100),
Duration.ofMillis(200),
Duration.ofMillis(500),
Duration.ofMillis(300),
8,
3,
Duration.ofSeconds(5));
@Test
void twoCoordinationCapabilitiesCreateOneRuntimeAndOneRouter() {
MockEnvironment environment =
new MockEnvironment()
.withProperty("ca-skeleton.capabilities.rate-limit.provider", "redis")
.withProperty("ca-skeleton.capabilities.idempotency.provider", "redis");
Map<RedisRole, Set<Capability>> capabilities =
RedisCanonicalConfig.selectedCapabilities(environment);
AtomicInteger runtimeBuilds = new AtomicInteger();
try (RedisCanonicalRoleRegistry registry =
new RedisCanonicalRoleRegistry(
new RedisDeploymentSettingsFactory()
.compileActive(declaredProvider(), selectedRoles(capabilities)),
CLIENT_SETTINGS,
4,
16_384,
1_048_576,
Duration.ofSeconds(1),
Duration.ofMinutes(5),
deployment -> {
runtimeBuilds.incrementAndGet();
return new FakeRuntime(deployment.deploymentId());
},
declaredProvider().roles(),
capabilities,
java.time.Clock.systemUTC())) {
assertThat(capabilities.get(RedisRole.COORDINATION))
.containsExactlyInAnyOrder(Capability.RATE_LIMIT, Capability.IDEMPOTENCY);
assertThat(registry.boundRoles()).containsExactly(RedisRole.COORDINATION);
assertThat(runtimeBuilds).hasValue(1);
}
}
private static Set<RedisRole> selectedRoles(Map<RedisRole, Set<Capability>> capabilities) {
return capabilities.entrySet().stream()
.filter(entry -> !entry.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
private static RedisProviderSettings declaredProvider() {
return new RedisProviderSettings(
Map.of(
"cache-main", standalone("cache-main"),
"coord-main", standalone("coord-main"),
"session-main", standalone("session-main")),
Map.of(
RedisRole.CACHE, new RedisRoleBinding("cache-main", false, "allkeys-lfu"),
RedisRole.COORDINATION, new RedisRoleBinding("coord-main", true, "noeviction"),
RedisRole.SESSION, new RedisRoleBinding("session-main", true, "noeviction")));
}
private static RedisProviderSettings.DeploymentProperties standalone(String deploymentId) {
return new RedisProviderSettings.DeploymentProperties(
RedisProviderSettings.Topology.STANDALONE,
new RedisProviderSettings.StandaloneProperties(
List.of(
new RedisProviderSettings.EndpointProperties(deploymentId + ".internal", 6379))),
null,
null,
0,
new RedisProviderSettings.AuthenticationProperties(
"runtime", "secret://environment/REDIS_PASSWORD"),
new RedisProviderSettings.TlsProperties(
true, true, "secret://environment/REDIS_TRUST_PEM"));
}
private static final class FakeRuntime implements RedisRoutableCommandRuntime {
private final String deploymentId;
private final Map<RedisPhysicalKey, byte[]> values = new java.util.HashMap<>();
private FakeRuntime(String deploymentId) {
this.deploymentId = deploymentId;
}
@Override
public void probe(Duration timeout) {}
@Override
public String deploymentId() {
return deploymentId;
}
@Override
public byte[] get(RedisPhysicalKey key) {
byte[] value = values.get(key);
return value == null ? null : value.clone();
}
@Override
public void set(RedisPhysicalKey key, RedisBinaryValue value, Duration timeToLive) {
values.put(key, value.copyEncoded());
}
@Override
public long delete(RedisPhysicalKey key) {
return values.remove(key) == null ? 0 : 1;
}
@Override
public RedisCatalogProgramReply executeCatalogProgram(
RedisCatalogProgramInvocation invocation) {
RedisProgramId programId = invocation.programIdOrNull();
if (programId == null) {
return RedisCatalogProgramReply.value(
"ACL_OK".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
}
return switch (programId) {
case RATE_FIXED_WINDOW_V2 ->
RedisCatalogProgramReply.multi(
ascii("STATE_INCOMPATIBLE", "NONE", "1", "1", "1", "0", "0", "0"));
case IDEMPOTENCY_CLAIM_V1 ->
RedisCatalogProgramReply.multi(ascii("STATE_INCOMPATIBLE", "0", "0", "-", "-", "-"));
default -> throw new AssertionError("unexpected semantic program " + programId);
};
}
@Override
public String loadCatalogProgram(RedisCatalogProgramInvocation invocation) {
return invocation.sha1();
}
@Override
public void close() {}
private static List<byte[]> ascii(String... fields) {
return java.util.Arrays.stream(fields)
.map(field -> field.getBytes(java.nio.charset.StandardCharsets.US_ASCII))
.toList();
}
}
}
@@ -0,0 +1,138 @@
package dev.caskeleton.adapter.outbound.cache.redis;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.cache.redis.security.DestroyableRedisSecret;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisCredentialMaterialProvider;
import dev.caskeleton.adapter.outbound.cache.redis.security.VersionedRedisCredentialMaterial;
import dev.caskeleton.bootstrap.runtime.redis.RedisHealthContributorConfig;
import java.time.Instant;
import java.util.Base64;
import org.junit.jupiter.api.Test;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class RedisOptionalCacheColdStartCompositionTest {
@Test
void selectedOptionalCacheStartsItsRuntimeCacheBeanAndDegradedHealthOnTypedTransientOutage() {
new ApplicationContextRunner()
.withUserConfiguration(
RedisCanonicalConfig.class,
RedisCanonicalCacheConfig.class,
RedisHealthContributorConfig.class)
.withBean(
RedisRuntimeConnector.class,
() ->
deployment -> {
throw new RedisTemporaryConnectionException();
})
.withBean(
RedisCredentialMaterialProvider.class,
RedisOptionalCacheColdStartCompositionTest::secretProvider)
.withPropertyValues(optionalCacheProperties())
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("redisCanonicalRoleRegistry");
assertThat(context).hasBean("redisCanonicalDefaultCacheRegion");
assertThat(context).hasBean("redisOptional");
assertThat(context).doesNotHaveBean("redisRequired");
assertThat(
context.getBean("redisOptional", HealthIndicator.class).health().toString())
.contains("DEGRADED", "COMMAND_UNAVAILABLE")
.doesNotContain("cache.internal");
});
}
@Test
void requiredTransientAndOptionalPermanentConnectorFailuresStillFailTheContext() {
new ApplicationContextRunner()
.withUserConfiguration(RedisCanonicalConfig.class, RedisHealthContributorConfig.class)
.withBean(
RedisRuntimeConnector.class,
() ->
deployment -> {
throw new RedisTemporaryConnectionException();
})
.withPropertyValues(requiredCoordinationProperties())
.run(context -> assertThat(context).hasFailed());
new ApplicationContextRunner()
.withUserConfiguration(RedisCanonicalConfig.class, RedisHealthContributorConfig.class)
.withBean(
RedisRuntimeConnector.class,
() ->
deployment -> {
throw new IllegalStateException("permanent authentication failure");
})
.withPropertyValues(optionalCacheProperties())
.run(context -> assertThat(context).hasFailed());
}
@Test
void duplicateRuntimeConnectorSeamsFailClosedInsteadOfChoosingSilently() {
RedisRuntimeConnector first =
deployment -> {
throw new RedisTemporaryConnectionException();
};
RedisRuntimeConnector second =
deployment -> {
throw new RedisTemporaryConnectionException();
};
new ApplicationContextRunner()
.withUserConfiguration(RedisCanonicalConfig.class)
.withBean("firstRedisRuntimeConnector", RedisRuntimeConnector.class, () -> first)
.withBean("secondRedisRuntimeConnector", RedisRuntimeConnector.class, () -> second)
.withPropertyValues(optionalCacheProperties())
.run(context -> assertThat(context).hasFailed());
}
private static RedisCredentialMaterialProvider secretProvider() {
byte[] raw = new byte[32];
java.util.Arrays.fill(raw, (byte) 7);
char[] encoded = Base64.getEncoder().encodeToString(raw).toCharArray();
java.util.Arrays.fill(raw, (byte) 0);
return reference ->
new VersionedRedisCredentialMaterial(
"composition-v1",
Instant.parse("2030-01-01T00:00:00Z"),
DestroyableRedisSecret.from(encoded));
}
private static String[] optionalCacheProperties() {
return new String[] {
"ca-skeleton.capabilities.cache.bindings.default=redis",
"ca-skeleton.capabilities.cache.regions.default.key-hmac-secret-reference=secret://environment/CACHE_KEY_HMAC",
"ca-skeleton.providers.redis.deployments.cache-main.topology=standalone",
"ca-skeleton.providers.redis.deployments.cache-main.standalone.endpoints[0].host=cache.internal",
"ca-skeleton.providers.redis.deployments.cache-main.standalone.endpoints[0].port=6379",
"ca-skeleton.providers.redis.deployments.cache-main.authentication.username=cache-runtime",
"ca-skeleton.providers.redis.deployments.cache-main.authentication.password-reference=secret://environment/CACHE_PASSWORD",
"ca-skeleton.providers.redis.deployments.cache-main.tls.enabled=true",
"ca-skeleton.providers.redis.deployments.cache-main.tls.verify-hostname=true",
"ca-skeleton.providers.redis.deployments.cache-main.tls.trust-bundle-reference=secret://environment/CACHE_TRUST",
"ca-skeleton.providers.redis.roles.cache.deployment-id=cache-main",
"ca-skeleton.providers.redis.roles.cache.required=false",
"ca-skeleton.providers.redis.roles.cache.expected-eviction=allkeys-lfu"
};
}
private static String[] requiredCoordinationProperties() {
return new String[] {
"ca-skeleton.capabilities.rate-limit.provider=redis",
"ca-skeleton.providers.redis.deployments.coord-main.topology=standalone",
"ca-skeleton.providers.redis.deployments.coord-main.standalone.endpoints[0].host=coord.internal",
"ca-skeleton.providers.redis.deployments.coord-main.standalone.endpoints[0].port=6379",
"ca-skeleton.providers.redis.deployments.coord-main.authentication.username=coord-runtime",
"ca-skeleton.providers.redis.deployments.coord-main.authentication.password-reference=secret://environment/COORD_PASSWORD",
"ca-skeleton.providers.redis.deployments.coord-main.tls.enabled=true",
"ca-skeleton.providers.redis.deployments.coord-main.tls.verify-hostname=true",
"ca-skeleton.providers.redis.deployments.coord-main.tls.trust-bundle-reference=secret://environment/COORD_TRUST",
"ca-skeleton.providers.redis.roles.coordination.deployment-id=coord-main",
"ca-skeleton.providers.redis.roles.coordination.required=true",
"ca-skeleton.providers.redis.roles.coordination.expected-eviction=noeviction"
};
}
}
@@ -0,0 +1,232 @@
package dev.caskeleton.bootstrap.redis;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.inbound.web.auth.RedisSessionWebConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCacheAdapterConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCanonicalCacheConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCanonicalConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisEfficiencyLeaseConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisIdempotencyConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisRateLimitConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisSessionConfig;
import dev.caskeleton.adapter.outbound.cache.redis.config.RedisRole;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisCredentialMaterialProvider;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisTrustMaterialProvider;
import dev.caskeleton.application.idempotency.IdempotencyStorePortV2;
import dev.caskeleton.application.lease.DistributedLeasePort;
import dev.caskeleton.bootstrap.runtime.redis.RedisHealthContributorConfig;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.Capability;
import dev.caskeleton.shared.ratelimit.EdgeRateLimitPort;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.mock.env.MockEnvironment;
class RedisCanonicalCompositionContractTest {
@Test
void everyRedisSelectorActivatesOnlyItsCanonicalRole() {
Map<String, ExpectedActivation> selections =
Map.of(
"ca-skeleton.capabilities.cache.bindings.default=redis",
new ExpectedActivation(RedisRole.CACHE, Capability.CACHE),
"ca-skeleton.capabilities.rate-limit.provider=redis",
new ExpectedActivation(RedisRole.COORDINATION, Capability.RATE_LIMIT),
"ca-skeleton.capabilities.idempotency.provider=redis",
new ExpectedActivation(RedisRole.COORDINATION, Capability.IDEMPOTENCY),
"ca-skeleton.capabilities.lease.provider=redis",
new ExpectedActivation(RedisRole.COORDINATION, Capability.EFFICIENCY_LEASE),
"ca-skeleton.security.auth-mode=redis-session",
new ExpectedActivation(RedisRole.SESSION, Capability.SESSION));
selections.forEach(
(selector, expected) -> {
MockEnvironment environment = declaredRoleEnvironment();
String[] selectorParts = selector.split("=", 2);
environment.setProperty(selectorParts[0], selectorParts[1]);
Map<RedisRole, Set<Capability>> active =
RedisCanonicalConfig.selectedCapabilities(environment);
assertThat(active.get(expected.role())).containsExactly(expected.capability());
active.forEach(
(role, capabilities) -> {
if (role != expected.role()) {
assertThat(capabilities).isEmpty();
}
});
});
}
@Test
void declaredRolesRemainFullyInertUntilACapabilitySelectsRedis() {
AtomicInteger credentialResolutions = new AtomicInteger();
AtomicInteger trustResolutions = new AtomicInteger();
new ApplicationContextRunner()
.withUserConfiguration(
RedisCanonicalConfig.class,
RedisCanonicalCacheConfig.class,
RedisCacheAdapterConfig.class,
RedisEfficiencyLeaseConfig.class,
RedisIdempotencyConfig.class,
RedisRateLimitConfig.class,
RedisSessionConfig.class,
RedisSessionWebConfig.class,
RedisHealthContributorConfig.class)
.withBean(
RedisCredentialMaterialProvider.class,
() ->
reference -> {
credentialResolutions.incrementAndGet();
throw new AssertionError("unselected Redis role resolved credential material");
})
.withBean(
RedisTrustMaterialProvider.class,
() ->
reference -> {
trustResolutions.incrementAndGet();
throw new AssertionError("unselected Redis role resolved trust material");
})
.withPropertyValues(disabledCapabilityProperties())
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context.getBean(RedisHealthSnapshotProvider.class).snapshot().roles())
.isEmpty();
assertThat(context.getBeansOfType(DistributedLeasePort.class)).isEmpty();
assertThat(context.getBeansOfType(IdempotencyStorePortV2.class)).isEmpty();
assertThat(context.getBeansOfType(EdgeRateLimitPort.class)).isEmpty();
assertThat(context)
.doesNotHaveBean("redisCanonicalDefaultCacheRegion")
.doesNotHaveBean("redisCanonicalDefaultCacheInvalidationSubscription")
.doesNotHaveBean("redisLuaVersionedSessionStore")
.doesNotHaveBean("redisVersionedSessionRepository")
.doesNotHaveBean("springSessionRepositoryFilter")
.doesNotHaveBean("redisRequired")
.doesNotHaveBean("redisOptional");
assertThat(context.getBeanFactory().getBeanDefinitionNames())
.allSatisfy(
beanName -> {
Class<?> beanType = context.getBeanFactory().getType(beanName, false);
assertThat(beanType == null ? "" : beanType.getName())
.doesNotStartWith("io.lettuce.");
});
assertThat(credentialResolutions).hasValue(0);
assertThat(trustResolutions).hasValue(0);
});
}
@Test
void unboundProviderDefinitionsResolveNoMaterialAndOpenNoNativeClient() {
AtomicInteger credentialResolutions = new AtomicInteger();
AtomicInteger trustResolutions = new AtomicInteger();
RedisCredentialMaterialProvider credentialProvider =
reference -> {
credentialResolutions.incrementAndGet();
throw new AssertionError("unbound Redis deployment resolved credential material");
};
RedisTrustMaterialProvider trustProvider =
reference -> {
trustResolutions.incrementAndGet();
throw new AssertionError("unbound Redis deployment resolved trust material");
};
new ApplicationContextRunner()
.withUserConfiguration(
RedisCanonicalConfig.class,
RedisEfficiencyLeaseConfig.class,
RedisIdempotencyConfig.class,
RedisRateLimitConfig.class)
.withBean(RedisCredentialMaterialProvider.class, () -> credentialProvider)
.withBean(RedisTrustMaterialProvider.class, () -> trustProvider)
.withPropertyValues(
"ca-skeleton.providers.redis.deployments.unused.topology=standalone",
"ca-skeleton.providers.redis.deployments.unused.standalone.endpoints[0].host=unused.invalid",
"ca-skeleton.providers.redis.deployments.unused.standalone.endpoints[0].port=6379",
"ca-skeleton.providers.redis.deployments.unused.database=0",
"ca-skeleton.providers.redis.deployments.unused.authentication.username=unused-runtime",
"ca-skeleton.providers.redis.deployments.unused.authentication.password-reference=secret://environment/APP_CACHE_REDIS_PASSWORD",
"ca-skeleton.providers.redis.deployments.unused.tls.enabled=true",
"ca-skeleton.providers.redis.deployments.unused.tls.verify-hostname=true",
"ca-skeleton.providers.redis.deployments.unused.tls.trust-bundle-reference=secret://environment/APP_CACHE_REDIS_TRUST_PEM")
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("redisCanonicalRoleRegistry");
assertThat(context.getBeansOfType(DistributedLeasePort.class)).isEmpty();
assertThat(context.getBeansOfType(IdempotencyStorePortV2.class)).isEmpty();
assertThat(context.getBeansOfType(EdgeRateLimitPort.class)).isEmpty();
assertThat(context.getBeanFactory().getBeanDefinitionNames())
.allSatisfy(
beanName -> {
Class<?> beanType = context.getBeanFactory().getType(beanName, false);
assertThat(beanType == null ? "" : beanType.getName())
.doesNotStartWith("io.lettuce.");
});
assertThat(credentialResolutions).hasValue(0);
assertThat(trustResolutions).hasValue(0);
});
}
private static String[] disabledCapabilityProperties() {
return new String[] {
"ca-skeleton.capabilities.cache.bindings.default=disabled",
"ca-skeleton.capabilities.rate-limit.provider=disabled",
"ca-skeleton.capabilities.idempotency.provider=jdbc",
"ca-skeleton.capabilities.lease.provider=disabled",
"ca-skeleton.security.auth-mode=jwt",
"ca-skeleton.providers.redis.deployments.cache-main.topology=standalone",
"ca-skeleton.providers.redis.deployments.cache-main.standalone.endpoints[0].host=cache.internal",
"ca-skeleton.providers.redis.deployments.cache-main.standalone.endpoints[0].port=6379",
"ca-skeleton.providers.redis.deployments.cache-main.database=0",
"ca-skeleton.providers.redis.deployments.cache-main.authentication.username=cache-runtime",
"ca-skeleton.providers.redis.deployments.cache-main.authentication.password-reference=secret://environment/APP_CACHE_REDIS_PASSWORD",
"ca-skeleton.providers.redis.deployments.cache-main.tls.enabled=true",
"ca-skeleton.providers.redis.deployments.cache-main.tls.verify-hostname=true",
"ca-skeleton.providers.redis.deployments.cache-main.tls.trust-bundle-reference=secret://environment/APP_CACHE_REDIS_TRUST_PEM",
"ca-skeleton.providers.redis.deployments.coord-main.topology=standalone",
"ca-skeleton.providers.redis.deployments.coord-main.standalone.endpoints[0].host=coord.internal",
"ca-skeleton.providers.redis.deployments.coord-main.standalone.endpoints[0].port=6379",
"ca-skeleton.providers.redis.deployments.coord-main.database=0",
"ca-skeleton.providers.redis.deployments.coord-main.authentication.username=coord-runtime",
"ca-skeleton.providers.redis.deployments.coord-main.authentication.password-reference=secret://environment/APP_RATE_LIMIT_REDIS_PASSWORD",
"ca-skeleton.providers.redis.deployments.coord-main.tls.enabled=true",
"ca-skeleton.providers.redis.deployments.coord-main.tls.verify-hostname=true",
"ca-skeleton.providers.redis.deployments.coord-main.tls.trust-bundle-reference=secret://environment/APP_RATE_LIMIT_REDIS_TRUST_PEM",
"ca-skeleton.providers.redis.deployments.session-main.topology=standalone",
"ca-skeleton.providers.redis.deployments.session-main.standalone.endpoints[0].host=session.internal",
"ca-skeleton.providers.redis.deployments.session-main.standalone.endpoints[0].port=6379",
"ca-skeleton.providers.redis.deployments.session-main.database=0",
"ca-skeleton.providers.redis.deployments.session-main.authentication.username=session-runtime",
"ca-skeleton.providers.redis.deployments.session-main.authentication.password-reference=secret://environment/APP_SESSION_REDIS_PASSWORD",
"ca-skeleton.providers.redis.deployments.session-main.tls.enabled=true",
"ca-skeleton.providers.redis.deployments.session-main.tls.verify-hostname=true",
"ca-skeleton.providers.redis.deployments.session-main.tls.trust-bundle-reference=secret://environment/APP_SESSION_REDIS_TRUST_PEM",
"ca-skeleton.providers.redis.roles.cache.deployment-id=cache-main",
"ca-skeleton.providers.redis.roles.cache.required=false",
"ca-skeleton.providers.redis.roles.cache.expected-eviction=allkeys-lfu",
"ca-skeleton.providers.redis.roles.coordination.deployment-id=coord-main",
"ca-skeleton.providers.redis.roles.coordination.required=true",
"ca-skeleton.providers.redis.roles.coordination.expected-eviction=noeviction",
"ca-skeleton.providers.redis.roles.session.deployment-id=session-main",
"ca-skeleton.providers.redis.roles.session.required=true",
"ca-skeleton.providers.redis.roles.session.expected-eviction=noeviction"
};
}
private static MockEnvironment declaredRoleEnvironment() {
MockEnvironment environment = new MockEnvironment();
for (String property : disabledCapabilityProperties()) {
String[] parts = property.split("=", 2);
environment.setProperty(parts[0], parts[1]);
}
return environment;
}
private record ExpectedActivation(RedisRole role, Capability capability) {}
}
@@ -0,0 +1,140 @@
package dev.caskeleton.bootstrap.redis;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
class RedisCiAggregatorContractTest {
private static final Set<String> BLOCKING_JOBS =
Set.of("quality-gates", "sample-off", "gate-matrix-lint", "redis-standalone");
@Test
void releaseAggregatorNeedsAndChecksEveryBlockingJob() throws IOException {
String workflow =
Files.readString(repositoryRoot().resolve(".github/workflows/ci-quality-gates.yml"));
String releaseGate = jobBody(workflow, "release-gate");
assertThat(needs(releaseGate)).containsExactlyInAnyOrderElementsOf(BLOCKING_JOBS);
assertThat(releaseGate)
.contains("QUALITY_RESULT: ${{ needs.quality-gates.result }}")
.contains("SAMPLE_OFF_RESULT: ${{ needs.sample-off.result }}")
.contains("MATRIX_RESULT: ${{ needs.gate-matrix-lint.result }}")
.contains("REDIS_RESULT: ${{ needs.redis-standalone.result }}")
.contains("\"${REDIS_RESULT}\"");
}
@Test
void readinessWorkflowUsesStrictGradleMatrixAndReconcilesSanitizedEvidence() throws IOException {
String workflow =
Files.readString(
repositoryRoot().resolve(".github/workflows/redis-production-readiness.yml"));
String resolver = jobBody(workflow, "resolve-redis-readiness");
assertThat(resolver)
.contains("./gradlew writeRedisCiMatrix")
.contains("redis-readiness-matrix.json")
.doesNotContain("registry.read_text");
for (String jobId :
Set.of(
"redis-security",
"redis-sentinel",
"redis-cluster",
"redis-fault",
"redis-compatibility",
"selected-card-readiness",
"redis-all-candidates")) {
assertThat(jobBody(workflow, jobId))
.as("sanitized evidence upload for %s", jobId)
.contains("id: redis-tests")
.contains("id: redis-evidence-sanitizer")
.contains("if: always()")
.contains("steps.redis-evidence-sanitizer.outcome == 'success'")
.contains("build/redis-evidence")
.contains("if-no-files-found: error")
.doesNotContain("build/test-results")
.doesNotContain("build/reports/tests")
.doesNotContain("container logs");
}
assertThat(jobBody(workflow, "selected-card-readiness"))
.contains("name: redis-selected-${{ matrix.cardId }}");
for (String jobId :
Set.of(
"resolve-redis-readiness",
"redis-security",
"redis-sentinel",
"redis-cluster",
"redis-fault",
"redis-compatibility",
"redis-all-candidates",
"redis-production-readiness")) {
assertThat(jobBody(workflow, jobId))
.as("selected artifact name ownership for %s", jobId)
.doesNotContain("name: redis-selected-");
}
String readiness = jobBody(workflow, "redis-production-readiness");
assertThat(readiness)
.contains("if: ${{ always() && needs.resolve-redis-readiness.result == 'success' }}")
.contains("Require the exact selected matrix result")
.contains("selected-card-readiness result mismatch")
.contains("Record the downloaded selected artifact inventory")
.contains("downloaded selected artifact inventory mismatch")
.contains("redis-ci-result.json")
.contains("actions/download-artifact")
.contains("name: redis-readiness-control")
.contains("pattern: redis-selected-*")
.contains("verifyRedisSelectedEvidenceArtifacts")
.contains("redisProductionReadiness")
.contains("-PredisCiResultFile=")
.contains("needs.resolve-redis-readiness.outputs.selected_count == '0'")
.contains("expected_result = \"skipped\" if selected_count == 0 else \"success\"")
.contains("needs.resolve-redis-readiness.outputs.selected_count != '0'")
.contains("needs.selected-card-readiness.result == 'success'");
}
private static Set<String> needs(String job) {
Set<String> result = new LinkedHashSet<>();
boolean inNeeds = false;
for (String line : job.lines().toList()) {
if (line.equals(" needs:")) {
inNeeds = true;
continue;
}
if (inNeeds && line.matches(" - [a-z0-9-]+")) {
result.add(line.substring(line.indexOf('-') + 1).trim());
} else if (inNeeds && !line.isBlank()) {
break;
}
}
return result;
}
private static String jobBody(String workflow, String jobId) {
Pattern pattern =
Pattern.compile(
"(?ms)^ " + Pattern.quote(jobId) + ":\\n(.*?)(?=^ [a-zA-Z0-9_-]+:\\n|\\z)");
Matcher matcher = pattern.matcher(workflow);
assertThat(matcher.find()).as("workflow job %s", jobId).isTrue();
return " " + jobId + ":\n" + matcher.group(1);
}
private static Path repositoryRoot() {
Path current = Path.of("").toAbsolutePath().normalize();
while (current != null && !Files.isDirectory(current.resolve(".github/workflows"))) {
current = current.getParent();
}
if (current == null) {
throw new IllegalStateException("repository root containing .github/workflows was not found");
}
return current;
}
}
@@ -0,0 +1,139 @@
package dev.caskeleton.bootstrap.redis;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.inbound.web.auth.RedisSessionWebConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCacheAdapterConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCanonicalCacheConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCanonicalConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisEfficiencyLeaseConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisIdempotencyConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisRateLimitConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisSessionConfig;
import dev.caskeleton.bootstrap.runtime.redis.RedisHealthContributorConfig;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
class RedisDefaultActivationContractTest {
@Test
void shippedLocalEnvironmentDoesNotEnableTransportWithoutAProvider() throws IOException {
Path root = repositoryRoot();
Map<String, String> environment =
Files.readAllLines(root.resolve("src/.env")).stream()
.filter(line -> line.matches("[A-Z][A-Z0-9_]*=.*"))
.map(line -> line.split("=", 2))
.collect(Collectors.toMap(parts -> parts[0], parts -> parts[1]));
assertThat(environment)
.containsEntry("APP_RATE_LIMIT_ENABLED", "false")
.containsEntry("APP_RATE_LIMIT_PROVIDER", "disabled");
}
@Test
void shippedConfigurationParsesUniquelyAndBootsWithoutRedisActivation() throws IOException {
Path root = repositoryRoot();
Map<String, String> environment = shippedEnvironment(root);
Map<String, Object> environmentProperties = new LinkedHashMap<>(environment);
Path applicationYaml = root.resolve("src/app-bootstrap/src/main/resources/application.yml");
Map<String, Object> application = parsedYaml(applicationYaml);
List<PropertySource<?>> configuration =
new YamlPropertySourceLoader()
.load("shipped-application", new FileSystemResource(applicationYaml));
Map<String, Object> caSkeleton = child(application, "ca-skeleton");
Map<String, Object> capabilities = child(caSkeleton, "capabilities");
Map<String, Object> rateLimit = child(capabilities, "rate-limit");
assertThat(caSkeleton).doesNotContainKey("rate-limit");
assertThat(rateLimit)
.containsEntry("provider", "${APP_RATE_LIMIT_PROVIDER:disabled}")
.doesNotContainKey("algorithm");
new ApplicationContextRunner()
.withInitializer(
context -> {
for (PropertySource<?> source : configuration) {
context.getEnvironment().getPropertySources().addLast(source);
}
context
.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("shipped-env", environmentProperties));
})
.withUserConfiguration(
RedisCanonicalConfig.class,
RedisCanonicalCacheConfig.class,
RedisCacheAdapterConfig.class,
RedisEfficiencyLeaseConfig.class,
RedisIdempotencyConfig.class,
RedisRateLimitConfig.class,
RedisSessionConfig.class,
RedisSessionWebConfig.class,
RedisHealthContributorConfig.class)
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context.getBean(RedisHealthSnapshotProvider.class).snapshot().roles())
.isEmpty();
assertThat(context)
.doesNotHaveBean("redisCanonicalDefaultCacheRegion")
.doesNotHaveBean("redisCanonicalDefaultCacheInvalidationSubscription")
.doesNotHaveBean("distributedRateLimiter")
.doesNotHaveBean("redisIdempotencyStoreV2")
.doesNotHaveBean("distributedLeasePort")
.doesNotHaveBean("redisLuaVersionedSessionStore")
.doesNotHaveBean("redisVersionedSessionRepository")
.doesNotHaveBean("springSessionRepositoryFilter")
.doesNotHaveBean("redisRequired")
.doesNotHaveBean("redisOptional");
});
}
private static Map<String, String> shippedEnvironment(Path root) throws IOException {
return Files.readAllLines(root.resolve("src/.env")).stream()
.filter(line -> line.matches("[A-Z][A-Z0-9_]*=.*"))
.map(line -> line.split("=", 2))
.collect(Collectors.toMap(parts -> parts[0], parts -> parts[1]));
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parsedYaml(Path path) throws IOException {
LoaderOptions options = new LoaderOptions();
options.setAllowDuplicateKeys(false);
try (var reader = Files.newBufferedReader(path)) {
return (Map<String, Object>) new Yaml(new SafeConstructor(options)).load(reader);
}
}
@SuppressWarnings("unchecked")
private static Map<String, Object> child(Map<String, Object> parent, String key) {
assertThat(parent).containsKey(key);
return (Map<String, Object>) parent.get(key);
}
private static Path repositoryRoot() {
Path current = Path.of("").toAbsolutePath().normalize();
while (current != null && !Files.isDirectory(current.resolve(".github/workflows"))) {
current = current.getParent();
}
if (current == null) {
throw new IllegalStateException("repository root containing .github/workflows was not found");
}
return current;
}
}
@@ -0,0 +1,203 @@
package dev.caskeleton.bootstrap.redis;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyReaper;
import dev.caskeleton.adapter.outbound.persistence.idempotency.IdempotencyStoreAdapter;
import dev.caskeleton.application.idempotency.IdempotencyClaimAttempt;
import dev.caskeleton.application.idempotency.IdempotencyClaimOutcome;
import dev.caskeleton.application.idempotency.IdempotencyClaimRequest;
import dev.caskeleton.application.idempotency.IdempotencyCompleteOutcome;
import dev.caskeleton.application.idempotency.IdempotencyExecutor;
import dev.caskeleton.application.idempotency.IdempotencyExecutorV2;
import dev.caskeleton.application.idempotency.IdempotencyFailOutcome;
import dev.caskeleton.application.idempotency.IdempotencyFailureDisposition;
import dev.caskeleton.application.idempotency.IdempotencyInspection;
import dev.caskeleton.application.idempotency.IdempotencyInspectionRequest;
import dev.caskeleton.application.idempotency.IdempotencyOwner;
import dev.caskeleton.application.idempotency.IdempotencyRecord;
import dev.caskeleton.application.idempotency.IdempotencyReleaseOutcome;
import dev.caskeleton.application.idempotency.IdempotencyRenewOutcome;
import dev.caskeleton.application.idempotency.IdempotencyScope;
import dev.caskeleton.application.idempotency.IdempotencyStartOutcome;
import dev.caskeleton.application.idempotency.IdempotencyStorePort;
import dev.caskeleton.application.idempotency.IdempotencyStorePortV2;
import dev.caskeleton.application.idempotency.RequestFingerprint;
import dev.caskeleton.application.idempotency.StoredResponse;
import dev.caskeleton.bootstrap.idempotency.IdempotencyProviderSelectionConfig;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class RedisIdempotencyProviderSelectionContractTest {
private final ApplicationContextRunner runner =
new ApplicationContextRunner()
.withUserConfiguration(IdempotencyProviderSelectionConfig.class);
@Test
void jdbcAndRedisModesEachRequireExactlyTheirOwnVersionedPair() {
IdempotencyStorePort jdbcStore = new NoOpJdbcStore();
runner
.withPropertyValues("ca-skeleton.capabilities.idempotency.provider=jdbc")
.withBean(IdempotencyStorePort.class, () -> jdbcStore)
.withBean(
IdempotencyExecutor.class,
() -> new IdempotencyExecutor(jdbcStore, Clock.systemUTC(), Duration.ofHours(1)))
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(IdempotencyStorePort.class);
assertThat(context.getBeansOfType(IdempotencyStorePortV2.class)).isEmpty();
});
IdempotencyStorePortV2 redisStore = new NoOpRedisStore();
runner
.withPropertyValues("ca-skeleton.capabilities.idempotency.provider=redis")
.withBean(IdempotencyStorePortV2.class, () -> redisStore)
.withBean(
IdempotencyExecutorV2.class,
() ->
new IdempotencyExecutorV2(
redisStore,
Duration.ofSeconds(30),
Duration.ofHours(1),
Duration.ofHours(1),
"json-v2",
"policy-v2"))
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(IdempotencyStorePortV2.class);
assertThat(context.getBeansOfType(IdempotencyStorePort.class)).isEmpty();
});
}
@Test
void duplicateCrossVersionProvidersAndUnknownSelectorFailFast() {
IdempotencyStorePort jdbcStore = new NoOpJdbcStore();
IdempotencyStorePortV2 redisStore = new NoOpRedisStore();
runner
.withPropertyValues("ca-skeleton.capabilities.idempotency.provider=redis")
.withBean(IdempotencyStorePort.class, () -> jdbcStore)
.withBean(
IdempotencyExecutor.class,
() -> new IdempotencyExecutor(jdbcStore, Clock.systemUTC(), Duration.ofHours(1)))
.withBean(IdempotencyStorePortV2.class, () -> redisStore)
.withBean(
IdempotencyExecutorV2.class,
() ->
new IdempotencyExecutorV2(
redisStore,
Duration.ofSeconds(30),
Duration.ofHours(1),
Duration.ofHours(1),
"json-v2",
"policy-v2"))
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("ambiguous");
});
runner
.withPropertyValues("ca-skeleton.capabilities.idempotency.provider=jdbc-and-redis")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure().getMessage())
.contains("ca-skeleton.capabilities.idempotency");
});
}
@Test
void jpaV1StoreAndReaperAreConditionedOnTheExactJdbcMode() {
assertJdbcCondition(IdempotencyStoreAdapter.class);
assertJdbcCondition(IdempotencyReaper.class);
}
private static void assertJdbcCondition(Class<?> type) {
ConditionalOnProperty condition = type.getAnnotation(ConditionalOnProperty.class);
assertThat(condition).isNotNull();
assertThat(condition.name()).containsExactly("ca-skeleton.capabilities.idempotency.provider");
assertThat(condition.havingValue()).isEqualTo("jdbc");
assertThat(condition.matchIfMissing()).isTrue();
}
private static final class NoOpJdbcStore implements IdempotencyStorePort {
@Override
public boolean tryBegin(
IdempotencyScope scope, RequestFingerprint fingerprint, Instant expiresAt) {
return false;
}
@Override
public Optional<IdempotencyRecord> find(IdempotencyScope scope, Instant now) {
return Optional.empty();
}
@Override
public void complete(IdempotencyScope scope, StoredResponse response) {}
@Override
public void discard(IdempotencyScope scope) {}
}
private static final class NoOpRedisStore implements IdempotencyStorePortV2 {
@Override
public IdempotencyClaimAttempt newClaimAttempt(String operationId) {
throw new UnsupportedOperationException();
}
@Override
public IdempotencyClaimOutcome claim(IdempotencyClaimRequest request) {
throw new UnsupportedOperationException();
}
@Override
public IdempotencyStartOutcome markExecutionStarted(
IdempotencyOwner owner, String operationId) {
throw new UnsupportedOperationException();
}
@Override
public IdempotencyRenewOutcome renew(
IdempotencyOwner owner, Duration processingLeaseTtl, String operationId) {
throw new UnsupportedOperationException();
}
@Override
public IdempotencyCompleteOutcome complete(
IdempotencyOwner owner, StoredResponse response, Duration replayTtl, String operationId) {
throw new UnsupportedOperationException();
}
@Override
public IdempotencyFailOutcome markFailed(
IdempotencyOwner owner,
IdempotencyFailureDisposition disposition,
Duration retention,
String operationId) {
throw new UnsupportedOperationException();
}
@Override
public IdempotencyReleaseOutcome releaseBeforeExecution(
IdempotencyOwner owner, String operationId) {
throw new UnsupportedOperationException();
}
@Override
public IdempotencyInspection inspect(IdempotencyInspectionRequest request) {
throw new UnsupportedOperationException();
}
}
}
@@ -10,6 +10,13 @@ import dev.caskeleton.adapter.outbound.cache.core.CacheStore;
import dev.caskeleton.adapter.outbound.cache.core.CacheStoreRouter;
import dev.caskeleton.adapter.outbound.cache.redis.RedisCacheAdapterConfig;
import dev.caskeleton.adapter.outbound.cache.redis.RedisClient;
import dev.caskeleton.adapter.outbound.cache.redis.RedisRateLimitConfig;
import dev.caskeleton.adapter.outbound.fileserver.FileExportConfig;
import dev.caskeleton.adapter.outbound.fileserver.FileserverR2Config;
import dev.caskeleton.adapter.outbound.httpclient.OutboundHttpClient;
import dev.caskeleton.adapter.outbound.httpclient.OutboundHttpShutdownGuard;
import dev.caskeleton.adapter.outbound.httpclient.activation.ResolvedHttpClientCapability;
import dev.caskeleton.adapter.outbound.httpclient.resilience.OutboundHttpResilience;
import dev.caskeleton.adapter.outbound.messaging.MessagingConfig;
import dev.caskeleton.adapter.outbound.messaging.core.DisabledMessagePublisher;
import dev.caskeleton.adapter.outbound.messaging.core.MessageBroker;
@@ -28,12 +35,15 @@ import dev.caskeleton.adapter.outbound.notification.email.google.GoogleEmailNoti
import dev.caskeleton.adapter.outbound.notification.slack.webhook.SlackClient;
import dev.caskeleton.adapter.outbound.notification.slack.webhook.SlackNotificationAdapterConfig;
import dev.caskeleton.adapter.outbound.support.OutboundSupportConfig;
import dev.caskeleton.application.filepublication.FilePublicationPort;
import dev.caskeleton.application.notification.Channel;
import dev.caskeleton.application.notification.Notification;
import dev.caskeleton.application.notification.NotificationPort;
import dev.caskeleton.application.outbox.OutboxMessagePublishPort;
import dev.caskeleton.application.outbox.OutboxRelayFailureReportPort;
import dev.caskeleton.bootstrap.httpclient.HttpClientCompositionConfig;
import dev.caskeleton.shared.error.AdapterDisabledException;
import dev.caskeleton.shared.ratelimit.EdgeRateLimitPort;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -68,10 +78,14 @@ class OptionalAdapterBeanGatingTest {
MessagingConfig.class,
KafkaAdapterConfig.class,
RedisCacheAdapterConfig.class,
RedisRateLimitConfig.class,
CacheRouterConfig.class,
NotificationConfig.class,
SlackNotificationAdapterConfig.class,
GoogleEmailNotificationAdapterConfig.class,
FileExportConfig.class,
FileserverR2Config.class,
HttpClientCompositionConfig.class,
StubClientsConfig.class);
@Test
@@ -84,7 +98,15 @@ class OptionalAdapterBeanGatingTest {
// real provider beans absent (Layer 1 — disabled, contributes nothing)
assertThat(context.getBeansOfType(MessageBroker.class)).isEmpty();
assertThat(context.getBeansOfType(CacheStore.class)).isEmpty();
assertThat(context.getBeansOfType(EdgeRateLimitPort.class)).isEmpty();
assertThat(context.containsBean("distributedRateLimiter")).isFalse();
assertThat(context.getBeansOfType(NotificationProvider.class)).isEmpty();
assertThat(context.getBeansOfType(FilePublicationPort.class)).isEmpty();
assertThat(context.getBeansOfType(OutboundHttpClient.class)).isEmpty();
assertThat(context.getBeansOfType(OutboundHttpShutdownGuard.class)).isEmpty();
assertThat(context.getBeansOfType(OutboundHttpResilience.class)).isEmpty();
assertThat(context.getBean(ResolvedHttpClientCapability.class).state())
.isEqualTo(ResolvedHttpClientCapability.State.DISABLED_VERIFIED);
// messaging: fail-fast sentinels satisfy the ports (Layer 3 fallback)
assertThat(context.getBean(MessagePublisher.class))
@@ -131,12 +153,24 @@ class OptionalAdapterBeanGatingTest {
});
}
@Test
void legacyRedisEnableWithoutExplicitMigrationModeCreatesNoBackend() {
runner
.withPropertyValues("app.cache.redis.enabled=true", "app.cache.redis.client-mode=external")
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context.getBeansOfType(CacheBackend.class)).isEmpty();
});
}
@Test
void redisEnabledContributesTheBackendAndRoutesBoundLogicalCaches() {
runner
.withPropertyValues(
"app.cache.redis.enabled=true",
"app.cache.redis.client-mode=external",
"ca-skeleton.providers.redis.legacy-migration-enabled=true",
"app.cache.bindings.worklog=redis")
.run(
context -> {
@@ -191,6 +225,7 @@ class OptionalAdapterBeanGatingTest {
.withPropertyValues(
"app.cache.redis.enabled=true",
"app.cache.redis.client-mode=external",
"ca-skeleton.providers.redis.legacy-migration-enabled=true",
"app.cache.test-second.enabled=true",
"app.cache.bindings.worklog=redis",
"app.cache.bindings.session=test-second")
@@ -7,6 +7,7 @@ import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.EvaluationResult;
import dev.caskeleton.application.architecture.violations.ApplicationDiagnosticFrameworkViolation;
import dev.caskeleton.bootstrap.architecture.allowed.application.CleanProjectionQueryPort;
import dev.caskeleton.bootstrap.architecture.fixtures.application.RootWriteTransactionBoundaryUseCase;
import dev.caskeleton.bootstrap.architecture.violations.application.BulkWriteWithoutWriteAccessUseCase;
import dev.caskeleton.bootstrap.architecture.violations.application.FixtureRepository;
import dev.caskeleton.bootstrap.architecture.violations.application.GenericLeakQueryPort;
@@ -59,6 +60,8 @@ class ArchitectureViolationFixtureTest {
new ClassFileImporter().importClasses(JakartaValidationApplicationFixture.class);
private static final JavaClasses APPLICATION_DIAGNOSTIC_FRAMEWORK_FIXTURE_ONLY =
new ClassFileImporter().importClasses(ApplicationDiagnosticFrameworkViolation.class);
private static final JavaClasses ROOT_WRITE_TRANSACTION_BOUNDARY_FIXTURE_ONLY =
new ClassFileImporter().importClasses(RootWriteTransactionBoundaryUseCase.class);
// Each WebSocket fixture is imported in ISOLATION so the two package globs in
// NO_WEBSOCKET_HANDLER ("org.springframework.web.socket.." vs "jakarta.websocket..")
@@ -80,6 +83,14 @@ class ArchitectureViolationFixtureTest {
new ClassFileImporter().importClasses(RawLeakQueryPort.class, FakeDomainEntity.class);
private static final JavaClasses GENERIC_LEAK_QUERY_PORT_ONLY =
new ClassFileImporter().importClasses(GenericLeakQueryPort.class, FakeDomainEntity.class);
private static final JavaClasses B7_SETTINGS_SUFFIX_BYPASS_ONLY =
new ClassFileImporter()
.importPackages(
"dev.caskeleton.bootstrap.architecture.violations.adapter.outbound.settingsbypass");
private static final JavaClasses B7_ACTIVATION_PACKAGE_BYPASS_ONLY =
new ClassFileImporter()
.importPackages(
"dev.caskeleton.bootstrap.architecture.violations.adapter.outbound.httpclient.activation");
/** Over-block guard corpus: a legitimate projection port the D1 rule must NOT flag. */
private static final JavaClasses CLEAN_PROJECTION_QUERY_PORT_ONLY =
@@ -261,10 +272,23 @@ class ArchitectureViolationFixtureTest {
.as(
"USE_CASE_CAPABILITY_MATCHES_TRANSACTION_PORT_BOUNDARY must catch "
+ "MissingTransactionBoundaryUseCase declaring WRITE_REPOSITORY without "
+ "TransactionPort.inWrite")
+ "TransactionPort.inWrite or TransactionPort.inRootWrite")
.isTrue();
}
@Test
void useCaseCapabilityMatchesTransactionPortBoundaryAllowsRootWriteBoundary() {
EvaluationResult result =
CleanArchitectureTest.USE_CASE_CAPABILITY_MATCHES_TRANSACTION_PORT_BOUNDARY.evaluate(
ROOT_WRITE_TRANSACTION_BOUNDARY_FIXTURE_ONLY);
assertThat(result.hasViolation())
.as(
"USE_CASE_CAPABILITY_MATCHES_TRANSACTION_PORT_BOUNDARY must allow a "
+ "WRITE_REPOSITORY use case that directly calls TransactionPort.inRootWrite")
.isFalse();
}
@Test
void sharedContractScopeRuleCatchesDomainSpecificSharedPackage() {
EvaluationResult result =
@@ -397,6 +421,30 @@ class ArchitectureViolationFixtureTest {
.isTrue();
}
@Test
void outboundAdapterMethodRuleCannotBeBypassedWithSettingsSuffix() {
EvaluationResult result =
CleanArchitectureTest.OUTBOUND_ADAPTER_METHOD_RETURNS_ONLY_DOMAIN_OR_PRIMITIVES.evaluate(
B7_SETTINGS_SUFFIX_BYPASS_ONLY);
assertThat(result.hasViolation())
.as("B7 must catch a raw adapter return even when the owner ends with Settings")
.isTrue();
}
@Test
void outboundAdapterMethodRuleCannotBeBypassedWithActivationPackage() {
EvaluationResult result =
CleanArchitectureTest.OUTBOUND_ADAPTER_METHOD_RETURNS_ONLY_DOMAIN_OR_PRIMITIVES.evaluate(
B7_ACTIVATION_PACKAGE_BYPASS_ONLY);
assertThat(result.hasViolation())
.as(
"B7 must catch whitelisted-name overloads returning raw adapter types inside an "
+ "activation-named package")
.isTrue();
}
@Test
void controllerRequestMappingsFollowAip122CatchesKebabPathFixture() {
EvaluationResult result =
@@ -387,7 +387,7 @@ class CleanArchitectureTest {
"feature-domain-feature-onboarding-contract D4: a use case that declares a "
+ "repository-backed transaction capability must call the matching "
+ "TransactionPort boundary directly: READ_REPOSITORY+READ_ONLY -> inRead, "
+ "WRITE_REPOSITORY+WRITE -> inWrite, REQUIRES_NEW -> inNew. "
+ "WRITE_REPOSITORY+WRITE -> inWrite or inRootWrite, REQUIRES_NEW -> inNew. "
+ "RepositoryAccess.NONE may intentionally skip a DB transaction. "
+ "UNSUPPORTED_IMPL_DECISION: static analysis reaches direct calls only; a "
+ "transaction hidden behind a helper remains a code-review concern.")
@@ -583,24 +583,24 @@ class CleanArchitectureTest {
String transactionMode = enumAnnotationValue(annotation, "transactionMode");
String repositoryAccess = enumAnnotationValue(annotation, "repositoryAccess");
String requiredMethod = null;
Set<String> requiredMethods = Set.of();
if ("REQUIRES_NEW".equals(transactionMode)) {
requiredMethod = "inNew";
requiredMethods = Set.of("inNew");
} else if ("WRITE".equals(transactionMode) && "WRITE_REPOSITORY".equals(repositoryAccess)) {
requiredMethod = "inWrite";
requiredMethods = Set.of("inWrite", "inRootWrite");
} else if ("READ_ONLY".equals(transactionMode)
&& "READ_REPOSITORY".equals(repositoryAccess)) {
requiredMethod = "inRead";
requiredMethods = Set.of("inRead");
}
if (requiredMethod == null) {
if (requiredMethods.isEmpty()) {
return;
}
for (JavaMethodCall call : item.getMethodCallsFromSelf()) {
if ("dev.caskeleton.application.transaction.TransactionPort"
.equals(call.getTargetOwner().getFullName())
&& requiredMethod.equals(call.getName())) {
&& requiredMethods.contains(call.getName())) {
return;
}
}
@@ -614,8 +614,8 @@ class CleanArchitectureTest {
+ transactionMode
+ ", repositoryAccess = "
+ repositoryAccess
+ ") but does not directly call TransactionPort."
+ requiredMethod
+ ") but does not directly call one of TransactionPort."
+ requiredMethods
+ "(...)"));
}
};
@@ -925,9 +925,9 @@ class CleanArchitectureTest {
JavaClass.Predicates.resideOutsideOfPackage(
"..adapter.outbound.identifier.."))))
.as(
"adapter:outbound:identifier is a non-IO driven adapter (UUIDv7 generation/codec) — it "
+ "must not reach into sibling adapters, persistence, or the composition "
+ "root (feature-resource-identifier-contract §4 taxonomy).")
"adapter:outbound:identifier is a non-IO driven adapter (UUIDv7 generation/codec) —"
+ " it must not reach into sibling adapters, persistence, or the composition root"
+ " (feature-resource-identifier-contract §4 taxonomy).")
.allowEmptyShould(true);
// ---- Task 7: explicit inbound/outbound adapter topology (hexagonal driving/driven split) ----
@@ -1051,9 +1051,9 @@ class CleanArchitectureTest {
.dependOnClassesThat()
.resideInAPackage("..fixtures..")
.as(
"feature-test-taxonomy-fixture-contract D6: production code must never depend on a "
+ "test fixture — fixtures live in src/test/.../fixtures/ (test-only); this guards "
+ "against a fixture leaking onto the main classpath.")
"feature-test-taxonomy-fixture-contract D6: production code must never depend on a"
+ " test fixture — fixtures live in src/test/.../fixtures/ (test-only); this"
+ " guards against a fixture leaking onto the main classpath.")
.allowEmptyShould(true);
// ---- feature-boundary-validation-mapping-contract ----
@@ -1252,6 +1252,20 @@ class CleanArchitectureTest {
.areDeclaredInClassesThat()
.areNotAnnotatedWith(
"org.springframework.boot.context.properties.ConfigurationProperties")
// The canonical HTTP control plane and explicit legacy factories have a small exact
// method whitelist below. Names, packages, and arbitrary @Bean annotations do not bypass
// B7.
.and(notAnExactHttpControlPlaneMethod())
// Redis provider binding and secret material provider SPIs likewise expose only a small
// exact set of typed composition accessors. Command/router/native runtime and secret
// value accessors are deliberately not exempt.
.and(notAnExactRedisCompositionMethod())
.and()
// Package-private implementation types cannot expose their methods outside the
// adapter package. B7 protects the externally reachable adapter API, not internal
// records and fault-injection seams used to implement that API.
.areDeclaredInClassesThat()
.arePublic()
.and()
.arePublic()
.and()
@@ -1263,15 +1277,226 @@ class CleanArchitectureTest {
"..adapter.inbound.web..",
"..adapter.outbound.persistence.."))
.as(
"B7: outbound adapter public methods must return domain types (or "
+ "primitives/wrappers/Optional) — raw external response types must not "
+ "escape the adapter package "
+ "(feature-boundary-validation-mapping-contract B7 ACL). @Configuration "
+ "@Bean factory methods and @ConfigurationProperties settings holders are "
+ "excluded — they assemble port bindings / bind config, not adapter "
+ "response surfaces.")
"B7: externally reachable outbound adapter public methods must return domain types"
+ " (or primitives/wrappers/Optional) — raw external response types must not"
+ " escape the adapter package (feature-boundary-validation-mapping-contract B7"
+ " ACL). @Configuration @Bean factory methods and @ConfigurationProperties"
+ " settings holders are excluded — they assemble port bindings / bind config,"
+ " not adapter response surfaces.")
.allowEmptyShould(true);
private static final Set<ExactMethodSignature> B7_EXACT_HTTP_CONTROL_PLANE_METHODS =
Set.of(
signature(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpSettings",
"retry",
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpSettings$Retry"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpSettings",
"circuitBreaker",
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpSettings$CircuitBreaker"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpClientConfig",
"outboundHttpShutdownGuard",
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpShutdownGuard"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpClientConfig",
"outboundHttpErrorMapper",
"dev.caskeleton.adapter.outbound.httpclient.diagnostics.OutboundHttpErrorMapper"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpClientConfig",
"outboundHttpDependencyLogger",
"dev.caskeleton.adapter.outbound.httpclient.diagnostics.OutboundHttpDependencyLogger"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpClientConfig",
"outboundRetryPolicy",
List.of(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpSettings",
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpShutdownGuard",
"dev.caskeleton.adapter.outbound.httpclient.diagnostics.OutboundHttpErrorMapper"),
"dev.caskeleton.adapter.outbound.httpclient.OutboundRetryPolicy"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.resilience.OutboundHttpResilienceConfig",
"outboundHttpResilience",
List.of(
"dev.caskeleton.adapter.outbound.httpclient.OutboundHttpSettings",
"dev.caskeleton.adapter.outbound.httpclient.OutboundRetryPolicy",
"org.springframework.beans.factory.ObjectProvider"),
"dev.caskeleton.adapter.outbound.httpclient.resilience.OutboundHttpResilience"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientActivationResolver",
"resolve",
List.of(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration",
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpOperationCatalogRegistry",
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientReadinessCardRegistry"),
"dev.caskeleton.adapter.outbound.httpclient.activation.ResolvedHttpClientCapability"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration",
"expectedState",
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientExpectedState"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration$DestinationDefinition",
"operationCatalogId",
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration$OperationCatalogId"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration$DestinationDefinition",
"profile",
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration$DestinationProfile"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfigurationBinder",
"bind",
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientReadinessCardRegistry",
"require",
List.of("java.lang.String"),
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientReadinessCardRegistry$Maturity"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpOperationCatalogRegistry",
"require",
List.of(
"dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration$OperationCatalogId"),
"dev.caskeleton.adapter.outbound.httpclient.operation.HttpOperationCatalog"),
signature(
"dev.caskeleton.adapter.outbound.httpclient.activation.ResolvedHttpClientCapability",
"state",
"dev.caskeleton.adapter.outbound.httpclient.activation.ResolvedHttpClientCapability$State"));
private static DescribedPredicate<JavaMethod> notAnExactHttpControlPlaneMethod() {
return new DescribedPredicate<>("not an exact HTTP control-plane accessor/factory") {
@Override
public boolean test(JavaMethod method) {
return !B7_EXACT_HTTP_CONTROL_PLANE_METHODS.contains(ExactMethodSignature.from(method));
}
};
}
private static final Set<ExactMethodSignature> B7_EXACT_REDIS_COMPOSITION_METHODS =
Set.of(
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings",
"dataAuthentication",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Authentication"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings",
"dataTls",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Tls"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Standalone",
"dataAuthentication",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Authentication"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Standalone",
"dataTls",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Tls"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Sentinel",
"sentinelAuthentication",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Authentication"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Sentinel",
"sentinelTls",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Tls"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Sentinel",
"dataAuthentication",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Authentication"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Sentinel",
"dataTls",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Tls"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Cluster",
"dataAuthentication",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Authentication"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Cluster",
"dataTls",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisDeploymentSettings$Tls"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$DeploymentProperties",
"topology",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$Topology"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$DeploymentProperties",
"standalone",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$StandaloneProperties"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$DeploymentProperties",
"sentinel",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$SentinelProperties"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$DeploymentProperties",
"cluster",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$ClusterProperties"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$DeploymentProperties",
"authentication",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$AuthenticationProperties"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$DeploymentProperties",
"tls",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$TlsProperties"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$SentinelProperties",
"authentication",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$AuthenticationProperties"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$SentinelProperties",
"tls",
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$TlsProperties"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings$RuntimeProperties",
"clientSettings",
"dev.caskeleton.adapter.outbound.cache.redis.runtime.RedisClientRuntimeSettings"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.security.RedisCredentialMaterialProvider",
"resolve",
List.of("dev.caskeleton.adapter.outbound.cache.redis.security.RedisSecretReference"),
"dev.caskeleton.adapter.outbound.cache.redis.security.VersionedRedisCredentialMaterial"),
signature(
"dev.caskeleton.adapter.outbound.cache.redis.security.RedisTrustMaterialProvider",
"resolve",
List.of("dev.caskeleton.adapter.outbound.cache.redis.security.RedisSecretReference"),
"dev.caskeleton.adapter.outbound.cache.redis.security.VersionedRedisTrustMaterial"));
private static DescribedPredicate<JavaMethod> notAnExactRedisCompositionMethod() {
return new DescribedPredicate<>("not an exact Redis composition accessor or material SPI") {
@Override
public boolean test(JavaMethod method) {
return !B7_EXACT_REDIS_COMPOSITION_METHODS.contains(ExactMethodSignature.from(method));
}
};
}
private static ExactMethodSignature signature(String owner, String name, String returnType) {
return signature(owner, name, List.of(), returnType);
}
private static ExactMethodSignature signature(
String owner, String name, List<String> parameters, String returnType) {
return new ExactMethodSignature(owner, name, parameters, returnType);
}
private record ExactMethodSignature(
String owner, String name, List<String> parameters, String returnType) {
private ExactMethodSignature {
parameters = List.copyOf(parameters);
}
private static ExactMethodSignature from(JavaMethod method) {
List<String> parameterTypes =
method.getRawParameterTypes().stream().map(JavaClass::getFullName).toList();
return new ExactMethodSignature(
method.getOwner().getFullName(),
method.getName(),
parameterTypes,
method.getRawReturnType().getFullName());
}
}
@ArchTest
static final ArchRule VALID_CASCADE_DEPTH_AT_MOST_THREE =
classes()
@@ -1584,11 +1809,11 @@ class CleanArchitectureTest {
.should()
.haveRawType(assignableTo(ResourceId.class))
.as(
"D17 NO_LONG_ID_PK: a domain entity 'id' field must be a ResourceId value "
+ "object (e.g. WorkLogId), never Long/long/int/Integer "
+ "(feature-resource-identifier-contract D17). JPA @Id UUID columns in "
+ "..adapter.outbound.persistence.. are out of scope — they store the UUID as the "
+ "PostgreSQL native uuid type per D10.")
"D17 NO_LONG_ID_PK: a domain entity 'id' field must be a ResourceId value object"
+ " (e.g. WorkLogId), never Long/long/int/Integer"
+ " (feature-resource-identifier-contract D17). JPA @Id UUID columns in"
+ " ..adapter.outbound.persistence.. are out of scope — they store the UUID as"
+ " the PostgreSQL native uuid type per D10.")
.allowEmptyShould(true);
@ArchTest
@@ -1759,15 +1984,15 @@ class CleanArchitectureTest {
"org.springframework.web..",
"org.hibernate.."))
.as(
"D1 QUERY_PORTS_DO_NOT_LEAK_DOMAIN_JPA_OR_WEB_TYPES: application read/query ports "
+ "(classes whose simple name ends with 'QueryPort') must return "
+ "application-layer projection DTOs — never a domain aggregate, JPA entity, or "
+ "web type, INCLUDING through generic type arguments like List<DomainType> "
+ "(checked via JavaType.getAllInvolvedRawTypes(), since a raw-return-type check "
+ "alone misses generic leakage) "
+ "(feature-application-query-bypass-contract D1 core purity guardrail). The "
+ "through-aggregate read path (repository ports returning the domain aggregate) "
+ "is a separate, equally-valid choice and is intentionally out of this rule's scope.")
"D1 QUERY_PORTS_DO_NOT_LEAK_DOMAIN_JPA_OR_WEB_TYPES: application read/query ports"
+ " (classes whose simple name ends with 'QueryPort') must return"
+ " application-layer projection DTOs — never a domain aggregate, JPA entity, or"
+ " web type, INCLUDING through generic type arguments like List<DomainType>"
+ " (checked via JavaType.getAllInvolvedRawTypes(), since a raw-return-type check"
+ " alone misses generic leakage) (feature-application-query-bypass-contract D1"
+ " core purity guardrail). The through-aggregate read path (repository ports"
+ " returning the domain aggregate) is a separate, equally-valid choice and is"
+ " intentionally out of this rule's scope.")
.allowEmptyShould(true);
private static ArchCondition<JavaMethod> notLeakDomainJpaOrWebThroughReturnType(
@@ -3,20 +3,26 @@ package dev.caskeleton.bootstrap.architecture;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
/**
* feature-integration-adapter-templates Layer 2 (D3 / §구현 가이드 §3) — static isolation + gating guard
* for the optional integration adapters (Kafka / Redis / Slack / Google Email). Owner: this branch.
* for the optional integration adapters (Kafka / Redis / Slack / Google Email / Fileserver). Owner:
* this branch.
*
* <p>What this layer statically guarantees, and its documented limit (branch-note L111 / D3 Open
* Risk): ArchUnit can prove (1) the application layer never imports an optional adapter package,
* and (2) every optional-adapter {@code @Bean} is gated by {@code @ConditionalOnProperty} — i.e.
* "the adapter candidate class HAS the {@code @ConditionalOnProperty} annotation". Whether the
* adapter is actually <em>active</em> at runtime is a config evaluation ArchUnit cannot reach; that
* runtime guarantee is delegated to Layer 3 ({@code AdapterDisabledException}).
* and (2) every resource-owning optional-adapter {@code @Bean} is gated by
* {@code @ConditionalOnProperty}. Redis's exact resource-free observation and zero-binding registry
* control-plane beans are exempt: their runtime test proves that an unselected provider resolves no
* material and opens no client. Whether a candidate is actually <em>active</em> at runtime is a
* config evaluation ArchUnit cannot reach; that runtime guarantee is delegated to Layer 3 ({@code
* AdapterDisabledException}).
*
* <p>Spring annotation types are referenced by fully-qualified NAME so this test needs no compile
* dependency on spring-context / spring-boot-autoconfigure (they arrive only via adapter-outbound's
@@ -29,7 +35,8 @@ class DisabledAdapterArchitectureTest {
"..adapter.outbound.messaging.kafka..",
"..adapter.outbound.cache.redis..",
"..adapter.outbound.notification.slack..",
"..adapter.outbound.notification.email.."
"..adapter.outbound.notification.email..",
"..adapter.outbound.fileserver.."
};
private static final String BEAN = "org.springframework.context.annotation.Bean";
@@ -51,7 +58,8 @@ class DisabledAdapterArchitectureTest {
.resideInAnyPackage(OPTIONAL_ADAPTER_PACKAGES)
.as(
"D3 APPLICATION_DOES_NOT_DEPEND_ON_OPTIONAL_ADAPTERS: the application layer must "
+ "not import an optional adapter package (Kafka/Redis/Slack/Google Email) — "
+ "not import an optional adapter package "
+ "(Kafka/Redis/Slack/Google Email/Fileserver) — "
+ "the static half of the disabled-adapter detection. Runtime activity is "
+ "delegated to Layer 3 (feature-integration-adapter-templates D3)")
.allowEmptyShould(true);
@@ -70,14 +78,63 @@ class DisabledAdapterArchitectureTest {
.and()
.areDeclaredInClassesThat()
.resideInAnyPackage(OPTIONAL_ADAPTER_PACKAGES)
.and(notCanonicalRedisResourceFreeControlPlaneBeans())
.should()
.beAnnotatedWith(CONDITIONAL_ON_PROPERTY)
.as(
"D3 OPTIONAL_ADAPTER_BEANS_ARE_GATED_BY_CONDITIONAL_ON_PROPERTY: every @Bean in an "
+ "optional adapter package (Kafka/Redis/Slack/Google Email) must declare "
+ "@ConditionalOnProperty(app.<domain>.<adapter>.enabled) — Layer 1 disabled-default "
+ "must not be bypassable by an ungated bean. ArchUnit reaches the annotation "
+ "presence only; runtime activation is Layer 3's job "
+ "(feature-integration-adapter-templates D3, L111)")
"D3 OPTIONAL_ADAPTER_BEANS_ARE_GATED_BY_CONDITIONAL_ON_PROPERTY: every @Bean in an"
+ " optional adapter package (Kafka/Redis/Slack/Google Email/Fileserver) must"
+ " declare @ConditionalOnProperty(app.<domain>.<adapter>.enabled) — Layer 1"
+ " disabled-default must not be bypassable by an ungated bean. ArchUnit reaches"
+ " the annotation presence only; runtime activation is Layer 3's job"
+ " (feature-integration-adapter-templates D3, L111)")
.allowEmptyShould(true);
private static DescribedPredicate<JavaMethod> notCanonicalRedisResourceFreeControlPlaneBeans() {
return new DescribedPredicate<>(
"not the exact canonical Redis resource-free control-plane beans") {
@Override
public boolean test(JavaMethod method) {
return !isCanonicalRedisObservationPort(method)
&& !isCanonicalRedisZeroBindingRegistry(method);
}
};
}
private static boolean isCanonicalRedisObservationPort(JavaMethod method) {
return hasOwnerAndSignature(
method,
"redisCapabilityObservationPort",
"dev.caskeleton.adapter.outbound.cache.redis.RedisCapabilityObservationPort",
java.util.List.of("org.springframework.beans.factory.ObjectProvider"));
}
private static boolean isCanonicalRedisZeroBindingRegistry(JavaMethod method) {
return hasOwnerAndSignature(
method,
"redisCanonicalRoleRegistry",
"dev.caskeleton.adapter.outbound.cache.redis.RedisCanonicalRoleRegistry",
java.util.List.of(
"dev.caskeleton.adapter.outbound.cache.redis.config.RedisProviderSettings",
"org.springframework.core.env.Environment",
"org.springframework.beans.factory.ObjectProvider",
"org.springframework.beans.factory.ObjectProvider",
"org.springframework.beans.factory.ObjectProvider",
"org.springframework.beans.factory.ObjectProvider",
"dev.caskeleton.adapter.outbound.cache.redis.RedisCapabilityObservationPort"));
}
private static boolean hasOwnerAndSignature(
JavaMethod method, String name, String returnType, java.util.List<String> parameterTypes) {
return method
.getOwner()
.getFullName()
.equals("dev.caskeleton.adapter.outbound.cache.redis.RedisCanonicalConfig")
&& method.getName().equals(name)
&& method.getRawReturnType().getFullName().equals(returnType)
&& method.getRawParameterTypes().stream()
.map(JavaClass::getFullName)
.toList()
.equals(parameterTypes);
}
}
@@ -0,0 +1,33 @@
package dev.caskeleton.bootstrap.architecture.fixtures.application;
import dev.caskeleton.application.capability.Idempotency;
import dev.caskeleton.application.capability.RepositoryAccess;
import dev.caskeleton.application.capability.UseCaseCapability;
import dev.caskeleton.application.command.Command;
import dev.caskeleton.application.security.RequiresPermission;
import dev.caskeleton.application.transaction.TransactionMode;
import dev.caskeleton.application.transaction.TransactionPort;
import dev.caskeleton.application.usecase.CommandUseCase;
/** Positive fixture: a root-only write boundary satisfies the WRITE_REPOSITORY fitness rule. */
@UseCaseCapability(
transactionMode = TransactionMode.WRITE,
idempotency = Idempotency.NOT_IDEMPOTENT,
repositoryAccess = RepositoryAccess.WRITE_REPOSITORY)
@RequiresPermission("fixture:root-write")
public final class RootWriteTransactionBoundaryUseCase
implements CommandUseCase<RootWriteTransactionBoundaryUseCase.CommandFixture, String> {
private final TransactionPort transactionPort;
public RootWriteTransactionBoundaryUseCase(TransactionPort transactionPort) {
this.transactionPort = transactionPort;
}
@Override
public String handle(CommandFixture command) {
return transactionPort.inRootWrite(command::value);
}
public record CommandFixture(String value) implements Command {}
}
@@ -0,0 +1,15 @@
package dev.caskeleton.bootstrap.architecture.violations.adapter.outbound.httpclient.activation;
import dev.caskeleton.bootstrap.architecture.violations.adapter.outbound.RawExternalResponseFixture;
/** Proves that an activation package name cannot bypass the outbound raw-type leak guard. */
public class EvilActivationLeak {
public RawExternalResponseFixture require() {
return new RawExternalResponseFixture();
}
public RawExternalResponseFixture require(String ignored) {
return new RawExternalResponseFixture();
}
}
@@ -0,0 +1,11 @@
package dev.caskeleton.bootstrap.architecture.violations.adapter.outbound.settingsbypass;
import dev.caskeleton.bootstrap.architecture.violations.adapter.outbound.RawExternalResponseFixture;
/** Proves that a Settings suffix cannot bypass the outbound raw-type leak guard. */
public class EvilSettings {
public RawExternalResponseFixture leak() {
return new RawExternalResponseFixture();
}
}
@@ -8,7 +8,10 @@ import dev.caskeleton.application.security.RequiresPermission;
import dev.caskeleton.application.transaction.TransactionMode;
import dev.caskeleton.application.usecase.CommandUseCase;
/** Intentional write-use-case violation: declares a write but skips TransactionPort.inWrite. */
/**
* Intentional write-use-case violation: declares a write but skips both TransactionPort.inWrite and
* TransactionPort.inRootWrite.
*/
@UseCaseCapability(
transactionMode = TransactionMode.WRITE,
idempotency = Idempotency.NOT_IDEMPOTENT,
@@ -5,8 +5,6 @@ import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import dev.caskeleton.bootstrap.contract.support.conditional.EnabledIfEmailNotificationConfigured;
import dev.caskeleton.bootstrap.contract.support.conditional.EnabledIfHttpCircuitBreakerEnabled;
import dev.caskeleton.bootstrap.contract.support.conditional.EnabledIfHttpRetryEnabled;
import dev.caskeleton.bootstrap.contract.support.conditional.EnabledIfMessagingBrokerConfigured;
import dev.caskeleton.bootstrap.contract.support.conditional.EnabledIfRedisCacheEnabled;
import dev.caskeleton.bootstrap.contract.support.conditional.EnabledIfSlackNotificationConfigured;
@@ -35,18 +33,6 @@ class OptionalAdapterConditionalExecutionContractTest {
assertThat(System.getenv("APP_CACHE_REDIS_ENABLED")).isEqualTo("true");
}
@Test
@EnabledIfHttpRetryEnabled
void httpRetryAdapterRunsOnlyWhenEnabled() {
assertThat(System.getenv("APP_OUTBOUND_HTTP_RETRY_ENABLED")).isEqualTo("true");
}
@Test
@EnabledIfHttpCircuitBreakerEnabled
void httpCircuitBreakerAdapterRunsOnlyWhenEnabled() {
assertThat(System.getenv("APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED")).isEqualTo("true");
}
@Test
@EnabledIfMessagingBrokerConfigured
void messagingBrokerAdapterRunsOnlyWhenConfigured() {
@@ -1,23 +0,0 @@
package dev.caskeleton.bootstrap.contract.support.conditional;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
/**
* Gates an optional-adapter contract test to the outbound-HTTP-circuit-breaker-enabled env matrix.
* Reports DISABLED (= SKIPPED, never FAILED) when {@code APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED}
* is unset or not {@code true} (feature-contract-verification-test-suite D3).
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIfEnvironmentVariable(
named = "APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED",
matches = "true",
disabledReason =
"Outbound HTTP circuit breaker disabled "
+ "(APP_OUTBOUND_HTTP_CIRCUIT_BREAKER_ENABLED != true) — optional-adapter contract "
+ "test runs only in the circuit-breaker-enabled env matrix")
public @interface EnabledIfHttpCircuitBreakerEnabled {}
@@ -1,22 +0,0 @@
package dev.caskeleton.bootstrap.contract.support.conditional;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
/**
* Gates an optional-adapter contract test to the outbound-HTTP-retry-enabled env matrix. Reports
* DISABLED (= SKIPPED, never FAILED) when {@code APP_OUTBOUND_HTTP_RETRY_ENABLED} is unset or not
* {@code true} (feature-contract-verification-test-suite D3).
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIfEnvironmentVariable(
named = "APP_OUTBOUND_HTTP_RETRY_ENABLED",
matches = "true",
disabledReason =
"Outbound HTTP retry disabled (APP_OUTBOUND_HTTP_RETRY_ENABLED != true) — "
+ "optional-adapter contract test runs only in the retry-enabled env matrix")
public @interface EnabledIfHttpRetryEnabled {}
@@ -0,0 +1,181 @@
package dev.caskeleton.bootstrap.httpclient;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.httpclient.OutboundHttpClient;
import dev.caskeleton.adapter.outbound.httpclient.OutboundHttpSettings;
import dev.caskeleton.adapter.outbound.httpclient.OutboundHttpShutdownGuard;
import dev.caskeleton.adapter.outbound.httpclient.activation.HttpClientCanonicalConfiguration;
import dev.caskeleton.adapter.outbound.httpclient.activation.HttpOperationCatalogRegistry;
import dev.caskeleton.adapter.outbound.httpclient.activation.ResolvedHttpClientCapability;
import dev.caskeleton.adapter.outbound.httpclient.operation.HttpDestinationId;
import dev.caskeleton.adapter.outbound.httpclient.operation.HttpOperationCatalog;
import dev.caskeleton.adapter.outbound.httpclient.operation.HttpOperationDescriptor;
import dev.caskeleton.adapter.outbound.httpclient.operation.HttpOperationId;
import dev.caskeleton.adapter.outbound.httpclient.resilience.OutboundHttpResilience;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.client.RestClient;
class HttpClientCompositionConfigTest {
private final ApplicationContextRunner runner =
new ApplicationContextRunner().withUserConfiguration(HttpClientCompositionConfig.class);
private final ApplicationContextRunner applicationYamlRunner =
new ApplicationContextRunner()
.withInitializer(HttpClientCompositionConfigTest::loadApplicationYaml)
.withUserConfiguration(HttpClientCompositionConfig.class);
@Test
void defaultZeroBindingPublishesOnlyAnInertDisabledDescriptorAndNoRuntimeResources() {
runner.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context.getBean(ResolvedHttpClientCapability.class).state())
.isEqualTo(ResolvedHttpClientCapability.State.DISABLED_VERIFIED);
assertThat(context.getBeansOfType(OutboundHttpClient.class)).isEmpty();
assertThat(context.getBeansOfType(OutboundHttpSettings.class)).isEmpty();
assertThat(context.getBeansOfType(RestClient.class)).isEmpty();
assertThat(context.getBeansOfType(RestClient.Builder.class)).isEmpty();
assertThat(context.getBeansOfType(OutboundHttpShutdownGuard.class)).isEmpty();
assertThat(context.getBeansOfType(OutboundHttpResilience.class)).isEmpty();
assertThat(beanNamesFor(context, "io.github.resilience4j.retry.RetryRegistry")).isEmpty();
assertThat(
beanNamesFor(
context, "io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry"))
.isEmpty();
assertThat(context.containsBean("outboundCallExecutor")).isFalse();
});
}
@Test
void disabledWithABindingFailsStartup() {
runner
.withPropertyValues(
"ca-skeleton.capabilities.http-client.expected-state=DISABLED",
"ca-skeleton.capabilities.http-client.bindings.partner=jdk-r1")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure().getMessage()).contains("DISABLED");
});
}
@Test
void activeFailsOnNotImplementedReadinessBeforeRuntimeResourcesExist() {
runner
.withBean(
HttpOperationCatalogRegistry.class, HttpClientCompositionConfigTest::activeCatalog)
.withPropertyValues(
"ca-skeleton.capabilities.http-client.expected-state=ACTIVE",
"ca-skeleton.capabilities.http-client.bindings.partner=jdk-r1",
"ca-skeleton.providers.http-client.jdk-r1.destinations.partner.operation-catalog=partner-v1",
"ca-skeleton.providers.http-client.jdk-r1.destinations.partner.profile=BUFFERED_CLASSIC")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure().getMessage())
.contains("httpclient-static-buffered")
.contains("NOT_IMPLEMENTED");
});
}
@Test
void activeCanonicalSelectionRejectsLegacySpringInputBeforeResolution() {
runner
.withPropertyValues(
"ca-skeleton.capabilities.http-client.expected-state=ACTIVE",
"ca-skeleton.capabilities.http-client.bindings.partner=jdk-r1",
"app.outbound.http.connect-timeout=2s")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure().getMessage())
.contains("canonical")
.contains("legacy");
});
}
@Test
void actualApplicationYamlKeepsLegacyInputAbsentAndZeroBindingDisabled() {
applicationYamlRunner.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context.getEnvironment().getProperty("app.outbound.http.connect-timeout"))
.isNull();
assertThat(context.getBean(ResolvedHttpClientCapability.class).state())
.isEqualTo(ResolvedHttpClientCapability.State.DISABLED_VERIFIED);
assertThat(context.getBeansOfType(OutboundHttpClient.class)).isEmpty();
});
}
@Test
void actualApplicationYamlActiveFailsAtReadinessRatherThanLegacyConflict() {
applicationYamlRunner
.withBean(
HttpOperationCatalogRegistry.class, HttpClientCompositionConfigTest::activeCatalog)
.withPropertyValues(
"ca-skeleton.capabilities.http-client.expected-state=ACTIVE",
"ca-skeleton.capabilities.http-client.bindings.partner=jdk-r1",
"ca-skeleton.providers.http-client.jdk-r1.destinations.partner.operation-catalog=partner-v1",
"ca-skeleton.providers.http-client.jdk-r1.destinations.partner.profile=BUFFERED_CLASSIC")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure().getMessage())
.contains("httpclient-static-buffered")
.contains("NOT_IMPLEMENTED")
.doesNotContain("legacy");
});
}
private static HttpOperationCatalogRegistry activeCatalog() {
HttpDestinationId destination = new HttpDestinationId("partner");
HttpOperationDescriptor operation =
new HttpOperationDescriptor(
new HttpOperationId("partner.fetch.v1"),
destination,
1,
HttpOperationDescriptor.Method.GET,
"/items/{id}",
HttpOperationDescriptor.OperationSemantics.SAFE_READ,
HttpOperationDescriptor.RequestMode.NONE,
HttpOperationDescriptor.ResponseMode.BUFFERED,
Set.of(200),
0,
1,
1024);
return new HttpOperationCatalogRegistry(
Map.of(
new HttpClientCanonicalConfiguration.OperationCatalogId("partner-v1"),
new HttpOperationCatalog(List.of(operation))));
}
private static String[] beanNamesFor(ApplicationContext context, String className) {
try {
return context.getBeanNamesForType(Class.forName(className));
} catch (ClassNotFoundException exception) {
return new String[0];
}
}
private static void loadApplicationYaml(ConfigurableApplicationContext context) {
try {
new YamlPropertySourceLoader()
.load("application.yml", new ClassPathResource("application.yml"))
.forEach(context.getEnvironment().getPropertySources()::addLast);
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
}
@@ -0,0 +1,116 @@
package dev.caskeleton.bootstrap.redis;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import dev.caskeleton.adapter.outbound.cache.redis.security.RedisSecretReference;
import dev.caskeleton.adapter.outbound.cache.redis.security.VersionedRedisCredentialMaterial;
import dev.caskeleton.adapter.outbound.cache.redis.security.VersionedRedisTrustMaterial;
import dev.caskeleton.bootstrap.runtime.SecretSource;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.io.ClassPathResource;
class RedisEnvironmentMaterialProviderTest {
@Test
void resolvesOnlyAllowlistedEnvironmentReferencesAndDescribesRestartOnlyRotation() {
AtomicInteger resolutions = new AtomicInteger();
SecretSource source =
key -> {
resolutions.incrementAndGet();
return Optional.of(
key.endsWith("TRUST_PEM")
? "-----BEGIN CERTIFICATE-----\ninvalid-test-body\n-----END CERTIFICATE-----"
: "private-material");
};
RedisEnvironmentCredentialMaterialProvider credentialProvider =
new RedisEnvironmentCredentialMaterialProvider(source);
RedisEnvironmentTrustMaterialProvider trustProvider =
new RedisEnvironmentTrustMaterialProvider(source);
try (VersionedRedisCredentialMaterial credential =
credentialProvider.resolve(
RedisSecretReference.parse("secret://environment/APP_RATE_LIMIT_REDIS_PASSWORD"));
VersionedRedisTrustMaterial trust =
trustProvider.resolve(
RedisSecretReference.parse(
"secret://environment/APP_RATE_LIMIT_REDIS_TRUST_PEM"))) {
String credentialValue = credential.useSecret(String::new);
int trustBytes = trust.usePem(bytes -> bytes.length);
assertThat(credentialValue).isEqualTo("private-material");
assertThat(trustBytes).isPositive();
}
assertThat(resolutions).hasValue(2);
assertThat(credentialProvider.descriptor().changeEventsSupported()).isFalse();
assertThat(trustProvider.descriptor().refreshMode())
.isEqualTo("restart-or-explicit-runtime-recomposition");
}
@Test
void unknownSchemeKeyBlankOversizeAndProviderFailuresAreSanitized() {
RedisEnvironmentCredentialMaterialProvider blank =
new RedisEnvironmentCredentialMaterialProvider(ignored -> Optional.of(" "));
RedisEnvironmentCredentialMaterialProvider oversized =
new RedisEnvironmentCredentialMaterialProvider(ignored -> Optional.of("x".repeat(16_385)));
RedisEnvironmentCredentialMaterialProvider leaking =
new RedisEnvironmentCredentialMaterialProvider(
ignored -> {
throw new IllegalStateException("raw-secret-and-reference");
});
for (org.assertj.core.api.ThrowableAssert.ThrowingCallable call :
java.util.List.<org.assertj.core.api.ThrowableAssert.ThrowingCallable>of(
() ->
blank.resolve(
RedisSecretReference.parse("secret://environment/APP_CACHE_REDIS_PASSWORD")),
() ->
oversized.resolve(
RedisSecretReference.parse("secret://environment/APP_CACHE_REDIS_PASSWORD")),
() ->
leaking.resolve(
RedisSecretReference.parse("secret://environment/APP_CACHE_REDIS_PASSWORD")),
() ->
blank.resolve(
RedisSecretReference.parse("secret://vault/APP_CACHE_REDIS_PASSWORD")),
() ->
blank.resolve(
RedisSecretReference.parse("secret://environment/UNREGISTERED_SECRET")))) {
assertThatThrownBy(call)
.isInstanceOf(IllegalStateException.class)
.hasMessage("Canonical Redis environment material resolution failed")
.hasMessageNotContaining("raw-secret")
.hasMessageNotContaining("APP_CACHE")
.hasNoCause();
}
}
@Test
void resolvesEveryCanonicalHmacReferenceShippedInApplicationYaml() throws Exception {
var properties =
new YamlPropertySourceLoader()
.load("application.yml", new ClassPathResource("application.yml"))
.getFirst();
RedisEnvironmentCredentialMaterialProvider provider =
new RedisEnvironmentCredentialMaterialProvider(
ignored -> Optional.of("cHJvZHVjdGlvbi1zYWZlLWhhcmRlbmVkLXRlc3QtaG1hYy1tYXRlcmlhbA=="));
for (String property :
java.util.List.of(
"ca-skeleton.capabilities.cache.regions.default.key-hmac-secret-reference",
"ca-skeleton.capabilities.rate-limit.key-hmac-secret-reference",
"ca-skeleton.capabilities.idempotency.key-hmac-secret-reference",
"ca-skeleton.capabilities.lease.key-hmac-secret-reference",
"ca-skeleton.capabilities.security.redis-session.key-hmac-secret-reference")) {
String reference = String.valueOf(properties.getProperty(property));
try (VersionedRedisCredentialMaterial material =
provider.resolve(RedisSecretReference.parse(reference))) {
String resolved = material.useSecret(String::new);
assertThat(resolved).isNotBlank();
}
}
}
}
@@ -2,12 +2,14 @@ package dev.caskeleton.bootstrap.runtime;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.util.Set;
import java.util.TimeZone;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.health.actuate.endpoint.HealthEndpointGroups;
import org.springframework.boot.health.actuate.endpoint.StatusAggregator;
import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointAutoConfiguration;
@@ -20,6 +22,9 @@ import org.springframework.boot.health.contributor.Status;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.io.ClassPathResource;
/**
* TDD contract test for feature-runtime-health-lifecycle-contract.
@@ -67,14 +72,33 @@ class RuntimeHealthLifecycleContractTest {
"management.health.readinessstate.enabled=true",
// The three production group properties under test.
"management.endpoint.health.probes.enabled=true",
"management.endpoint.health.validate-group-membership=false",
"management.endpoint.health.group.liveness.include=livenessState",
"management.endpoint.health.group.readiness.include=readinessState,db",
"management.endpoint.health.group.readiness.include=readinessState,db,redisRequired",
"management.endpoint.health.group.startup.include=readinessState");
// =========================================================================
// 1. Three health groups are configured
// =========================================================================
@Test
@DisplayName(
"shipped health groups keep Redis out of liveness and optional Redis out of readiness")
void shippedHealthGroupSettingsPreserveRedisDependencyTaxonomy() throws IOException {
MutablePropertySources sources = new MutablePropertySources();
new YamlPropertySourceLoader()
.load("application", new ClassPathResource("application.yml"))
.forEach(sources::addLast);
PropertySourcesPropertyResolver properties = new PropertySourcesPropertyResolver(sources);
assertThat(properties.getProperty("management.endpoint.health.group.liveness.include"))
.isEqualTo("livenessState");
assertThat(properties.getProperty("management.endpoint.health.group.readiness.include"))
.isEqualTo("readinessState,db,redisRequired");
assertThat(properties.getProperty("management.endpoint.health.validate-group-membership"))
.isEqualTo("false");
}
@Test
@DisplayName("health probes: liveness group is configured")
void livenessGroupIsConfigured() {
@@ -100,7 +124,7 @@ class RuntimeHealthLifecycleContractTest {
assertThat(groups.get("readiness"))
.as(
"readiness group must be configured "
+ "(management.endpoint.health.group.readiness.include=readinessState,db)")
+ "(include=readinessState,db,redisRequired)")
.isNotNull();
});
}
@@ -140,7 +164,7 @@ class RuntimeHealthLifecycleContractTest {
HealthEndpointGroups groups = ctx.getBean(HealthEndpointGroups.class);
var readiness = groups.get("readiness");
assertThat(readiness).as("readiness group must exist").isNotNull();
// The group membership is defined by "include=readinessState,db".
// The group membership is defined by "include=readinessState,db,redisRequired".
// isMember() returns true when the contributor name is in the include list.
assertThat(readiness.isMember("db"))
.as(
@@ -150,6 +174,19 @@ class RuntimeHealthLifecycleContractTest {
});
}
@Test
@DisplayName("readiness includes required Redis but excludes optional cache Redis")
void readinessIncludesOnlyTheRequiredRedisContributor() {
runner.run(
ctx -> {
assertThat(ctx).hasNotFailed();
var readiness = ctx.getBean(HealthEndpointGroups.class).get("readiness");
assertThat(readiness).as("readiness group must exist").isNotNull();
assertThat(readiness.isMember("redisRequired")).isTrue();
assertThat(readiness.isMember("redisOptional")).isFalse();
});
}
@Test
@DisplayName("liveness group does NOT include db (liveness is independent of REQUIRED deps)")
void livenessGroupDoesNotIncludeDb() {
@@ -164,6 +201,8 @@ class RuntimeHealthLifecycleContractTest {
"liveness group must NOT include 'db' "
+ "(a DOWN DB must not flip liveness — the JVM can still continue)")
.isFalse();
assertThat(liveness.isMember("redisRequired")).isFalse();
assertThat(liveness.isMember("redisOptional")).isFalse();
});
}
@@ -212,9 +251,10 @@ class RuntimeHealthLifecycleContractTest {
void jvmDefaultTimezoneIsUtc() {
assertThat(TimeZone.getDefault().getID())
.as(
"JVM default timezone must be UTC — enforced by -Duser.timezone=UTC in the "
+ "app-bootstrap test task. A drift here means the test JVM arg was removed. "
+ "Production UTC is owned by feature-container-runtime-contract (TZ=UTC in Dockerfile).")
"JVM default timezone must be UTC — enforced by -Duser.timezone=UTC in the"
+ " app-bootstrap test task. A drift here means the test JVM arg was removed."
+ " Production UTC is owned by feature-container-runtime-contract (TZ=UTC in"
+ " Dockerfile).")
.isEqualTo("UTC");
}
@@ -223,7 +263,7 @@ class RuntimeHealthLifecycleContractTest {
// Mirrors the liveness/readiness isMember tests (sections 1 and 2).
// The startup group is configured with include=readinessState — assert
// membership explicitly to give the startup group the same coverage parity
// as liveness (livenessState) and readiness (readinessState,db).
// as liveness (livenessState) and readiness (readinessState,db,redisRequired).
// =========================================================================
@Test
@@ -3,6 +3,7 @@ package dev.caskeleton.bootstrap.runtime;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.bootstrap.runtime.startup.StartupValidationException;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@@ -30,6 +31,8 @@ class SecretSourceValidatorTest {
"APP_EXTERNAL_API_KEY=real-api-key",
"APP_CACHE_REDIS_PASSWORD=real-redis-password",
"APP_CACHE_REDIS_KEY_HMAC_SECRET=real-redis-key-hmac-secret",
"APP_RATE_LIMIT_REDIS_PASSWORD=real-rate-limit-redis-password",
"APP_RATE_LIMIT_REDIS_KEY_HMAC_SECRET=real-rate-limit-redis-key-hmac-secret",
"APP_PRIVACY_PSEUDONYMIZATION_SALT=real-salt"
};
}
@@ -106,6 +109,102 @@ class SecretSourceValidatorTest {
.run(context -> assertThat(context).hasNotFailed());
}
@Test
void enabledRateLimitRedisRequiresItsDedicatedSecretsInProd() {
runner
.withInitializer(ctx -> ctx.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(allRequiredSecretsPresent())
.withPropertyValues(
"ca-skeleton.capabilities.rate-limit.provider=redis",
"APP_RATE_LIMIT_REDIS_KEY_HMAC_SECRET=")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.isInstanceOf(StartupValidationException.class)
.hasStackTraceContaining("APP_RATE_LIMIT_REDIS_KEY_HMAC_SECRET");
});
}
@Test
void disabledRateLimitRedisDoesNotRequireItsDedicatedSecretsInProd() {
String[] baselineSecrets =
Arrays.stream(allRequiredSecretsPresent())
.filter(value -> !value.startsWith("APP_RATE_LIMIT_REDIS_"))
.toArray(String[]::new);
runner
.withInitializer(ctx -> ctx.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(baselineSecrets)
.withPropertyValues("ca-skeleton.capabilities.rate-limit.provider=disabled")
.run(context -> assertThat(context).hasNotFailed());
}
@Test
void transportEnableAloneDoesNotRequireRedisProviderSecretsInProd() {
String[] baselineSecrets =
Arrays.stream(allRequiredSecretsPresent())
.filter(value -> !value.startsWith("APP_RATE_LIMIT_REDIS_"))
.toArray(String[]::new);
runner
.withInitializer(ctx -> ctx.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(baselineSecrets)
.withPropertyValues("app.rate-limit.enabled=true")
.run(context -> assertThat(context).hasNotFailed());
}
@Test
void selectedRedisIdempotencyRequiresItsHmacMaterialInProd() {
runner
.withInitializer(ctx -> ctx.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(allRequiredSecretsPresent())
.withPropertyValues(
"ca-skeleton.capabilities.idempotency.provider=redis",
"APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET=")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.isInstanceOf(StartupValidationException.class)
.hasStackTraceContaining("APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET");
});
}
@Test
void selectedRedisEfficiencyLeaseRequiresItsHmacMaterialInProd() {
runner
.withInitializer(ctx -> ctx.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(allRequiredSecretsPresent())
.withPropertyValues(
"ca-skeleton.capabilities.lease.provider=redis", "APP_LEASE_REDIS_KEY_HMAC_SECRET=")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.isInstanceOf(StartupValidationException.class)
.hasStackTraceContaining("APP_LEASE_REDIS_KEY_HMAC_SECRET");
});
}
@Test
void redisSessionModeRequiresItsHmacMaterialInProd() {
runner
.withInitializer(ctx -> ctx.getEnvironment().setActiveProfiles("prod"))
.withPropertyValues(allRequiredSecretsPresent())
.withPropertyValues(
"ca-skeleton.security.auth-mode=redis-session",
"APP_SESSION_REDIS_PASSWORD=real-session-password",
"APP_SESSION_REDIS_KEY_HMAC_SECRET=")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.isInstanceOf(StartupValidationException.class)
.hasStackTraceContaining("APP_SESSION_REDIS_KEY_HMAC_SECRET");
});
}
@Configuration
static class ValidatorConfig {
@Bean
@@ -0,0 +1,191 @@
package dev.caskeleton.bootstrap.runtime.redis;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.Capability;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.EvictionAttestation;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.EvictionPolicy;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.Reason;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.Role;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.RoleHealth;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.Snapshot;
import dev.caskeleton.shared.health.RedisHealthSnapshotProvider.State;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.boot.health.contributor.Status;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class RedisHealthContributorConfigTest {
private static final Instant OBSERVED_AT = Instant.parse("2026-07-29T01:02:03Z");
private final ApplicationContextRunner runner =
new ApplicationContextRunner().withUserConfiguration(RedisHealthContributorConfig.class);
@Test
void noRoleBindingCreatesNoRedisHealthContributor() {
runner
.withBean(RedisHealthSnapshotProvider.class, () -> () -> snapshot())
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).doesNotHaveBean("redisRequired");
assertThat(context).doesNotHaveBean("redisOptional");
});
}
@Test
void declaredButUnselectedRoleCreatesNoRedisHealthContributor() {
runner
.withPropertyValues("ca-skeleton.providers.redis.roles.cache.deployment-id=cache-main")
.withBean(RedisHealthSnapshotProvider.class, () -> () -> snapshot())
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).doesNotHaveBean("redisRequired");
assertThat(context).doesNotHaveBean("redisOptional");
});
}
@Test
void optionalCacheOutageStaysUpAndReportsOnlyDegradedSanitizedDetail() {
runner
.withPropertyValues(
"ca-skeleton.capabilities.cache.bindings.default=redis",
"ca-skeleton.providers.redis.roles.cache.deployment-id=cache-main")
.withBean(
RedisHealthSnapshotProvider.class,
() ->
() ->
snapshot(
role(
Role.CACHE,
false,
Set.of(Capability.CACHE),
State.UNAVAILABLE,
Reason.SEMANTIC_PROGRAM_FAILED,
EvictionPolicy.ALLKEYS_LFU)))
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("redisOptional");
assertThat(context).doesNotHaveBean("redisRequired");
var health = context.getBean("redisOptional", HealthIndicator.class).health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails())
.containsEntry("state", "DEGRADED")
.doesNotContainKey("exception");
assertThat(health.toString())
.contains("SEMANTIC_PROGRAM_FAILED", "INCOMPLETE")
.doesNotContain("cache-main");
});
}
@Test
void requiredCoordinationOutageTurnsRequiredContributorDown() {
runner
.withPropertyValues(
"ca-skeleton.capabilities.rate-limit.provider=redis",
"ca-skeleton.providers.redis.roles.coordination.deployment-id=coord-main")
.withBean(
RedisHealthSnapshotProvider.class,
() ->
() ->
snapshot(
role(
Role.COORDINATION,
true,
Set.of(Capability.RATE_LIMIT, Capability.IDEMPOTENCY),
State.UNAVAILABLE,
Reason.SEMANTIC_PROGRAM_ACL_DENIED,
EvictionPolicy.NOEVICTION)))
.run(
context -> {
assertThat(context).hasNotFailed();
assertThat(context).hasBean("redisRequired");
assertThat(context).doesNotHaveBean("redisOptional");
var health = context.getBean("redisRequired", HealthIndicator.class).health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails())
.containsEntry("state", "UNAVAILABLE")
.containsEntry("missingRoles", List.of());
assertThat(health.toString()).contains("SEMANTIC_PROGRAM_ACL_DENIED");
});
}
@Test
void missingRequiredSessionSnapshotFailsClosed() {
runner
.withPropertyValues(
"ca-skeleton.security.auth-mode=redis-session",
"ca-skeleton.providers.redis.roles.session.deployment-id=session-main")
.withBean(RedisHealthSnapshotProvider.class, () -> () -> snapshot())
.run(
context -> {
assertThat(context).hasNotFailed();
var health = context.getBean("redisRequired", HealthIndicator.class).health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails())
.containsEntry("missingRoles", List.of(Role.SESSION.name()));
});
}
@Test
void availableRequiredRoleIsUpWhileEvictionRemainsExplicitlyConfigOnly() {
runner
.withPropertyValues(
"ca-skeleton.security.auth-mode=redis-session",
"ca-skeleton.providers.redis.roles.session.deployment-id=session-main")
.withBean(
RedisHealthSnapshotProvider.class,
() ->
() ->
snapshot(
role(
Role.SESSION,
true,
Set.of(Capability.SESSION),
State.AVAILABLE,
Reason.SEMANTIC_PROBE_SUCCEEDED,
EvictionPolicy.NOEVICTION)))
.run(
context -> {
assertThat(context).hasNotFailed();
var health = context.getBean("redisRequired", HealthIndicator.class).health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.toString())
.contains(EvictionAttestation.CONFIGURED_EXPECTATION_ONLY.name(), "INCOMPLETE");
});
}
private static Snapshot snapshot(RoleHealth... roles) {
return new Snapshot(OBSERVED_AT, List.of(roles));
}
private static RoleHealth role(
Role role,
boolean required,
Set<Capability> capabilities,
State state,
Reason reason,
EvictionPolicy eviction) {
return new RoleHealth(
role,
"deployment-id-never-exposed",
required,
eviction,
EvictionAttestation.CONFIGURED_EXPECTATION_ONLY,
capabilities,
state,
reason,
OBSERVED_AT,
0,
false);
}
}
@@ -0,0 +1,54 @@
package dev.caskeleton.bootstrap.security;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class AuthenticationModeCompositionConfigTest {
private final ApplicationContextRunner runner =
new ApplicationContextRunner()
.withUserConfiguration(AuthenticationModeCompositionConfig.class);
@Test
void jwtModeRequiresOnlyJwtInfrastructure() {
runner
.withBean("jwtDecoder", Object.class, Object::new)
.withPropertyValues("ca-skeleton.security.auth-mode=jwt")
.run(context -> assertThat(context).hasNotFailed());
runner
.withBean("jwtDecoder", Object.class, Object::new)
.withBean("redisVersionedSessionRepository", Object.class, Object::new)
.withPropertyValues("ca-skeleton.security.auth-mode=jwt")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.hasMessage(
"Authentication mode composition is not exclusive for JWT: [Redis Session repository/filter is active]");
});
}
@Test
void redisSessionModeRequiresCompleteSessionInfrastructureAndNoJwtDecoder() {
runner
.withBean("redisVersionedSessionRepository", Object.class, Object::new)
.withBean("springSessionRepositoryFilter", Object.class, Object::new)
.withPropertyValues("ca-skeleton.security.auth-mode=redis-session")
.run(context -> assertThat(context).hasNotFailed());
runner
.withBean("jwtDecoder", Object.class, Object::new)
.withBean("redisVersionedSessionRepository", Object.class, Object::new)
.withPropertyValues("ca-skeleton.security.auth-mode=redis-session")
.run(
context -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.hasMessage(
"Authentication mode composition is not exclusive for REDIS_SESSION: [jwtDecoder is active, Redis Session repository/filter is incomplete]");
});
}
}
@@ -150,15 +150,3 @@ ca-skeleton:
sampling-rate: 1.0
privacy:
pseudonymization-salt: __LOCAL_DEV_test_salt
# feature-outbound-http-client-baseline: required timeout properties for any test
# context that scans dev.caskeleton (OutboundHttpSettings requires non-zero timeouts).
app:
outbound:
http:
connect-timeout: 2s
read-timeout: 5s
global-call-timeout: 10s
retry-enabled: false
circuit-breaker-enabled: false
response-size-limit: 10MB