feat: Tech Log Studio 백엔드 기반 — 계약 배선, 오류 코드, 경계 규칙, 스키마, 엔드포인트 2종
설계 패키지의 studio-v1.yaml(v3.0.0, 응답 봉투)을 이 저장소에 배선하고 슬라이스 1의 기반을 세운다. 19개 operation 중 getStudioSession과 listStudioCatalog를 구현했다. 계약과 생성 - src/config/openapi/studio-v1.yaml 을 vendor하고 MANIFEST에 출처 커밋을 기록 - openapi-generator로 DTO(model)만 생성한다. generateApis 대신 globalProperties.set(['models': '']) — 그 두 속성은 플러그인 7.18.0에 없다 - useOneOfInterfaces=false. 그 대가로 discriminator union 5종의 Jackson 배선이 깨진다(spec §3.1). 그 5종을 쓰는 7개 operation은 Plan 02에서 전략을 정한 뒤 구현한다 - 생성 코드는 별도 generatedOpenapi sourceSet에 둔다. -Werror가 생성물의 deprecated API 사용을 빌드 실패로 승격하기 때문이다. jar와 test 클래스패스에 별도로 얹는다 오류 계약 - StudioError 23종(계약 ApiError.code와 1:1) + StudioException(ApiErrorCarrier) - StudioExceptionHandler는 techlog 패키지로 범위를 좁힌다. 다른 기능의 오류 응답을 바꾸지 않기 위해서다 - 클라이언트 문구는 레지스트리의 client_safe_message에서 가져오고 예외 메시지는 로그 전용이다(ApiErrorCarrier javadoc의 요구) - 바인딩 예외를 봉투로 옮긴다. 그러지 않으면 bare RFC 7807이 새어 나가 ADR-006을 위반한다 게이트 - TechLogBoundaryArchTest 7종 — spec §4.3의 bounded context 경계. Gradle leaf를 늘릴 수 없어 이 규칙이 경계의 유일한 방어선이다 - StudioErrorRegistryTest — enum ↔ 레지스트리 ↔ 계약 3축 대조, vendor 사본 해시 검증 - StudioContractDriftTest — springdoc 표면이 계약을 벗어나면 실패. @ComponentScan이라 새 컨트롤러가 자동으로 걸린다 - StudioSessionCsrfHeaderProfileContractTest — 배포 가능한 세 프로파일이 계약의 csrf-header-name const로 해소되는지 고정. 이 저장소는 실제 composition root를 테스트에서 부팅할 수 없어 파일 단언으로 그 층을 덮는다 스키마 - V7__techlog_core.sql, 28 테이블. 설계 DDL에서 studio_idempotency(기존 idempotency_record 재사용)와 범위 밖 6종을 제외했다 - 원본의 tech_log 스키마 대신 public을 쓴다. 원본의 SET search_path는 Flyway 세션에만 적용되고 런타임 커넥션 풀은 상속하지 않는다 알려진 제약 - getStudioSession은 세션 인프라(redis-session)가 없어 503 STUDIO_UNAVAILABLE을 반환한다. 계약이 이 operation에 허용하는 유일한 실패 코드다. 가짜 CSRF 토큰으로 200을 만들지 않았다 - 따라서 슬라이스 1의 "프론트 로그인 실동작" 목표는 아직 달성되지 않았다 이 커밋은 AGENTS.md의 human-only 커밋 정책에 대한 저장소 소유자의 명시적 지시로 작성됐다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
697fc740e6
commit
91e6d99654
@@ -1,3 +1,51 @@
|
||||
plugins { id 'org.openapi.generator' }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Studio 계약 DTO 생성 — sourceSet 정의와 jar/test classpath 배선 (ADR-004/ADR-006).
|
||||
// 이 블록은 파일 맨 위, 아래 커스텀 Test 태스크 등록(jpaPersistenceRedactionContractTest,
|
||||
// webSecurityBoundaryTest)보다 반드시 먼저 와야 한다. 그 둘은
|
||||
// `classpath = sourceSets.test.runtimeClasspath`로 **eager** 대입한다(Gradle 9.0.0의
|
||||
// DefaultSourceSet을 디컴파일해 확인 — lazy ConventionMapping이 아니라 단순
|
||||
// getfield/putfield). 이 배선이 그보다 아래 있으면, 커스텀 태스크는 아직 생성 DTO가
|
||||
// 안 얹힌 옛 FileCollection 참조를 이미 붙잡은 뒤라 나중에 sourceSet 필드를 새
|
||||
// composite로 갈아 끼워도 못 본다 — 컴파일이 아니라 테스트 런타임에 NoClassDefFoundError로
|
||||
// 터진다(발견 당시 실측). 표준 `test` 태스크는 JvmTestSuitePlugin이 lazy
|
||||
// ConventionMapping Callable로 배선해 괜찮지만(이것도 디컴파일로 확인), register()로 만든
|
||||
// 이 두 커스텀 Test 태스크는 lazy 배선을 안 타서 못 본다.
|
||||
//
|
||||
// openApiGenerate 확장 자체(무엇을 어떤 옵션으로 생성하는지)는 이 파일 아래쪽, 같은 제목의
|
||||
// 주석 섹션에 그대로 있다 — 여기는 sourceSet 정의와 그 소비자(jar, test classpath)만
|
||||
// 옮겼다. generatedOpenapiImplementation의 의존성 상속(configurations 블록)과
|
||||
// compileGeneratedOpenapiJava/checkstyle/spotbugs/spotless 배선도 원래 자리에 남아있다 —
|
||||
// 전부 lazy(tasks.named/tasks.matching)라 순서 문제가 없다.
|
||||
// ---------------------------------------------------------------------------
|
||||
sourceSets {
|
||||
generatedOpenapi {
|
||||
java.srcDir(layout.buildDirectory.dir('generated/openapi/src/main/java'))
|
||||
}
|
||||
// main이 생성 DTO를 참조할 수 있어야 한다(Task 8/9 controller). implementation
|
||||
// Configuration으로 연결하면(즉 main의 implementation에 generatedOpenapi.output을
|
||||
// 넣으면) 아래 generatedOpenapiImplementation.extendsFrom(implementation)과 맞물려
|
||||
// "컴파일하려면 자기 자신의 산출물이 먼저 있어야 한다"는 순환 태스크 의존성이 생긴다
|
||||
// (직접 겪음). 그래서 Configuration이 아니라 SourceSet의 compile/runtime classpath
|
||||
// FileCollection에 직접 이어 붙인다 — 태스크 의존성은 그대로 따라가면서 순환은 없다.
|
||||
main {
|
||||
compileClasspath += generatedOpenapi.output
|
||||
runtimeClasspath += generatedOpenapi.output
|
||||
}
|
||||
}
|
||||
|
||||
// main.runtimeClasspath로는 부족하다 — 그건 "실행 시 클래스를 찾을 수 있다"는 뜻일 뿐,
|
||||
// consumer(app-bootstrap 등)가 보는 web 모듈의 runtimeElements(= jar 산출물)에는 여전히
|
||||
// 생성 DTO가 없다. app-bootstrap은 web을 project dependency로만 물고 web의 build/classes를
|
||||
// 직접 보지 않으므로, jar 안에 없으면 부팅/요청 시 NoClassDefFoundError로 터진다.
|
||||
// 같은 이유로 test sourceSet도 main.output만 물려받지 generatedOpenapi.output까지
|
||||
// 자동으로 따라오지 않는다 — controller 테스트가 컴파일조차 안 된다. 셋 다 명시적으로
|
||||
// 채워야 한다.
|
||||
tasks.named('jar') { from sourceSets.generatedOpenapi.output }
|
||||
sourceSets.test.compileClasspath += sourceSets.generatedOpenapi.output
|
||||
sourceSets.test.runtimeClasspath += sourceSets.generatedOpenapi.output
|
||||
|
||||
// HTTP / web adapters. Depends on application and shared operational contracts.
|
||||
dependencies {
|
||||
implementation project(':application-core')
|
||||
@@ -72,3 +120,95 @@ tasks.register('webSecurityBoundaryTest', Test) {
|
||||
tasks.named('check') {
|
||||
dependsOn tasks.named('webSecurityBoundaryTest')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Studio 계약 DTO 생성 (ADR-004 / ADR-006).
|
||||
//
|
||||
// generateApis에 해당하는 효과: 계약이 봉투를 기술하므로 API interface까지 생성하면
|
||||
// 봉투 wrapper 타입을 반환하게 되고, 그 타입은 dev.caskeleton.shared.response.Envelope가
|
||||
// 아니라서 EnvelopeBodyAdvice가 한 번 더 감싼다(이중 래핑). controller는 손으로 쓴다.
|
||||
//
|
||||
// globalProperties.set(['models': '']): openapi-generator-gradle-plugin 7.18.0의
|
||||
// openApiGenerate 확장에는 generateApis/generateModels/generateSupportingFiles 프로퍼티가
|
||||
// 존재하지 않는다(디컴파일로 확인, task-4-report.md 참고) — 대신 CLI --global-property와
|
||||
// 같은 의미인 globalProperties로 "models만" 생성하게 제한한다.
|
||||
//
|
||||
// useOneOfInterfaces=false: discriminator(oneOf) union을 부모 Java interface로 생성하면
|
||||
// 하위 타입이 그 인터페이스를 구현하는데, 판별 필드가 enum이면(narrowing 여부와 무관하게)
|
||||
// 인터페이스의 getter는 무조건 String을 반환하고 하위 타입의 getter는 그 프로퍼티의 실제
|
||||
// 타입(nested enum이든 공유 named enum이든)을 반환해 컴파일이 깨진다. 판별 필드를 하위
|
||||
// 타입에서 narrowing하지 않고 base의 공유 enum(RecordKind 등)을 그대로 상속하게 계약을
|
||||
// 고쳐도 동일하게 깨진다는 것까지 스크래치에서 직접 검증했다 — SpringCodegen이
|
||||
// useOneOfInterfaces=true일 때 discriminator getter를 String으로 고정하는 게 근본
|
||||
// 원인이라 계약 쪽에서 우회할 수 없다(3개 설정 조합 + 이 검증 전부 task-4-report.md 참고).
|
||||
//
|
||||
// useOneOfInterfaces=false는 컴파일은 통과시키지만 대가가 있다: 각 하위 타입이 독립
|
||||
// 클래스로 생성되고(WorkingCopyInput 등 union 타입과 CaseInput 등 하위 타입 사이에
|
||||
// Java의 implements 관계가 전혀 없다), 그리고 실측 결과 Jackson 배선도 계약대로 동작하지
|
||||
// 않는다 — 역직렬화는 InvalidTypeIdException으로 실패하고("Class CaseInput not subtype
|
||||
// of WorkingCopyInput"), 직렬화는 @JsonIgnoreProperties(value="kind", allowSetters=true)
|
||||
// 때문에 실제 kind 값 대신 클래스 simple name이 나간다. 즉 생성된 union 클래스는 Jackson
|
||||
// 양방향 모두 계약을 위반한다. union을 필드 타입으로 쓰는 5개 union(WorkingCopyInput,
|
||||
// WorkingCopy, Inline, CaseRenderBlock, PublicRenderModel)을 실제로 쓰는 operation은
|
||||
// Plan 02에서 전략을 정한 뒤 구현한다 — 이번 Task 8/9(getStudioSession,
|
||||
// listStudioCatalog)는 이 union들을 쓰지 않으므로 막히지 않는다.
|
||||
// ---------------------------------------------------------------------------
|
||||
openApiGenerate {
|
||||
generatorName = 'spring'
|
||||
inputSpec = "${rootDir}/config/openapi/studio-v1.yaml".toString()
|
||||
outputDir = layout.buildDirectory.dir('generated/openapi').get().asFile.path
|
||||
modelPackage = 'dev.caskeleton.adapter.inbound.web.techlog.studio.api.model'
|
||||
globalProperties.set(['models': ''])
|
||||
generateModelTests = false
|
||||
generateModelDocumentation = false
|
||||
configOptions = [
|
||||
useSpringBoot3: 'true',
|
||||
useJakartaEe: 'true',
|
||||
openApiNullable: 'true',
|
||||
useOneOfInterfaces: 'false',
|
||||
]
|
||||
}
|
||||
|
||||
// sourceSets.generatedOpenapi 정의와 그걸 소비하는 jar/test classpath 배선은 이 파일
|
||||
// 맨 위로 옮겼다(리뷰 라운드 2 fix) — 이유는 그 자리의 주석 참고. 여기 남은 건
|
||||
// generatedOpenapiImplementation의 의존성 상속뿐이다.
|
||||
configurations {
|
||||
// 생성 DTO 컴파일에 필요한 의존성(jakarta.validation, swagger-annotations,
|
||||
// jackson-databind-nullable, spring-web의 @Nullable/@DateTimeFormat 등)은 이미 main의
|
||||
// implementation에 다 있다 — 따로 중복 선언하지 않고 그대로 물려받는다. main의
|
||||
// implementation은 generatedOpenapi.output을 포함하지 않으므로(위 참고) 이 확장은
|
||||
// 순환을 만들지 않는다.
|
||||
generatedOpenapiImplementation.extendsFrom(implementation)
|
||||
}
|
||||
|
||||
tasks.named('compileGeneratedOpenapiJava', JavaCompile) {
|
||||
dependsOn 'openApiGenerate'
|
||||
options.errorprone.enabled = false
|
||||
// 루트 build.gradle의 tasks.withType(JavaCompile).configureEach가 -Werror와
|
||||
// -Xlint:deprecation을 이미 넣어 놓은 뒤에 이 설정이 평가되므로(subprojects 블록이
|
||||
// 먼저, 이 파일이 나중) 여기서 빼는 게 마지막 값으로 남는다.
|
||||
doFirst {
|
||||
options.compilerArgs.removeAll(['-Werror', '-Xlint:deprecation'])
|
||||
}
|
||||
}
|
||||
|
||||
// checkstyle/spotbugs는 sourceSet마다 별도 태스크(checkstyleGeneratedOpenapi,
|
||||
// spotbugsGeneratedOpenapi)를 만든다 — 그 태스크만 끈다. checkstyleMain/spotbugsMain은
|
||||
// 생성 코드가 더 이상 main sourceSet에 없으므로 애초에 이 파일들을 보지 않는다.
|
||||
tasks.matching { it.name == 'checkstyleGeneratedOpenapi' }.configureEach {
|
||||
dependsOn 'openApiGenerate'
|
||||
enabled = false
|
||||
}
|
||||
tasks.matching { it.name == 'spotbugsGeneratedOpenapi' }.configureEach {
|
||||
dependsOn 'openApiGenerate'
|
||||
enabled = false
|
||||
}
|
||||
|
||||
// spotless는 sourceSet과 무관하게 글롭으로 java 파일을 찾으므로 생성 경로를 명시적으로
|
||||
// 뺀다. spotlessJava가 openApiGenerate보다 먼저 돌면 아직 없는 디렉터리를 글롭 검사하다
|
||||
// 있으나 마나 한 차이라 dependsOn은 필요 없지만, 생성 전 상태에서 우연히 이전 빌드의 생성물이
|
||||
// 남아 채점되는 걸 막기 위해 순서를 맞춘다.
|
||||
tasks.matching { it.name.startsWith('spotless') }.configureEach {
|
||||
dependsOn 'openApiGenerate'
|
||||
}
|
||||
spotless { java { targetExclude('build/generated/**') } }
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
# 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.
|
||||
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,testCompileClasspath
|
||||
ch.qos.logback:logback-classic:1.5.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
ch.qos.logback:logback-core:1.5.21=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml:classmate:1.7.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,testAnnotationProcessor
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,testAnnotationProcessor
|
||||
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,generatedOpenapiCompileClasspath,testCompileClasspath
|
||||
ch.qos.logback:logback-classic:1.5.21=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
ch.qos.logback:logback-core:1.5.21=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml:classmate:1.7.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
|
||||
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,testCompileClasspath
|
||||
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,generatedOpenapiCompileClasspath,testCompileClasspath
|
||||
com.github.spotbugs:spotbugs:4.10.2=spotbugs
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,testAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.2=annotationProcessor,testAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,spotbugs,testCompileClasspath
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.2=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,generatedOpenapiCompileClasspath,spotbugs,testCompileClasspath
|
||||
com.google.code.gson:gson:2.13.2=spotbugs
|
||||
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,testCompileClasspath
|
||||
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,generatedOpenapiCompileClasspath,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,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.guava:guava:33.5.0-jre=annotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:guava:33.5.0-jre=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:guava:33.6.0-jre=checkstyle
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,testAnnotationProcessor
|
||||
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,testAnnotationProcessor
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
|
||||
com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath
|
||||
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
|
||||
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath
|
||||
commons-beanutils:commons-beanutils:1.11.0=checkstyle
|
||||
commons-collections:commons-collections:3.2.2=checkstyle
|
||||
commons-io:commons-io:2.21.0=spotbugs
|
||||
commons-logging:commons-logging:1.3.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
commons-logging:commons-logging:1.3.5=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
info.picocli:picocli:4.7.7=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,testAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,testAnnotationProcessor
|
||||
io.micrometer:micrometer-commons:1.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-observation:1.16.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.projectreactor:reactor-core:3.8.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
io.micrometer:micrometer-commons:1.16.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-observation:1.16.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.projectreactor:reactor-core:3.8.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.projectreactor:reactor-test:3.8.0=testCompileClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-core-jakarta:2.2.38=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-models-jakarta:2.2.38=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.activation:jakarta.activation-api:2.1.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,testAnnotationProcessor
|
||||
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-core-jakarta:2.2.38=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-models-jakarta:2.2.38=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.activation:jakarta.activation-api:2.1.4=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
jaxen:jaxen:2.0.0=spotbugs
|
||||
net.bytebuddy:byte-buddy-agent:1.17.8=testCompileClasspath,testRuntimeClasspath
|
||||
net.bytebuddy:byte-buddy:1.17.8=testCompileClasspath,testRuntimeClasspath
|
||||
@@ -68,21 +68,21 @@ net.minidev:json-smart:2.6.0=testCompileClasspath,testRuntimeClasspath
|
||||
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
|
||||
org.antlr:antlr4-runtime:4.13.2=checkstyle
|
||||
org.apache.bcel:bcel:6.12.0=spotbugs
|
||||
org.apache.commons:commons-lang3:3.20.0=checkstyle,compileClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.commons:commons-lang3:3.20.0=checkstyle,compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,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
|
||||
org.apache.httpcomponents:httpcore:4.4.16=checkstyle
|
||||
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
|
||||
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,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:11.0.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.xbean:xbean-reflect:3.7=checkstyle
|
||||
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
|
||||
org.assertj:assertj-core:3.27.6=testCompileClasspath,testRuntimeClasspath
|
||||
@@ -93,10 +93,10 @@ 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.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.javassist:javassist:3.28.0-GA=checkstyle
|
||||
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
|
||||
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,generatedOpenapiAnnotationProcessor,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:6.0.1=testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-params:6.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
@@ -109,80 +109,80 @@ org.junit:junit-bom:6.1.0=spotbugs
|
||||
org.mockito:mockito-core:5.20.0=mockitoAgent,testCompileClasspath,testRuntimeClasspath
|
||||
org.mockito:mockito-junit-jupiter:5.20.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.objenesis:objenesis:3.3=testRuntimeClasspath
|
||||
org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.resource:1.0.0=compileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,generatedOpenapiCompileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,generatedOpenapiCompileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.resource:1.0.0=compileClasspath,generatedOpenapiCompileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,generatedOpenapiCompileClasspath,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=testCompileClasspath,testRuntimeClasspath
|
||||
org.pcollections:pcollections:4.0.1=annotationProcessor,testAnnotationProcessor
|
||||
org.reactivestreams:reactive-streams:1.0.4=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.pcollections:pcollections:4.0.1=annotationProcessor,generatedOpenapiAnnotationProcessor,testAnnotationProcessor
|
||||
org.reactivestreams:reactive-streams:1.0.4=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.reflections:reflections:0.10.2=checkstyle
|
||||
org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-api:2.0.17=compileClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-api:2.0.17=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
|
||||
org.springdoc:springdoc-openapi-starter-common:3.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-common:3.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-configuration-processor:4.0.0=annotationProcessor
|
||||
org.springframework.boot:spring-boot-http-client:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-http-converter:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-jackson:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-http-converter:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-jackson:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-restclient:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-resttestclient:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-security:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-servlet:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-security:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-servlet:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jackson:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-security:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jackson:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-security:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-web:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-web:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-tomcat:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-validation:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-web-server:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-tomcat:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-validation:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-web-server:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-webmvc-test:4.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-webmvc:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-config:7.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-core:7.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-jose:7.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-webmvc:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-config:7.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-core:7.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-jose:7.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-test:7.0.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-web:7.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.session:spring-session-core:4.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aop:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-beans:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-context:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-core:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-expression:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-web:7.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.session:spring-session-core:4.0.0=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aop:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-beans:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-context:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-core:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-expression:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-test:7.0.1=testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-web:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webflux:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webmvc:7.0.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-web:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webflux:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webmvc:7.0.1=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
|
||||
org.xmlunit:xmlunit-core:2.10.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.yaml:snakeyaml:2.5=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-core:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-databind:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson:jackson-bom:3.0.2=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.yaml:snakeyaml:2.5=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-core:3.0.2=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-databind:3.0.2=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson:jackson-bom:3.0.2=compileClasspath,generatedOpenapiCompileClasspath,generatedOpenapiRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
empty=
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog;
|
||||
|
||||
import dev.caskeleton.application.techlog.error.StudioError;
|
||||
|
||||
/**
|
||||
* Studio 실패의 client-safe {@code error.message} 단일 출처.
|
||||
*
|
||||
* <p>{@code StudioException#getMessage()}는 Studio use case/facade가 진단용으로 채우는 원문이라 {@code
|
||||
* ApiErrorCarrier} javadoc이 경고하는 대로 SQLState나 upstream detail을 실을 수 있다. 그래서 응답에는 절대 흘리지 않고, 이 클래스가
|
||||
* code별 고정 문구만 내보낸다 — 스켈레톤의 {@code ClientSafeErrorMessages}가 {@code OperationalError}에 대해 하는 것과 같은
|
||||
* 역할을 {@link StudioError}에 대해 한다.
|
||||
*
|
||||
* <p>문구는 {@code docs/registries/error-codes.yaml}의 각 row {@code client_safe_message}와 정확히 같아야 한다 —
|
||||
* {@code StudioErrorRegistryTest}가 그 일치를 고정한다. {@code PAYLOAD_TOO_LARGE}/{@code
|
||||
* UNSUPPORTED_MEDIA_TYPE}은 Studio 전용 row가 없고 {@code feature-api-contract-baseline}이 이미 등록한 row를
|
||||
* 재사용하므로(Task 5 report 5절) 그 row의 영문 문구를 그대로 따른다 — 나머지는 Studio 전용 한국어 문구다.
|
||||
*
|
||||
* <p>{@link StudioError}를 exhaustive switch로 매핑하므로(default 없음) 새 상수를 추가하면 이 파일도 컴파일 타임에 고쳐야 한다 — 문구
|
||||
* 누락이 생길 수 없다.
|
||||
*/
|
||||
public final class StudioClientSafeMessages {
|
||||
|
||||
private StudioClientSafeMessages() {}
|
||||
|
||||
public static String forError(StudioError error) {
|
||||
return switch (error) {
|
||||
case AUTHENTICATION_REQUIRED -> "Studio 인증이 필요합니다";
|
||||
case STUDIO_ACCESS_DENIED -> "이 Studio 리소스에 접근할 권한이 없습니다";
|
||||
case DOCUMENT_NOT_FOUND -> "요청한 문서를 찾을 수 없습니다";
|
||||
case VERSION_CONFLICT -> "저장된 version이 더 최신입니다";
|
||||
case REQUEST_VALIDATION_FAILED -> "요청 형식이 올바르지 않습니다";
|
||||
case DOCUMENT_VALIDATION_FAILED -> "문서 검증에 실패했습니다";
|
||||
case VALIDATION_STALE -> "검증 결과가 최신 문서 기준이 아닙니다. 다시 검증해 주세요";
|
||||
case PREVIEW_NOT_FOUND -> "요청한 미리보기를 찾을 수 없습니다";
|
||||
case PREVIEW_STALE -> "미리보기가 최신 문서 기준이 아닙니다. 다시 생성해 주세요";
|
||||
case PREVIEW_EXPIRED -> "미리보기가 만료되었습니다. 다시 생성해 주세요";
|
||||
case PUBLICATION_NOT_FOUND -> "요청한 게시물을 찾을 수 없습니다";
|
||||
case PUBLICATION_CONFLICT -> "게시 작업이 다른 변경과 충돌했습니다";
|
||||
case PUBLICATION_EVENT_NOT_FOUND -> "요청한 게시 이벤트를 찾을 수 없습니다";
|
||||
case PUBLICATION_SNAPSHOT_NOT_FOUND -> "요청한 게시 스냅샷을 찾을 수 없습니다";
|
||||
case WARNING_ACKNOWLEDGEMENT_REQUIRED -> "경고 확인이 필요합니다. 확인 후 다시 시도해 주세요";
|
||||
case IDEMPOTENCY_KEY_REUSED -> "Idempotency 키가 다른 요청에 재사용되었습니다";
|
||||
case ASSET_NOT_FOUND -> "요청한 자산을 찾을 수 없습니다";
|
||||
case ASSET_NOT_READY -> "자산 처리가 아직 완료되지 않았습니다";
|
||||
case ASSET_IN_USE -> "자산이 사용 중이라 이 작업을 수행할 수 없습니다";
|
||||
case ASSET_QUARANTINED -> "자산이 격리 처리되어 사용할 수 없습니다";
|
||||
case PAYLOAD_TOO_LARGE -> "Request payload is too large";
|
||||
case UNSUPPORTED_MEDIA_TYPE -> "Request Content-Type is not supported";
|
||||
case STUDIO_UNAVAILABLE -> "Studio 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도해 주세요";
|
||||
};
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.error.ErrorResponseFactory;
|
||||
import dev.caskeleton.application.techlog.error.StudioError;
|
||||
import dev.caskeleton.application.techlog.error.StudioException;
|
||||
import dev.caskeleton.shared.response.Envelope;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
|
||||
/**
|
||||
* Studio 실패를 스켈레톤 봉투로 옮긴다. 템플릿의 {@code GlobalExceptionHandler}를 수정하지 않기 위해 별도 advice로 둔다 — 그 파일은
|
||||
* template sync 대상이다.
|
||||
*
|
||||
* <p>{@code error.message}에는 {@link StudioClientSafeMessages}가 주는 code별 고정 문구만 싣는다 — {@link
|
||||
* StudioException#getMessage()}(진단용 원문)는 SQLState·upstream detail을 실을 수 있어 client-unsafe하다({@code
|
||||
* ApiErrorCarrier} javadoc). 원문은 버리지 않고 서버 로그에만 남긴다 — {@code GlobalExceptionHandler}의 {@code
|
||||
* handlePersistenceFailure}/{@code handleDependencyFailure}가 분류된 하위 계층 실패를 로깅하는 것과 같은 패턴이다.
|
||||
*
|
||||
* <p><b>{@code basePackages} 스코프 (final whole-branch review B4).</b> 이 advice는 {@code
|
||||
* dev.caskeleton.adapter.inbound.web.techlog} 아래의 컨트롤러(현재 studio 컨트롤러 전부가 여기 산다, {@code
|
||||
* studio.controller})에만 적용된다. {@link #handleMissingParameter}/{@link #handleTypeMismatch}는 Spring
|
||||
* MVC 표준 바인딩 예외를 계약 코드로 옮기는데, 스코프 없이 전역으로 두면 fileserver·healthcheck 같은 studio 밖 컨트롤러의 같은 예외까지 가로채 그
|
||||
* 기능들의 기존 오류 응답 모양(바로 이 advice가 없었을 때의 {@code GlobalExceptionHandler} 동작)을 바꿔버린다 — 이 브랜치가 건드릴 권한이
|
||||
* 없는 기능이다. {@code StudioException} 처리는 애초에 studio 코드만 이 예외를 던지므로 스코프를 좁혀도 동작이 바뀌지 않는다.
|
||||
*/
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@RestControllerAdvice(basePackages = "dev.caskeleton.adapter.inbound.web.techlog")
|
||||
public class StudioExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(StudioExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(StudioException.class)
|
||||
public ResponseEntity<Envelope<Void>> handleStudio(StudioException ex) {
|
||||
StudioError error = ex.studioError();
|
||||
log.error(
|
||||
"studio failure classified as {} (category={}, retryable={}): {}",
|
||||
error.code(),
|
||||
error.category(),
|
||||
error.retryable(),
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
return ErrorResponseFactory.envelope(
|
||||
error, StudioClientSafeMessages.forError(error), ex.details());
|
||||
}
|
||||
|
||||
/**
|
||||
* 필수 쿼리 파라미터 누락(예: {@code GET /api/v1/studio/catalog}의 {@code type}). {@code
|
||||
* GlobalExceptionHandler}는 이 예외를 오버라이드하지 않으므로 부모 {@code ResponseEntityExceptionHandler}가 그대로 bare
|
||||
* {@code ProblemDetail}(content-type {@code application/problem+json})을 만들고, {@code
|
||||
* EnvelopeBodyAdvice}의 JSON 미디어타입 검사에 걸려 봉투를 못 씌운다 — ADR-006이 쓰지 않기로 한 RFC 7807이 그대로 나간다(final
|
||||
* whole-branch review B4). studio 스코프에서 계약 코드 {@link StudioError#REQUEST_VALIDATION_FAILED}(422)로
|
||||
* 옮긴다.
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public ResponseEntity<Envelope<Void>> handleMissingParameter(
|
||||
MissingServletRequestParameterException ex) {
|
||||
return requestValidationFailed(ex.getParameterName(), "Required parameter is missing");
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿼리 파라미터 타입 불일치(예: {@code type=BOGUS}, {@code limit=abc}). {@code GlobalExceptionHandler}도 이 예외를
|
||||
* 처리하지만 {@code OperationalError.BAD_PARAMETER}를 낸다 — Studio 계약 23종에 없는 코드다. studio 스코프에서 계약 코드로
|
||||
* 옮긴다.
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public ResponseEntity<Envelope<Void>> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
|
||||
return requestValidationFailed(ex.getName(), "Parameter value is invalid");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code details}를 계약의 {@code ValidationErrorDetails}({@code fieldErrors: [{path, message}]}) 모양에
|
||||
* 맞춰 싣는다 — 자유형 {@code Object}로 아무 모양이나 실으면 계약의 {@code oneOf} 제약을 위반한다.
|
||||
*/
|
||||
private static ResponseEntity<Envelope<Void>> requestValidationFailed(
|
||||
String parameterName, String message) {
|
||||
Map<String, Object> fieldError = Map.of("path", "/" + parameterName, "message", message);
|
||||
Map<String, Object> details = Map.of("fieldErrors", List.of(fieldError));
|
||||
return ErrorResponseFactory.envelope(
|
||||
StudioError.REQUEST_VALIDATION_FAILED,
|
||||
StudioClientSafeMessages.forError(StudioError.REQUEST_VALIDATION_FAILED),
|
||||
details);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog.studio.controller;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.studio.api.model.CatalogEntry;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.studio.api.model.CatalogPage;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryType;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryView;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogPageView;
|
||||
import dev.caskeleton.application.techlog.studio.query.ListCatalogQuery;
|
||||
import dev.caskeleton.application.techlog.studio.service.ListCatalogUseCase;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 반환값을 Envelope로 감싸지 않는다 — EnvelopeBodyAdvice가 감싼다.
|
||||
*
|
||||
* <p>{@code CatalogEntryType}은 application({@code
|
||||
* dev.caskeleton.application.techlog.studio.query})과 웹 계약 생성 DTO({@code
|
||||
* dev.caskeleton.adapter.inbound.web.techlog.studio.api.model})에 같은 이름으로 각각 존재한다 — 한 파일에서 둘 다 단일 타입
|
||||
* import로 쓰면 컴파일이 깨진다. 이 컨트롤러는 application 쪽을 단일 import로 쓰고(요청 파라미터·use case 입력), 웹 계약 쪽은 {@link
|
||||
* #toApi}에서 FQN으로만 참조한다(응답 DTO 조립).
|
||||
*/
|
||||
@RestController
|
||||
public class StudioCatalogController {
|
||||
|
||||
private final ListCatalogUseCase listCatalog;
|
||||
|
||||
public StudioCatalogController(ListCatalogUseCase listCatalog) {
|
||||
this.listCatalog = listCatalog;
|
||||
}
|
||||
|
||||
@GetMapping("/api/v1/studio/catalog")
|
||||
public CatalogPage listStudioCatalog(
|
||||
@RequestParam("type") CatalogEntryType type,
|
||||
@RequestParam(value = "q", required = false) String q,
|
||||
@RequestParam(value = "cursor", required = false) String cursor,
|
||||
@RequestParam(value = "limit", defaultValue = "20") int limit) {
|
||||
CatalogPageView page = listCatalog.handle(new ListCatalogQuery(type, q, cursor, limit));
|
||||
CatalogPage body = new CatalogPage();
|
||||
body.setItems(page.items().stream().map(StudioCatalogController::toApi).toList());
|
||||
body.setNextCursor(page.nextCursor());
|
||||
return body;
|
||||
}
|
||||
|
||||
private static CatalogEntry toApi(CatalogEntryView view) {
|
||||
CatalogEntry entry = new CatalogEntry();
|
||||
entry.setId(view.id());
|
||||
entry.setType(
|
||||
dev.caskeleton.adapter.inbound.web.techlog.studio.api.model.CatalogEntryType.fromValue(
|
||||
view.type().name()));
|
||||
entry.setLabel(view.label());
|
||||
entry.setDependencyRevision(view.dependencyRevision());
|
||||
if (view.kind() != null) {
|
||||
entry.setKind(CatalogEntry.KindEnum.fromValue(view.kind()));
|
||||
}
|
||||
entry.setPublicPath(view.publicPath());
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog.studio.controller;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
|
||||
import dev.caskeleton.adapter.inbound.web.settings.SecuritySettings;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.studio.api.model.StudioSession;
|
||||
import dev.caskeleton.application.techlog.error.StudioError;
|
||||
import dev.caskeleton.application.techlog.error.StudioException;
|
||||
import java.util.Set;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 세션은 순수 전송 상태다 — principal과 CSRF 토큰뿐이라 도메인 규칙이 없다. application use case를 끼우지 않는 이유이고,
|
||||
* application-core는 Spring을 볼 수 없어 SecurityContext에 접근할 수도 없다.
|
||||
*
|
||||
* <p>반환값을 {@code Envelope}로 감싸지 않는다. {@code EnvelopeBodyAdvice}가 감싼다.
|
||||
*
|
||||
* <p><b>CSRF가 꺼진 auth-mode.</b> {@code SecurityConfig.filterChain}의 JWT 분기는 {@code csrf(csrf ->
|
||||
* csrf.disable())}로 {@code CsrfConfigurer} 자체를 제거한다 — {@code CsrfFilter}가 돌지 않고 request attribute를
|
||||
* 아무도 채우지 않는다. 그런데 Spring Security 7.0.0의 {@code CsrfTokenArgumentResolver}는
|
||||
* {@code @EnableWebSecurity}만 있으면 무조건 등록되고, {@code resolveArgument}는 request attribute를 캐스팅만 할 뿐
|
||||
* null 체크가 없다 — 그래서 CSRF가 꺼진 모드에서는 {@code csrfToken} 파라미터가 항상 {@code null}이다. Studio는 CSRF 없이 실제로
|
||||
* 동작할 수 없으므로, 가짜 토큰을 지어내 200을 돌려주는 대신 {@link StudioError#STUDIO_UNAVAILABLE}(503)로 그 사실을 정직하게 보고한다
|
||||
* — 세션 인프라가 갖춰지면(redis-session + 완전한 CSRF 배선) 코드 변경 없이 200으로 바뀐다. ({@code
|
||||
* StudioSessionCsrfDisabledTest}가 이 경로를 고정한다.)
|
||||
*/
|
||||
@RestController
|
||||
public class StudioSessionController {
|
||||
|
||||
/** studio-v1.yaml {@code StudioSession.csrfHeaderName}은 {@code const} — 이 값만 유효하다. */
|
||||
private static final String CONTRACT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
|
||||
|
||||
/** studio-v1.yaml {@code StudioSession.roles.maxItems}. */
|
||||
private static final int CONTRACT_MAX_ROLES = 20;
|
||||
|
||||
private final String csrfHeaderName;
|
||||
|
||||
/**
|
||||
* {@code csrf-header-name}을 하드코드하지 않고 설정에서 읽되, 계약이 고정한 값과 다르면 부팅 시점에 즉시 실패한다 — 계약의 {@code const}와
|
||||
* 설정이 갈라지는 건 배포 오류이지 런타임에 조용히 넘어갈 문제가 아니다.
|
||||
*/
|
||||
public StudioSessionController(SecuritySettings securitySettings) {
|
||||
String configured = securitySettings.session().csrfHeaderName();
|
||||
if (!CONTRACT_CSRF_HEADER_NAME.equals(configured)) {
|
||||
throw new IllegalStateException(
|
||||
"ca-skeleton.security.session.csrf-header-name must be \""
|
||||
+ CONTRACT_CSRF_HEADER_NAME
|
||||
+ "\" (studio-v1.yaml StudioSession.csrfHeaderName is a contract const) but was"
|
||||
+ " configured as \""
|
||||
+ configured
|
||||
+ "\"");
|
||||
}
|
||||
this.csrfHeaderName = configured;
|
||||
}
|
||||
|
||||
@GetMapping("/api/v1/studio/session")
|
||||
public StudioSession getStudioSession(
|
||||
@AuthenticationPrincipal AuthenticatedPrincipal principal, CsrfToken csrfToken) {
|
||||
if (csrfToken == null) {
|
||||
throw StudioException.of(
|
||||
StudioError.STUDIO_UNAVAILABLE,
|
||||
"CSRF token unavailable: CSRF protection is disabled for the active auth-mode");
|
||||
}
|
||||
|
||||
Set<String> roles = Set.copyOf(principal.roles());
|
||||
if (roles.size() > CONTRACT_MAX_ROLES) {
|
||||
// 조용히 잘라내면 클라이언트가 실제 권한과 다른 role 집합을 받는다 — IdP 쪽 role 매핑이 잘못됐다는
|
||||
// 신호를 숨기는 셈이라, 잘라내는 대신 실패시켜 드러낸다.
|
||||
throw StudioException.of(
|
||||
StudioError.STUDIO_UNAVAILABLE,
|
||||
"principal role count "
|
||||
+ roles.size()
|
||||
+ " exceeds contract max "
|
||||
+ CONTRACT_MAX_ROLES
|
||||
+ " (studio-v1.yaml StudioSession.roles.maxItems)");
|
||||
}
|
||||
|
||||
String displayName = displayNameOf(principal);
|
||||
if (displayName == null || displayName.isBlank()) {
|
||||
throw StudioException.of(
|
||||
StudioError.STUDIO_UNAVAILABLE,
|
||||
"principal has neither a usable email nor idpUserId; cannot satisfy"
|
||||
+ " StudioSession.displayName minLength 1");
|
||||
}
|
||||
|
||||
StudioSession session = new StudioSession();
|
||||
session.setAuthenticated(true);
|
||||
session.setDisplayName(displayName);
|
||||
session.setRoles(roles);
|
||||
session.setCsrfToken(csrfToken.getToken());
|
||||
session.setCsrfHeaderName(csrfHeaderName);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* `displayName`은 계약상 1자 이상이다. profile capability(identity 모듈)가 들어오기 전까지 email을 쓰고, 없으면 IdP
|
||||
* subject로 대체한다. 둘 다 비어 있으면 {@code getStudioSession}이 실패시킨다(위 참조).
|
||||
*/
|
||||
private static String displayNameOf(AuthenticatedPrincipal principal) {
|
||||
String email = principal.email();
|
||||
return (email == null || email.isBlank()) ? principal.idpUserId() : email;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package dev.caskeleton.adapter.inbound.web.error;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.StudioExceptionHandler;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* final whole-branch review B4: proves the negative for {@link StudioExceptionHandler}'s
|
||||
* {@code @RestControllerAdvice(basePackages = "dev.caskeleton.adapter.inbound.web.techlog")}
|
||||
* scoping.
|
||||
*
|
||||
* <p>{@link Probe} lives in {@code dev.caskeleton.adapter.inbound.web.error} — outside the {@code
|
||||
* ...web.techlog} tree the advice is scoped to, standing in for a non-Studio feature (fileserver,
|
||||
* healthcheck). A missing required parameter on it must still fall through to the inherited {@code
|
||||
* ResponseEntityExceptionHandler} behaviour (bare {@code ProblemDetail}, {@code
|
||||
* application/problem+json}) exactly as it did before {@code StudioExceptionHandler} grew {@code
|
||||
* MissingServletRequestParameterException}/{@code MethodArgumentTypeMismatchException} handlers —
|
||||
* this branch has no mandate to change fileserver/healthcheck's error shape. If the {@code
|
||||
* basePackages} scope were ever dropped or widened to cover this package, this probe would start
|
||||
* seeing {@code REQUEST_VALIDATION_FAILED} at 422 instead and this test would fail.
|
||||
*/
|
||||
@WebMvcTest(
|
||||
controllers = StudioExceptionHandlerScopeTest.Probe.class,
|
||||
excludeAutoConfiguration = SecurityAutoConfiguration.class)
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
@Import({
|
||||
StudioExceptionHandlerScopeTest.Probe.class,
|
||||
StudioExceptionHandler.class,
|
||||
GlobalExceptionHandler.class,
|
||||
EnvelopeBodyAdvice.class
|
||||
})
|
||||
class StudioExceptionHandlerScopeTest {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void missingParameterOnANonStudioControllerKeepsTheUnenvelopedGlobalHandlerBehaviour()
|
||||
throws Exception {
|
||||
mvc.perform(get("/probe/non-studio"))
|
||||
.andExpect(status().is(400))
|
||||
.andExpect(
|
||||
content().contentTypeCompatibleWith(MediaType.valueOf("application/problem+json")));
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class Probe {
|
||||
@GetMapping("/probe/non-studio")
|
||||
String probe(@RequestParam("required") String required) {
|
||||
return required;
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
|
||||
static class TestBootstrap {}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.application.techlog.error.StudioError;
|
||||
import dev.caskeleton.application.techlog.error.StudioException;
|
||||
import dev.caskeleton.shared.response.Envelope;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
class StudioExceptionHandlerTest {
|
||||
|
||||
private final StudioExceptionHandler handler = new StudioExceptionHandler();
|
||||
|
||||
@Test
|
||||
void mapsStudioExceptionToFailureEnvelopeWithContractCode() {
|
||||
ResponseEntity<Envelope<Void>> response =
|
||||
handler.handleStudio(StudioException.of(StudioError.DOCUMENT_NOT_FOUND, "없음"));
|
||||
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(404);
|
||||
Envelope<Void> body = response.getBody();
|
||||
assertThat(body).isNotNull();
|
||||
assertThat(body.success()).isFalse();
|
||||
assertThat(body.error().code()).isEqualTo("DOCUMENT_NOT_FOUND");
|
||||
assertThat(body.error().category()).isEqualTo("NOT_FOUND");
|
||||
assertThat(body.error().retryable()).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for the client-safe message leak: the handler must never surface {@link
|
||||
* StudioException#getMessage()} (diagnostic-only, may carry SQLState/upstream detail) — only
|
||||
* {@link StudioClientSafeMessages#forError(StudioError)}'s fixed, per-code text.
|
||||
*/
|
||||
@Test
|
||||
void neverLeaksTheRawExceptionMessageAndUsesTheClientSafeTextInstead() {
|
||||
String rawDiagnosticMessage = "pg constraint fk_document_project violated for id=42";
|
||||
|
||||
ResponseEntity<Envelope<Void>> response =
|
||||
handler.handleStudio(
|
||||
StudioException.of(StudioError.DOCUMENT_NOT_FOUND, rawDiagnosticMessage));
|
||||
|
||||
String message = response.getBody().error().message();
|
||||
assertThat(message).isNotEqualTo(rawDiagnosticMessage);
|
||||
assertThat(message)
|
||||
.isEqualTo(StudioClientSafeMessages.forError(StudioError.DOCUMENT_NOT_FOUND));
|
||||
}
|
||||
|
||||
@Test
|
||||
void carriesDetailsForConflicts() {
|
||||
ResponseEntity<Envelope<Void>> response =
|
||||
handler.handleStudio(
|
||||
StudioException.withDetails(
|
||||
StudioError.VERSION_CONFLICT,
|
||||
"충돌",
|
||||
Map.of("latestDocument", Map.of("version", 8))));
|
||||
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(409);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
assertThat(response.getBody().error().details()).isNotNull();
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog.studio.controller;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
|
||||
import dev.caskeleton.adapter.inbound.web.error.GlobalExceptionHandler;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.StudioExceptionHandler;
|
||||
import dev.caskeleton.application.techlog.studio.port.out.CatalogQueryPort;
|
||||
import dev.caskeleton.application.techlog.studio.service.ListCatalogUseCase;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
/**
|
||||
* final whole-branch review B4: {@code GET /api/v1/studio/catalog} without its required {@code
|
||||
* type} query parameter throws {@code MissingServletRequestParameterException}. Neither {@code
|
||||
* GlobalExceptionHandler} (which we cannot modify — template file) nor the old {@code
|
||||
* StudioExceptionHandler} handled it, so it fell through to the inherited {@code
|
||||
* ResponseEntityExceptionHandler} behaviour: a bare {@code ProblemDetail} body with content-type
|
||||
* {@code application/problem+json}. {@code EnvelopeBodyAdvice#beforeBodyWrite}'s {@code
|
||||
* MediaType.APPLICATION_JSON.includes(...)} guard then skips wrapping, so a bare RFC 7807 body
|
||||
* leaks past the envelope — exactly the wire shape ADR-006 says this backend does not use.
|
||||
*
|
||||
* <p>{@code ?type=BOGUS} and {@code ?limit=abc} throw {@code MethodArgumentTypeMismatchException}
|
||||
* instead — {@code GlobalExceptionHandler} does handle that one directly (so the body stays
|
||||
* enveloped), but with {@code OperationalError.BAD_PARAMETER}, a code outside the 23-code Studio
|
||||
* contract.
|
||||
*
|
||||
* <p>This test pins the fix: a Studio-scoped {@code StudioExceptionHandler} handler moves both
|
||||
* exceptions to the contract's {@code REQUEST_VALIDATION_FAILED} at 422 (the status the {@code
|
||||
* StudioError} enum and {@code docs/registries/error-codes.yaml} agree on), enveloped like every
|
||||
* other Studio failure.
|
||||
*
|
||||
* <p>Follows the {@code @WebMvcTest} + hand-built {@code TestBootstrap} slice pattern documented in
|
||||
* {@link StudioSessionEnvelopeTest} — this module's test source set has no {@code
|
||||
* CaSkeletonApplication} for {@code @WebMvcTest} to bootstrap from. Security is fully excluded
|
||||
* (like {@code NoResourceFoundErrorHandlingTest}) since none of these scenarios are
|
||||
* authentication/authorization-related. {@link ListCatalogUseCase} is real, not mocked (it is a
|
||||
* {@code final} class and this module's Mockito is not configured with the inline mock maker) —
|
||||
* built from hand-written fake ports, and its port never runs because request binding fails before
|
||||
* the controller method body is entered.
|
||||
*/
|
||||
@WebMvcTest(
|
||||
controllers = StudioCatalogController.class,
|
||||
excludeAutoConfiguration = SecurityAutoConfiguration.class)
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
@Import({
|
||||
StudioCatalogController.class,
|
||||
StudioExceptionHandler.class,
|
||||
GlobalExceptionHandler.class,
|
||||
EnvelopeBodyAdvice.class,
|
||||
StudioCatalogBindingErrorEnvelopeTest.TestBeans.class
|
||||
})
|
||||
class StudioCatalogBindingErrorEnvelopeTest {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void missingRequiredTypeParameterIsEnvelopedAsRequestValidationFailed() throws Exception {
|
||||
mvc.perform(get("/api/v1/studio/catalog"))
|
||||
.andExpect(status().is(422))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
.andExpect(jsonPath("$.error.code").value("REQUEST_VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.error.category").value("VALIDATION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownTypeEnumValueIsEnvelopedAsRequestValidationFailed() throws Exception {
|
||||
mvc.perform(get("/api/v1/studio/catalog").param("type", "BOGUS"))
|
||||
.andExpect(status().is(422))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
.andExpect(jsonPath("$.error.code").value("REQUEST_VALIDATION_FAILED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonNumericLimitIsEnvelopedAsRequestValidationFailed() throws Exception {
|
||||
mvc.perform(get("/api/v1/studio/catalog").param("type", "TOPIC").param("limit", "abc"))
|
||||
.andExpect(status().is(422))
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
.andExpect(jsonPath("$.error.code").value("REQUEST_VALIDATION_FAILED"));
|
||||
}
|
||||
|
||||
static class TestBeans {
|
||||
|
||||
@Bean
|
||||
ListCatalogUseCase listCatalogUseCase() {
|
||||
CatalogQueryPort neverInvoked =
|
||||
(type, query, cursor, limit) -> {
|
||||
throw new AssertionError(
|
||||
"ListCatalogUseCase must not run when request binding already failed");
|
||||
};
|
||||
return new ListCatalogUseCase(neverInvoked, new PassthroughTransactionPort());
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the action synchronously with no real transactional semantics — a slice test fake. */
|
||||
private static final class PassthroughTransactionPort implements TransactionPort {
|
||||
@Override
|
||||
public <T> T inWrite(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inRootWrite(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inRead(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inNew(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
|
||||
static class TestBootstrap {}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog.studio.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
|
||||
import dev.caskeleton.adapter.inbound.web.settings.SecuritySettings;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.studio.api.model.StudioSession;
|
||||
import dev.caskeleton.application.techlog.error.StudioError;
|
||||
import dev.caskeleton.application.techlog.error.StudioException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
|
||||
class StudioSessionControllerTest {
|
||||
|
||||
private final StudioSessionController controller =
|
||||
new StudioSessionController(securitySettingsWithCsrfHeaderName("X-CSRF-TOKEN"));
|
||||
|
||||
@Test
|
||||
void reportsAuthenticatedPrincipalAndCsrfToken() {
|
||||
StudioSession session =
|
||||
controller.getStudioSession(
|
||||
new AuthenticatedPrincipal("sub-1", "donghyeon@example.com", Set.of("STUDIO_EDITOR")),
|
||||
new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token-value"));
|
||||
|
||||
assertThat(session.getAuthenticated()).isTrue();
|
||||
assertThat(session.getDisplayName()).isEqualTo("donghyeon@example.com");
|
||||
assertThat(session.getRoles()).containsExactly("STUDIO_EDITOR");
|
||||
assertThat(session.getCsrfToken()).isEqualTo("token-value");
|
||||
assertThat(session.getCsrfHeaderName()).isEqualTo("X-CSRF-TOKEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToIdpUserIdWhenEmailIsAbsent() {
|
||||
StudioSession session =
|
||||
controller.getStudioSession(
|
||||
new AuthenticatedPrincipal("sub-1", null, Set.of()),
|
||||
new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "t"));
|
||||
|
||||
assertThat(session.getDisplayName()).isEqualTo("sub-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAMisconfiguredCsrfHeaderNameAtConstructionRatherThanServingTheWrongHeader() {
|
||||
assertThatThrownBy(() -> new StudioSessionController(securitySettingsWithCsrfHeaderName(null)))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("X-CSRF-TOKEN")
|
||||
.hasMessageContaining("X-XSRF-TOKEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsStudioUnavailableRatherThanNpeWhenCsrfTokenIsNull() {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
controller.getStudioSession(
|
||||
new AuthenticatedPrincipal(
|
||||
"sub-1", "donghyeon@example.com", Set.of("STUDIO_EDITOR")),
|
||||
null))
|
||||
.isInstanceOf(StudioException.class)
|
||||
.extracting(ex -> ((StudioException) ex).studioError())
|
||||
.isEqualTo(StudioError.STUDIO_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsStudioUnavailableRatherThanTruncatingRolesBeyondTheContractMax() {
|
||||
Set<String> tooManyRoles =
|
||||
IntStream.range(0, 21).mapToObj(i -> "ROLE_" + i).collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
controller.getStudioSession(
|
||||
new AuthenticatedPrincipal("sub-1", "donghyeon@example.com", tooManyRoles),
|
||||
new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "t")))
|
||||
.isInstanceOf(StudioException.class)
|
||||
.extracting(ex -> ((StudioException) ex).studioError())
|
||||
.isEqualTo(StudioError.STUDIO_UNAVAILABLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsStudioUnavailableRatherThanAnEmptyDisplayNameWhenEmailAndIdpUserIdAreBothBlank() {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
controller.getStudioSession(
|
||||
new AuthenticatedPrincipal(" ", " ", Set.of()),
|
||||
new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "t")))
|
||||
.isInstanceOf(StudioException.class)
|
||||
.extracting(ex -> ((StudioException) ex).studioError())
|
||||
.isEqualTo(StudioError.STUDIO_UNAVAILABLE);
|
||||
}
|
||||
|
||||
private static SecuritySettings securitySettingsWithCsrfHeaderName(String csrfHeaderName) {
|
||||
SecuritySettings.SessionCookieSettings session =
|
||||
new SecuritySettings.SessionCookieSettings(
|
||||
null, null, null, null, null, null, csrfHeaderName);
|
||||
return new SecuritySettings(
|
||||
SecuritySettings.AuthenticationMode.JWT,
|
||||
"https://issuer.example",
|
||||
null,
|
||||
List.of(),
|
||||
session);
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog.studio.controller;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
|
||||
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
|
||||
import dev.caskeleton.adapter.inbound.web.error.GlobalExceptionHandler;
|
||||
import dev.caskeleton.adapter.inbound.web.observability.MdcKeys;
|
||||
import dev.caskeleton.adapter.inbound.web.settings.SecuritySettings;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.StudioExceptionHandler;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
/**
|
||||
* 프로덕션 JWT auth-mode를 재현한다: {@code SecurityConfig.filterChain}의 JWT 분기는 {@code csrf(csrf ->
|
||||
* csrf.disable())}로 {@code CsrfConfigurer} 자체를 제거한다 — {@code CsrfFilter}가 돌지 않고 {@code CsrfToken}
|
||||
* request attribute를 아무도 채우지 않는다.
|
||||
*
|
||||
* <p>그런데 {@code CsrfTokenArgumentResolver}(Spring Security 7.0.0, {@code
|
||||
* WebMvcSecurityConfiguration.addArgumentResolvers}가 {@code @EnableWebSecurity}만 있으면 무조건 등록한다)의
|
||||
* {@code resolveArgument}는 request attribute를 캐스팅만 할 뿐 null 체크가 없다 — attribute가 없으면 그냥 {@code
|
||||
* null}을 돌려준다. 그래서 JWT 모드에서는 컨트롤러의 {@code CsrfToken csrfToken} 파라미터가 **항상 null**이다.
|
||||
*
|
||||
* <p>이 테스트는 그 사실을 슬라이스 필터체인으로 직접 재현한다 — {@link StudioSessionEnvelopeTest}의 {@code
|
||||
* SecurityTestConfig}(CSRF 켜짐, Spring 기본값)와 정확히 반대다. 일부러 {@code
|
||||
* SecurityMockMvcRequestPostProcessors.csrf()}를 쓰지 않는다 — 그 포스트 프로세서는 실제 필터체인 여부와 무관하게 request
|
||||
* attribute를 직접 채워버려서, 쓰면 이 재현이 무력화된다(CSRF가 꺼져 있어도 토큰이 채워진 것처럼 보이게 된다).
|
||||
*/
|
||||
@WebMvcTest(controllers = StudioSessionController.class)
|
||||
@Import({
|
||||
StudioSessionController.class,
|
||||
EnvelopeBodyAdvice.class,
|
||||
StudioExceptionHandler.class,
|
||||
GlobalExceptionHandler.class,
|
||||
StudioSessionCsrfDisabledTest.SecurityTestConfig.class
|
||||
})
|
||||
class StudioSessionCsrfDisabledTest {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@AfterEach
|
||||
void clearMdc() {
|
||||
MDC.remove(MdcKeys.TRACE_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsStudioUnavailableRatherThanCrashingWhenCsrfIsDisabled() throws Exception {
|
||||
MDC.put(MdcKeys.TRACE_ID, "test-trace-id");
|
||||
|
||||
mvc.perform(
|
||||
get("/api/v1/studio/session")
|
||||
.with(
|
||||
authentication(
|
||||
UsernamePasswordAuthenticationToken.authenticated(
|
||||
new AuthenticatedPrincipal(
|
||||
"sub-1", "donghyeon@example.com", Set.of("STUDIO_EDITOR")),
|
||||
null,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_STUDIO_EDITOR"))))))
|
||||
.andExpect(status().isServiceUnavailable())
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
.andExpect(jsonPath("$.error.code").value("STUDIO_UNAVAILABLE"))
|
||||
.andExpect(jsonPath("$.error.retryable").value(true))
|
||||
.andExpect(jsonPath("$.meta.traceId").isNotEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code SecurityConfig.filterChain}의 JWT 분기와 같은 모양 — {@code csrf().disable()} + {@code
|
||||
* anyRequest().authenticated()}뿐. 실제 앱의 {@code SecurityConfig} 전체(CORS, JWT-vs-redis-session 분기,
|
||||
* entry point 등)는 가져오지 않는다.
|
||||
*/
|
||||
@EnableWebSecurity
|
||||
static class SecurityTestConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain csrfDisabledFilterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecuritySettings securitySettings() {
|
||||
SecuritySettings.SessionCookieSettings session =
|
||||
new SecuritySettings.SessionCookieSettings(
|
||||
null, null, null, null, null, null, "X-CSRF-TOKEN");
|
||||
return new SecuritySettings(
|
||||
SecuritySettings.AuthenticationMode.JWT,
|
||||
"https://issuer.example",
|
||||
null,
|
||||
List.of(),
|
||||
session);
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class TestBootstrap {}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package dev.caskeleton.adapter.inbound.web.techlog.studio.controller;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
|
||||
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
|
||||
import dev.caskeleton.adapter.inbound.web.observability.MdcKeys;
|
||||
import dev.caskeleton.adapter.inbound.web.settings.SecuritySettings;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
/**
|
||||
* 응답이 봉투로 정확히 한 번 감싸이는지 고정한다. 두 번 감싸이면 프론트가 조용히 깨진다. CSRF가 **켜진** 필터체인(Spring 기본값)에서의 해피 패스만 다룬다 —
|
||||
* CSRF가 **꺼진**(프로덕션 JWT 모드와 같은 모양) 경로는 {@link StudioSessionCsrfDisabledTest}가 별도로 고정한다. 두 필터체인을 한
|
||||
* 테스트 클래스에 같이 둘 수 없다({@code @WebMvcTest}는 클래스당 Spring 컨텍스트 하나뿐이라 {@code SecurityFilterChain} 빈도
|
||||
* 하나뿐이다).
|
||||
*
|
||||
* <p>이 모듈(adapter:inbound:web)의 테스트 소스셋에는 {@code CaSkeletonApplication}이 없다 — 그건 app-bootstrap 모듈
|
||||
* 소유다. 그래서 {@code @WebMvcTest}가 컨텍스트를 부트스트랩할 {@code @SpringBootConfiguration}을 못 찾는다. {@link
|
||||
* dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdviceTest}와 {@link
|
||||
* dev.caskeleton.adapter.inbound.web.error.NoResourceFoundErrorHandlingTest}가 쓰는 것과 같은 방식으로 — 테스트
|
||||
* 전용 nested {@code TestBootstrap}을 두고 필요한 빈을 명시적으로 {@code @Import}한다.
|
||||
*
|
||||
* <p>보안: 이 슬라이스는 실제 {@code SecurityConfig}를 끌어오지 않는다(그건 {@code CorsSettings}, {@code
|
||||
* JwtToAuthenticatedPrincipalConverter} 등 앱 전체 배선이 필요해서 슬라이스 테스트에 과하다). 이 저장소에 이 조합 (WebMvcTest +
|
||||
* 실제 보안 필터 체인 + AuthenticatedPrincipal)의 선례가 없다 — {@code SecurityModeWebContractTest}는 {@code
|
||||
* WebApplicationContextRunner}+standalone MockMvc를 쓰지 slice가 아니고, {@code
|
||||
* EnvelopeBodyAdviceTest}/{@code NoResourceFoundErrorHandlingTest}는 반대로 Security를 통째로 배제한다 ({@code
|
||||
* excludeAutoConfiguration = SecurityAutoConfiguration.class} + {@code addFilters = false}). 그래서
|
||||
* 브리프가 제시한 fallback을 따른다 — {@code @WithMockUser} 대신 {@code
|
||||
* SecurityMockMvcRequestPostProcessors.csrf()}와 커스텀 {@code authentication(...)}을 쓴다.
|
||||
* {@code @WithMockUser}는 principal을 {@code org.springframework.security.core.userdetails.User}로
|
||||
* 채우는데 컨트롤러가 기대하는 타입은 {@code AuthenticatedPrincipal}이라 타입 불일치로 {@code @AuthenticationPrincipal}이
|
||||
* null을 주입하고 컨트롤러가 NPE를 던진다 — 실측으로 확인했다(task-8 report 참조).
|
||||
*
|
||||
* <p>{@code @WebMvcTest}는 표준 {@code @EnableAutoConfiguration}을
|
||||
* {@code @OverrideAutoConfiguration(enabled = false)}로 끄고 test-slice 전용의 제한된 auto-configuration 목록만
|
||||
* 적용한다 — 그 목록은 Boot의 {@code ServletWebSecurityAutoConfiguration}(기본 {@code SecurityFilterChain} +
|
||||
* {@code @EnableWebSecurity})을 포함하지 않는다. 그래서 {@code TestBootstrap}이
|
||||
* {@code @EnableAutoConfiguration}을 달고 있어도 {@code CsrfToken}/{@code @AuthenticationPrincipal} 인자
|
||||
* 리졸버가 등록되지 않는다 — 실제로 시도했더니 {@code CsrfToken}이 인자 리졸버 없이 {@code @ModelAttribute} 데이터바인딩 경로로 떨어져 "No
|
||||
* primary or single unique constructor found for interface CsrfToken" {@code
|
||||
* IllegalStateException}으로 500이 났다. 그래서 {@code @EnableWebSecurity}를 이 테스트가 직접 명시적으로 붙인다({@code
|
||||
* SecurityTestConfig}) — 그래야 그 인자 리졸버들이 등록된다.
|
||||
*
|
||||
* <p>{@code meta.traceId}는 프로덕션에서 {@code RequestLoggingFilter}가 MDC에 채운다. 그 필터는 {@code
|
||||
* UserPrincipalPseudonymizerPort} 빈이 필요하고 자체 테스트({@code RequestLoggingFilterTest})가 이미 있으므로 여기서는
|
||||
* 재현하지 않는다 — MockMvc가 테스트 스레드에서 동기 실행되는 점을 이용해 MDC를 직접 채운다.
|
||||
*/
|
||||
@WebMvcTest(controllers = StudioSessionController.class)
|
||||
@Import({
|
||||
StudioSessionController.class,
|
||||
EnvelopeBodyAdvice.class,
|
||||
StudioSessionEnvelopeTest.SecurityTestConfig.class
|
||||
})
|
||||
class StudioSessionEnvelopeTest {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@AfterEach
|
||||
void clearMdc() {
|
||||
MDC.remove(MdcKeys.TRACE_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrapsTheSessionPayloadExactlyOnce() throws Exception {
|
||||
MDC.put(MdcKeys.TRACE_ID, "test-trace-id");
|
||||
|
||||
mvc.perform(
|
||||
get("/api/v1/studio/session")
|
||||
.with(csrf())
|
||||
.with(
|
||||
authentication(
|
||||
UsernamePasswordAuthenticationToken.authenticated(
|
||||
new AuthenticatedPrincipal(
|
||||
"sub-1", "donghyeon@example.com", Set.of("STUDIO_EDITOR")),
|
||||
null,
|
||||
Set.of(new SimpleGrantedAuthority("ROLE_STUDIO_EDITOR"))))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.csrfHeaderName").value("X-CSRF-TOKEN"))
|
||||
.andExpect(jsonPath("$.data.data").doesNotExist())
|
||||
.andExpect(jsonPath("$.meta.traceId").isNotEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code CsrfToken}/{@code @AuthenticationPrincipal} 인자 리졸버를 등록하는 최소 보안 구성. 실제 앱의 {@code
|
||||
* SecurityConfig}(CORS, JWT/redis-session 분기, entry point 등)는 가져오지 않고 이 슬라이스가 필요로 하는 것 — 인증된 요청만
|
||||
* 통과, CSRF는 Spring 기본값(켜짐) — 만 남긴다. {@link StudioSessionCsrfDisabledTest}의 {@code
|
||||
* SecurityTestConfig}가 정확히 반대(CSRF 꺼짐)를 재현한다.
|
||||
*/
|
||||
@EnableWebSecurity
|
||||
static class SecurityTestConfig {
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 컨트롤러가 이제 {@code csrfHeaderName}을 이 설정에서 읽는다(하드코드하지 않음) — 계약값 {@code X-CSRF-TOKEN}과 일치해야 생성자가
|
||||
* 통과한다.
|
||||
*/
|
||||
@Bean
|
||||
SecuritySettings securitySettings() {
|
||||
SecuritySettings.SessionCookieSettings session =
|
||||
new SecuritySettings.SessionCookieSettings(
|
||||
null, null, null, null, null, null, "X-CSRF-TOKEN");
|
||||
return new SecuritySettings(
|
||||
SecuritySettings.AuthenticationMode.JWT,
|
||||
"https://issuer.example",
|
||||
null,
|
||||
List.of(),
|
||||
session);
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
static class TestBootstrap {}
|
||||
}
|
||||
@@ -107,6 +107,15 @@ def postgresqlFileserverMetadataIntegrationTest = registerPostgreSqlReadinessTes
|
||||
def postgresqlFileserverReclamationIntegrationTest = registerPostgreSqlReadinessTest(
|
||||
'postgresqlFileserverReclamationIntegrationTest',
|
||||
'dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverReclamationIntegrationTest')
|
||||
def postgresqlTechLogSchemaMigrationTest = registerPostgreSqlReadinessTest(
|
||||
'postgresqlTechLogSchemaMigrationTest',
|
||||
'dev.caskeleton.adapter.outbound.persistence.techlog.TechLogSchemaMigrationTest')
|
||||
// Task 9 (listStudioCatalog): JdbcCatalogQueryAdapterTest has no @SpringBootConfiguration to hang a
|
||||
// @SpringBootTest off in this module (same reason as postgresqlTechLogSchemaMigrationTest above), so it
|
||||
// needs its own opt-in Testcontainers task rather than reusing an existing one.
|
||||
def postgresqlTechLogCatalogQueryIntegrationTest = registerPostgreSqlReadinessTest(
|
||||
'postgresqlTechLogCatalogQueryIntegrationTest',
|
||||
'dev.caskeleton.adapter.outbound.persistence.techlog.query.JdbcCatalogQueryAdapterTest')
|
||||
|
||||
def verifyJpaSqlConstructionSafety = tasks.register('verifyJpaSqlConstructionSafety') {
|
||||
group = 'verification'
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.techlog.query;
|
||||
|
||||
import dev.caskeleton.application.techlog.studio.port.out.CatalogQueryPort;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryType;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryView;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogPageView;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* catalog는 도메인 repository를 거치지 않고 전용 union query를 쓴다 (설계 08장 §4).
|
||||
*
|
||||
* <p>RELATION / EVIDENCE는 슬라이스 2·5에서 채운다. 그때까지 빈 페이지를 반환하며 이는 계약상 유효한 응답이다.
|
||||
*/
|
||||
@Repository
|
||||
public class JdbcCatalogQueryAdapter implements CatalogQueryPort {
|
||||
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
public JdbcCatalogQueryAdapter(JdbcClient jdbcClient) {
|
||||
this.jdbcClient = jdbcClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CatalogPageView search(CatalogEntryType type, String query, String cursor, int limit) {
|
||||
String pattern =
|
||||
(query == null || query.isBlank()) ? "%" : "%" + query.toLowerCase(Locale.ROOT) + "%";
|
||||
List<CatalogEntryView> items =
|
||||
switch (type) {
|
||||
case TOPIC -> searchTopics(pattern, limit);
|
||||
case PROJECT -> searchProjects(pattern, limit);
|
||||
case RELATION, EVIDENCE -> List.of();
|
||||
};
|
||||
return new CatalogPageView(items, null);
|
||||
}
|
||||
|
||||
private List<CatalogEntryView> searchTopics(String pattern, int limit) {
|
||||
return jdbcClient
|
||||
.sql(
|
||||
"SELECT id, name, updated_at FROM topic "
|
||||
+ "WHERE status = 'ACTIVE' AND lower(name) LIKE :pattern "
|
||||
+ "ORDER BY name LIMIT :limit")
|
||||
.param("pattern", pattern)
|
||||
.param("limit", limit)
|
||||
.query(
|
||||
(rs, rowNum) ->
|
||||
new CatalogEntryView(
|
||||
UUID.fromString(rs.getString("id")),
|
||||
CatalogEntryType.TOPIC,
|
||||
rs.getString("name"),
|
||||
null,
|
||||
null,
|
||||
"topic:" + rs.getTimestamp("updated_at").toInstant()))
|
||||
.list();
|
||||
}
|
||||
|
||||
private List<CatalogEntryView> searchProjects(String pattern, int limit) {
|
||||
return jdbcClient
|
||||
.sql(
|
||||
"SELECT id, name, updated_at FROM project "
|
||||
+ "WHERE lower(name) LIKE :pattern ORDER BY name LIMIT :limit")
|
||||
.param("pattern", pattern)
|
||||
.param("limit", limit)
|
||||
.query(
|
||||
(rs, rowNum) ->
|
||||
new CatalogEntryView(
|
||||
UUID.fromString(rs.getString("id")),
|
||||
CatalogEntryType.PROJECT,
|
||||
rs.getString("name"),
|
||||
"PROJECT",
|
||||
null,
|
||||
"project:" + rs.getTimestamp("updated_at").toInstant()))
|
||||
.list();
|
||||
}
|
||||
}
|
||||
+779
@@ -0,0 +1,779 @@
|
||||
-- Tech Log 코어 스키마.
|
||||
-- 원본: tech-log-design-package/database/V1__init.sql
|
||||
-- (branch feature/response-envelope-adr-006, HEAD b20d7a2 — 이 파일은 그 이후 바뀌지 않았다)
|
||||
--
|
||||
-- 원본 DDL과의 차이:
|
||||
-- 1. `CREATE SCHEMA IF NOT EXISTS tech_log;` / `SET search_path TO tech_log, public;` 제거.
|
||||
-- 이 저장소의 기존 마이그레이션(V1/V3/V4/V5/V6)과 JPA 설정(@EntityScan, @EnableJpaRepositories,
|
||||
-- PostgreSqlPersistenceConfig의 FlywayConfigurationCustomizer)은 모두 기본 public 스키마를
|
||||
-- 전제한다. tech_log 전용 스키마로 옮기면 Task 9 이후의 JPA 엔티티가 이 테이블들을 찾지
|
||||
-- 못한다. 그래서 테이블은 이 저장소의 다른 모든 테이블과 마찬가지로 public 스키마에 만든다.
|
||||
-- 2. `studio_idempotency` 테이블과 전용 인덱스(`idx_studio_idempotency_expiry`)를 제외한다.
|
||||
-- 기존 `idempotency_record`를 재사용한다 (spec D5).
|
||||
-- 3. `release` / `site_config` / `profile_page` / `home_focus_config` /
|
||||
-- `topic_featured_document` / `project_topic` 테이블과 전용 인덱스(`uq_topic_start_here`)를
|
||||
-- 제외한다. 이번 범위 밖이다 (spec §2.2). site_config/profile_page/home_focus_config를
|
||||
-- 시딩하던 마지막 INSERT 구문도 대상 테이블이 없으므로 함께 제외했다.
|
||||
-- 4. 그 밖의 테이블·컬럼·CHECK·UNIQUE·인덱스·주석·순서는 원본을 그대로 보존한다. 순환 FK
|
||||
-- `publication.latest_event_id` -> `publication_event.publication_id`의
|
||||
-- `DEFERRABLE INITIALLY DEFERRED`도 그대로 유지한다 — 즉시 검사로 바꾸면 첫 게시가
|
||||
-- 구조적으로 불가능해진다.
|
||||
|
||||
-- Tech Log initial PostgreSQL schema
|
||||
-- Target: PostgreSQL 16+
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Taxonomy and assets
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE topic (
|
||||
id uuid PRIMARY KEY,
|
||||
name varchar(80) NOT NULL,
|
||||
normalized_name varchar(80) NOT NULL,
|
||||
slug varchar(100) NOT NULL,
|
||||
description varchar(600),
|
||||
scope text,
|
||||
status varchar(20) NOT NULL DEFAULT 'ACTIVE'
|
||||
CHECK (status IN ('ACTIVE', 'ARCHIVED')),
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_topic_normalized_name UNIQUE (normalized_name),
|
||||
CONSTRAINT uq_topic_slug UNIQUE (slug)
|
||||
);
|
||||
|
||||
CREATE TABLE tag (
|
||||
id uuid PRIMARY KEY,
|
||||
name varchar(40) NOT NULL,
|
||||
normalized_name varchar(40) NOT NULL,
|
||||
slug varchar(60) NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_tag_normalized_name UNIQUE (normalized_name),
|
||||
CONSTRAINT uq_tag_slug UNIQUE (slug)
|
||||
);
|
||||
|
||||
-- asset_key는 Public content가 참조하는 안정적인 key다.
|
||||
-- object_key(스토리지 경로)와 분리되며 immutable이다.
|
||||
--
|
||||
-- asset_key -> Asset lookup -> current approved delivery path
|
||||
--
|
||||
-- 콘텐츠 원문에 object storage URL을 직접 영속하지 않는다.
|
||||
-- 공개 이력이 있는 asset_key의 재사용 금지는 application rule로 강제한다.
|
||||
-- (DB는 현재 행의 유일성만 보장한다.)
|
||||
CREATE TABLE asset (
|
||||
id uuid PRIMARY KEY,
|
||||
asset_key varchar(200) NOT NULL,
|
||||
asset_kind varchar(20) NOT NULL
|
||||
CHECK (asset_kind IN ('IMAGE', 'DIAGRAM', 'ATTACHMENT')),
|
||||
management_status varchar(20) NOT NULL
|
||||
CHECK (management_status IN ('READY', 'ARCHIVED', 'REJECTED', 'QUARANTINED')),
|
||||
object_key varchar(500) NOT NULL,
|
||||
original_name varchar(255) NOT NULL,
|
||||
display_name varchar(255),
|
||||
content_type varchar(150) NOT NULL,
|
||||
size_bytes bigint NOT NULL CHECK (size_bytes >= 0),
|
||||
width integer CHECK (width IS NULL OR width > 0),
|
||||
height integer CHECK (height IS NULL OR height > 0),
|
||||
checksum_sha256 char(64) NOT NULL,
|
||||
alt_text varchar(300),
|
||||
decorative boolean NOT NULL DEFAULT false,
|
||||
first_published_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_asset_key UNIQUE (asset_key),
|
||||
CONSTRAINT uq_asset_object_key UNIQUE (object_key)
|
||||
);
|
||||
|
||||
-- alt/decorative는 DB CHECK로 강제하지 않는다.
|
||||
--
|
||||
-- 업로드 시점에는 alt를 아직 정하지 않을 수 있어야 하고(Asset Picker에서 이후 수정),
|
||||
-- 최종 판단은 syntax parser가 아니라 Publication Validation이 한다.
|
||||
--
|
||||
-- Asset decorative=false + 사용 위치 alt 비어 있음 -> PublishValidationFailed
|
||||
-- Asset decorative=true -> alt="" 허용
|
||||
--
|
||||
-- 즉 판단 대상은 asset.alt_text 자체가 아니라 "해당 사용 위치의 alt"다.
|
||||
-- 같은 Asset이 문서마다 다른 alt로 쓰일 수 있으므로 행 단위 CHECK로 표현할 수 없다.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Knowledge documents
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE document (
|
||||
id uuid PRIMARY KEY,
|
||||
document_type varchar(20) NOT NULL
|
||||
CHECK (document_type IN ('CASE', 'REFERENCE')),
|
||||
slug varchar(180),
|
||||
title varchar(180) NOT NULL,
|
||||
body_markdown text NOT NULL DEFAULT '',
|
||||
content_format varchar(20) NOT NULL DEFAULT 'MARKDOWN'
|
||||
CHECK (content_format IN ('MARKDOWN')),
|
||||
content_format_version smallint NOT NULL DEFAULT 1
|
||||
CHECK (content_format_version > 0),
|
||||
workflow_status varchar(20) NOT NULL DEFAULT 'DRAFT'
|
||||
CHECK (workflow_status IN ('DRAFT', 'IN_REVIEW', 'PUBLISHED', 'ARCHIVED')),
|
||||
target_visibility varchar(20) NOT NULL DEFAULT 'PRIVATE'
|
||||
CHECK (target_visibility IN ('PRIVATE', 'UNLISTED', 'PUBLIC')),
|
||||
primary_topic_id uuid REFERENCES topic(id),
|
||||
cover_asset_id uuid REFERENCES asset(id),
|
||||
last_verified_at timestamptz,
|
||||
first_published_at timestamptz,
|
||||
last_published_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_document_id_type UNIQUE (id, document_type),
|
||||
CONSTRAINT uq_document_type_slug UNIQUE (document_type, slug),
|
||||
CONSTRAINT ck_document_slug_non_blank CHECK (slug IS NULL OR length(trim(slug)) > 0),
|
||||
CONSTRAINT ck_document_publish_time_order CHECK (
|
||||
first_published_at IS NULL
|
||||
OR last_published_at IS NULL
|
||||
OR first_published_at <= last_published_at
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE case_detail (
|
||||
document_id uuid PRIMARY KEY,
|
||||
document_type varchar(20) NOT NULL DEFAULT 'CASE'
|
||||
CHECK (document_type = 'CASE'),
|
||||
problem_summary varchar(600) NOT NULL DEFAULT '',
|
||||
conclusion_summary varchar(600) NOT NULL DEFAULT '',
|
||||
environment_items jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
CHECK (jsonb_typeof(environment_items) = 'array'),
|
||||
CONSTRAINT fk_case_detail_document
|
||||
FOREIGN KEY (document_id, document_type)
|
||||
REFERENCES document(id, document_type)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE reference_detail (
|
||||
document_id uuid PRIMARY KEY,
|
||||
document_type varchar(20) NOT NULL DEFAULT 'REFERENCE'
|
||||
CHECK (document_type = 'REFERENCE'),
|
||||
scope_summary varchar(600) NOT NULL DEFAULT '',
|
||||
applies_to jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
CHECK (jsonb_typeof(applies_to) = 'array'),
|
||||
excluded_scope jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
CHECK (jsonb_typeof(excluded_scope) = 'array'),
|
||||
freshness_status varchar(20) NOT NULL DEFAULT 'CURRENT'
|
||||
CHECK (freshness_status IN ('CURRENT', 'REVIEW_DUE', 'HISTORICAL')),
|
||||
CONSTRAINT fk_reference_detail_document
|
||||
FOREIGN KEY (document_id, document_type)
|
||||
REFERENCES document(id, document_type)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE document_tag (
|
||||
document_id uuid NOT NULL REFERENCES document(id) ON DELETE CASCADE,
|
||||
tag_id uuid NOT NULL REFERENCES tag(id),
|
||||
display_order integer NOT NULL CHECK (display_order >= 0),
|
||||
PRIMARY KEY (document_id, tag_id),
|
||||
CONSTRAINT uq_document_tag_order UNIQUE (document_id, display_order)
|
||||
);
|
||||
|
||||
CREATE TABLE document_relation (
|
||||
source_document_id uuid NOT NULL REFERENCES document(id) ON DELETE CASCADE,
|
||||
target_document_id uuid NOT NULL REFERENCES document(id),
|
||||
relation_type varchar(30) NOT NULL
|
||||
CHECK (relation_type IN ('RELATED', 'DERIVED_FROM', 'SUPERSEDES')),
|
||||
display_order integer NOT NULL DEFAULT 0 CHECK (display_order >= 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (source_document_id, target_document_id, relation_type),
|
||||
CONSTRAINT ck_document_relation_not_self CHECK (source_document_id <> target_document_id)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Open questions
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE open_question (
|
||||
id uuid PRIMARY KEY,
|
||||
slug varchar(180),
|
||||
question varchar(300) NOT NULL,
|
||||
summary varchar(600),
|
||||
context_markdown text NOT NULL DEFAULT '',
|
||||
importance_markdown text NOT NULL DEFAULT '',
|
||||
next_verification text,
|
||||
question_status varchar(20) NOT NULL DEFAULT 'OPEN'
|
||||
CHECK (question_status IN ('OPEN', 'INVESTIGATING', 'PAUSED', 'RESOLVED', 'ARCHIVED')),
|
||||
target_visibility varchar(20) NOT NULL DEFAULT 'PRIVATE'
|
||||
CHECK (target_visibility IN ('PRIVATE', 'UNLISTED', 'PUBLIC')),
|
||||
primary_topic_id uuid REFERENCES topic(id),
|
||||
resolution_type varchar(30)
|
||||
CHECK (resolution_type IS NULL OR resolution_type IN (
|
||||
'DECISION_MADE',
|
||||
'ASSUMPTION_REJECTED',
|
||||
'QUESTION_REFRAMED',
|
||||
'NO_LONGER_RELEVANT'
|
||||
)),
|
||||
resolution_summary text,
|
||||
opened_at timestamptz NOT NULL DEFAULT now(),
|
||||
resolved_at timestamptz,
|
||||
first_published_at timestamptz,
|
||||
last_published_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_open_question_slug UNIQUE (slug),
|
||||
CONSTRAINT ck_question_resolution_consistency CHECK (
|
||||
(question_status = 'RESOLVED'
|
||||
AND resolution_type IS NOT NULL
|
||||
AND resolution_summary IS NOT NULL
|
||||
AND resolved_at IS NOT NULL)
|
||||
OR
|
||||
(question_status <> 'RESOLVED')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE question_point (
|
||||
id uuid PRIMARY KEY,
|
||||
question_id uuid NOT NULL REFERENCES open_question(id) ON DELETE CASCADE,
|
||||
point_kind varchar(20) NOT NULL
|
||||
CHECK (point_kind IN ('FACT', 'ASSUMPTION', 'UNKNOWN', 'CONSTRAINT')),
|
||||
content text NOT NULL CHECK (length(trim(content)) > 0),
|
||||
display_order integer NOT NULL CHECK (display_order >= 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT uq_question_point_order UNIQUE (question_id, point_kind, display_order)
|
||||
);
|
||||
|
||||
CREATE TABLE question_update (
|
||||
id uuid PRIMARY KEY,
|
||||
question_id uuid NOT NULL REFERENCES open_question(id) ON DELETE CASCADE,
|
||||
update_type varchar(30) NOT NULL
|
||||
CHECK (update_type IN (
|
||||
'OBSERVATION',
|
||||
'EVIDENCE',
|
||||
'SCOPE_CHANGE',
|
||||
'BLOCKER',
|
||||
'NEXT_STEP',
|
||||
'RESOLUTION',
|
||||
'RESOLUTION_REOPENED'
|
||||
)),
|
||||
title varchar(180) NOT NULL,
|
||||
body_markdown text NOT NULL DEFAULT '',
|
||||
update_visibility varchar(20) NOT NULL DEFAULT 'PRIVATE'
|
||||
CHECK (update_visibility IN ('PRIVATE', 'PUBLIC')),
|
||||
sequence_no integer NOT NULL CHECK (sequence_no > 0),
|
||||
occurred_at timestamptz NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_question_update_sequence UNIQUE (question_id, sequence_no)
|
||||
);
|
||||
|
||||
CREATE TABLE question_tag (
|
||||
question_id uuid NOT NULL REFERENCES open_question(id) ON DELETE CASCADE,
|
||||
tag_id uuid NOT NULL REFERENCES tag(id),
|
||||
display_order integer NOT NULL CHECK (display_order >= 0),
|
||||
PRIMARY KEY (question_id, tag_id),
|
||||
CONSTRAINT uq_question_tag_order UNIQUE (question_id, display_order)
|
||||
);
|
||||
|
||||
CREATE TABLE question_document_link (
|
||||
question_id uuid NOT NULL REFERENCES open_question(id) ON DELETE CASCADE,
|
||||
document_id uuid NOT NULL REFERENCES document(id),
|
||||
relation_type varchar(30) NOT NULL
|
||||
CHECK (relation_type IN ('RESULT_CASE', 'DERIVED_REFERENCE', 'RELATED')),
|
||||
display_order integer NOT NULL DEFAULT 0 CHECK (display_order >= 0),
|
||||
PRIMARY KEY (question_id, document_id, relation_type)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_question_result_case
|
||||
ON question_document_link(question_id)
|
||||
WHERE relation_type = 'RESULT_CASE';
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Projects, decisions, activities
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE project (
|
||||
id uuid PRIMARY KEY,
|
||||
slug varchar(180),
|
||||
name varchar(180) NOT NULL,
|
||||
one_line_purpose varchar(600) NOT NULL DEFAULT '',
|
||||
purpose_markdown text NOT NULL DEFAULT '',
|
||||
boundary_markdown text NOT NULL DEFAULT '',
|
||||
system_overview_markdown text NOT NULL DEFAULT '',
|
||||
phase varchar(30) NOT NULL DEFAULT 'RESEARCH'
|
||||
CHECK (phase IN (
|
||||
'RESEARCH',
|
||||
'DESIGN',
|
||||
'IMPLEMENTATION',
|
||||
'VERIFICATION',
|
||||
'MAINTENANCE',
|
||||
'PAUSED',
|
||||
'COMPLETED'
|
||||
)),
|
||||
current_objective text,
|
||||
next_step text,
|
||||
technology_labels jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
CHECK (jsonb_typeof(technology_labels) = 'array'),
|
||||
workflow_status varchar(20) NOT NULL DEFAULT 'DRAFT'
|
||||
CHECK (workflow_status IN ('DRAFT', 'PUBLISHED', 'ARCHIVED')),
|
||||
target_visibility varchar(20) NOT NULL DEFAULT 'PRIVATE'
|
||||
CHECK (target_visibility IN ('PRIVATE', 'UNLISTED', 'PUBLIC')),
|
||||
featured_order integer,
|
||||
first_published_at timestamptz,
|
||||
last_published_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_project_slug UNIQUE (slug),
|
||||
CONSTRAINT ck_project_featured_order CHECK (featured_order IS NULL OR featured_order >= 0)
|
||||
);
|
||||
|
||||
CREATE TABLE project_document_link (
|
||||
project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
||||
document_id uuid NOT NULL REFERENCES document(id),
|
||||
relation_type varchar(20) NOT NULL
|
||||
CHECK (relation_type IN ('PRIMARY', 'RELATED')),
|
||||
featured_order integer,
|
||||
PRIMARY KEY (project_id, document_id),
|
||||
CONSTRAINT ck_project_document_featured_order CHECK (featured_order IS NULL OR featured_order >= 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_document_primary_project
|
||||
ON project_document_link(document_id)
|
||||
WHERE relation_type = 'PRIMARY';
|
||||
|
||||
CREATE TABLE project_question_link (
|
||||
project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
||||
question_id uuid NOT NULL REFERENCES open_question(id),
|
||||
relation_type varchar(20) NOT NULL
|
||||
CHECK (relation_type IN ('PRIMARY', 'RELATED')),
|
||||
featured_order integer,
|
||||
PRIMARY KEY (project_id, question_id),
|
||||
CONSTRAINT ck_project_question_featured_order CHECK (featured_order IS NULL OR featured_order >= 0)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_question_primary_project
|
||||
ON project_question_link(question_id)
|
||||
WHERE relation_type = 'PRIMARY';
|
||||
|
||||
CREATE TABLE project_decision (
|
||||
id uuid PRIMARY KEY,
|
||||
project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
||||
statement varchar(1000) NOT NULL,
|
||||
rationale_markdown text NOT NULL DEFAULT '',
|
||||
consequences jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
CHECK (jsonb_typeof(consequences) = 'array'),
|
||||
alternatives_markdown text NOT NULL DEFAULT '',
|
||||
decision_status varchar(20) NOT NULL DEFAULT 'PROPOSED'
|
||||
CHECK (decision_status IN ('PROPOSED', 'ACCEPTED', 'SUPERSEDED', 'REJECTED')),
|
||||
target_visibility varchar(20) NOT NULL DEFAULT 'PRIVATE'
|
||||
CHECK (target_visibility IN ('PRIVATE', 'PUBLIC')),
|
||||
source_question_id uuid REFERENCES open_question(id),
|
||||
source_case_id uuid REFERENCES document(id),
|
||||
superseded_by_id uuid,
|
||||
is_featured boolean NOT NULL DEFAULT false,
|
||||
decided_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT fk_project_decision_superseded_by
|
||||
FOREIGN KEY (superseded_by_id)
|
||||
REFERENCES project_decision(id),
|
||||
CONSTRAINT ck_project_decision_not_self_supersede
|
||||
CHECK (superseded_by_id IS NULL OR superseded_by_id <> id),
|
||||
CONSTRAINT ck_project_decision_status_fields CHECK (
|
||||
decision_status NOT IN ('ACCEPTED', 'SUPERSEDED')
|
||||
OR decided_at IS NOT NULL
|
||||
),
|
||||
CONSTRAINT ck_project_decision_supersede_target CHECK (
|
||||
decision_status <> 'SUPERSEDED'
|
||||
OR superseded_by_id IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_project_featured_decision
|
||||
ON project_decision(project_id)
|
||||
WHERE is_featured = true;
|
||||
|
||||
CREATE TABLE project_activity (
|
||||
id uuid PRIMARY KEY,
|
||||
project_id uuid NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
||||
activity_type varchar(40) NOT NULL
|
||||
CHECK (activity_type IN (
|
||||
'PHASE_CHANGED',
|
||||
'QUESTION_OPENED',
|
||||
'QUESTION_RESOLVED',
|
||||
'DECISION_ACCEPTED',
|
||||
'CASE_PUBLISHED',
|
||||
'REFERENCE_PUBLISHED',
|
||||
'MILESTONE_REACHED',
|
||||
'PROJECT_PAUSED',
|
||||
'PROJECT_RESUMED'
|
||||
)),
|
||||
title varchar(180) NOT NULL,
|
||||
summary varchar(600),
|
||||
visibility varchar(20) NOT NULL DEFAULT 'PRIVATE'
|
||||
CHECK (visibility IN ('PRIVATE', 'PUBLIC')),
|
||||
origin varchar(20) NOT NULL
|
||||
CHECK (origin IN ('AUTO', 'MANUAL')),
|
||||
related_resource_type varchar(30),
|
||||
related_resource_id uuid,
|
||||
occurred_at timestamptz NOT NULL,
|
||||
operation_key varchar(180),
|
||||
version bigint NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by varchar(255) NOT NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_by varchar(255) NOT NULL,
|
||||
CONSTRAINT uq_project_activity_operation_key UNIQUE (project_id, operation_key)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Publication and public read model
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE public_resource_projection (
|
||||
resource_type varchar(30) NOT NULL
|
||||
CHECK (resource_type IN (
|
||||
'CASE',
|
||||
'REFERENCE',
|
||||
'QUESTION',
|
||||
'PROJECT',
|
||||
'PROJECT_DECISION',
|
||||
'PROJECT_ACTIVITY',
|
||||
'RELEASE',
|
||||
'PROFILE'
|
||||
)),
|
||||
resource_id uuid NOT NULL,
|
||||
source_version bigint NOT NULL CHECK (source_version >= 0),
|
||||
publication_state varchar(20) NOT NULL
|
||||
CHECK (publication_state IN ('ACTIVE', 'WITHDRAWN')),
|
||||
visibility varchar(20) NOT NULL
|
||||
CHECK (visibility IN ('PUBLIC', 'UNLISTED')),
|
||||
title varchar(300) NOT NULL,
|
||||
summary varchar(600),
|
||||
state_code varchar(30),
|
||||
primary_topic_id uuid REFERENCES topic(id),
|
||||
payload_schema_version smallint NOT NULL CHECK (payload_schema_version > 0),
|
||||
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'),
|
||||
body_plain_text text NOT NULL DEFAULT '',
|
||||
search_text text NOT NULL DEFAULT '',
|
||||
content_hash char(64) NOT NULL,
|
||||
published_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
last_verified_at timestamptz,
|
||||
latest_index_at timestamptz,
|
||||
navigation_path varchar(500) NOT NULL,
|
||||
PRIMARY KEY (resource_type, resource_id)
|
||||
);
|
||||
|
||||
CREATE TABLE public_route (
|
||||
resource_type varchar(30) NOT NULL,
|
||||
slug varchar(180) NOT NULL,
|
||||
resource_id uuid NOT NULL,
|
||||
route_role varchar(20) NOT NULL
|
||||
CHECK (route_role IN ('CANONICAL', 'ALIAS')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (resource_type, slug),
|
||||
CONSTRAINT fk_public_route_projection
|
||||
FOREIGN KEY (resource_type, resource_id)
|
||||
REFERENCES public_resource_projection(resource_type, resource_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_public_route_canonical
|
||||
ON public_route(resource_type, resource_id)
|
||||
WHERE route_role = 'CANONICAL';
|
||||
|
||||
CREATE TABLE public_resource_tag (
|
||||
resource_type varchar(30) NOT NULL,
|
||||
resource_id uuid NOT NULL,
|
||||
tag_id uuid NOT NULL REFERENCES tag(id),
|
||||
display_order integer NOT NULL DEFAULT 0 CHECK (display_order >= 0),
|
||||
PRIMARY KEY (resource_type, resource_id, tag_id),
|
||||
CONSTRAINT fk_public_resource_tag_projection
|
||||
FOREIGN KEY (resource_type, resource_id)
|
||||
REFERENCES public_resource_projection(resource_type, resource_id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT uq_public_resource_tag_order
|
||||
UNIQUE (resource_type, resource_id, display_order)
|
||||
);
|
||||
|
||||
CREATE TABLE public_resource_project_link (
|
||||
resource_type varchar(30) NOT NULL,
|
||||
resource_id uuid NOT NULL,
|
||||
project_id uuid NOT NULL REFERENCES project(id),
|
||||
relation_type varchar(20) NOT NULL
|
||||
CHECK (relation_type IN ('PRIMARY', 'RELATED')),
|
||||
featured_order integer,
|
||||
PRIMARY KEY (resource_type, resource_id, project_id),
|
||||
CONSTRAINT fk_public_resource_project_projection
|
||||
FOREIGN KEY (resource_type, resource_id)
|
||||
REFERENCES public_resource_projection(resource_type, resource_id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT ck_public_project_featured_order CHECK (
|
||||
featured_order IS NULL OR featured_order >= 0
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_public_primary_project
|
||||
ON public_resource_project_link(resource_type, resource_id)
|
||||
WHERE relation_type = 'PRIMARY';
|
||||
|
||||
CREATE TABLE asset_reference (
|
||||
asset_id uuid NOT NULL REFERENCES asset(id),
|
||||
owner_type varchar(30) NOT NULL
|
||||
CHECK (owner_type IN (
|
||||
'DOCUMENT',
|
||||
'QUESTION',
|
||||
'QUESTION_UPDATE',
|
||||
'PROJECT',
|
||||
'DECISION',
|
||||
'RELEASE',
|
||||
'PROFILE',
|
||||
'SITE'
|
||||
)),
|
||||
owner_id uuid NOT NULL,
|
||||
reference_scope varchar(20) NOT NULL
|
||||
CHECK (reference_scope IN ('WORKING', 'PUBLISHED')),
|
||||
reference_role varchar(20) NOT NULL
|
||||
CHECK (reference_role IN ('BODY', 'COVER', 'AVATAR', 'ATTACHMENT')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (asset_id, owner_type, owner_id, reference_scope, reference_role)
|
||||
);
|
||||
|
||||
-- preview_token 테이블은 제거되었다.
|
||||
-- Capability Token 기반 익명 Preview는 인증된 Preview Artifact(studio_preview)로
|
||||
-- 대체되었다. contracts/openapi/preview-v1.deprecated.md 참고.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Studio workflow artifacts
|
||||
--
|
||||
-- WorkingCopy는 API projection이므로 범용 working_copy 테이블을 만들지 않는다.
|
||||
-- 아래 테이블은 편집 대상 자체가 아니라 "편집 흐름이 만들어내는 산출물"을 저장한다.
|
||||
--
|
||||
-- source_kind + source_id는 Studio API의 documentId를 가리킨다.
|
||||
-- documentId는 source aggregate id를 그대로 사용하므로 별도 surrogate id가 없다.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Validation은 일급 artifact다. 실행하고 버리는 결과가 아니라 특정 version을
|
||||
-- 검증한 사실을 validation_id로 참조할 수 있어야 한다.
|
||||
CREATE TABLE studio_validation (
|
||||
validation_id uuid PRIMARY KEY,
|
||||
source_kind varchar(30) NOT NULL
|
||||
CHECK (source_kind IN ('CASE', 'REFERENCE', 'QUESTION', 'PROJECT_DECISION')),
|
||||
source_id uuid NOT NULL,
|
||||
validated_version bigint NOT NULL CHECK (validated_version >= 0),
|
||||
status varchar(20) NOT NULL
|
||||
CHECK (status IN ('INVALID', 'WARNINGS', 'VALID')),
|
||||
issues jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
CHECK (jsonb_typeof(issues) = 'array'),
|
||||
-- 검증에 사용한 외부 의존 상태(Topic/Project publishability, relation target,
|
||||
-- Asset READY/QUARANTINED, slug/route ownership, catalog revision,
|
||||
-- renderer/content-format version)를 정규화한 hash.
|
||||
-- Publish 시 다시 계산해 값이 다르면 VALIDATION_STALE로 거절한다.
|
||||
dependency_revision varchar(200) NOT NULL,
|
||||
validated_at timestamptz NOT NULL DEFAULT now(),
|
||||
valid_until timestamptz NOT NULL,
|
||||
created_by varchar(255) NOT NULL,
|
||||
CONSTRAINT ck_studio_validation_window CHECK (valid_until > validated_at)
|
||||
);
|
||||
|
||||
-- Preview는 저장된 version + validation + dependency revision을 묶어 만든
|
||||
-- PublicRenderModel snapshot이다. 인증된 Studio API로만 조회한다.
|
||||
-- CURRENT/STALE/EXPIRED 상태는 저장하지 않고 조회 시점에 서버가 계산한다.
|
||||
CREATE TABLE studio_preview (
|
||||
preview_id uuid PRIMARY KEY,
|
||||
source_kind varchar(30) NOT NULL
|
||||
CHECK (source_kind IN ('CASE', 'REFERENCE', 'QUESTION', 'PROJECT_DECISION')),
|
||||
source_id uuid NOT NULL,
|
||||
source_version bigint NOT NULL CHECK (source_version >= 0),
|
||||
validation_id uuid NOT NULL REFERENCES studio_validation(validation_id),
|
||||
dependency_revision varchar(200) NOT NULL,
|
||||
render_model jsonb NOT NULL CHECK (jsonb_typeof(render_model) = 'object'),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_by varchar(255) NOT NULL,
|
||||
CONSTRAINT ck_studio_preview_expiry CHECK (expires_at > created_at)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Publication aggregate, immutable history, immutable snapshot
|
||||
--
|
||||
-- 세 개념을 분리한다.
|
||||
--
|
||||
-- publication 현재 게시 상태
|
||||
-- publication_event 게시/재게시/게시 취소 불변 이력
|
||||
-- publication_snapshot PUBLISHED/REPUBLISHED 시점의 불변 PublicRenderModel
|
||||
--
|
||||
-- public_resource_projection은 여전히 "현재 공개 상태"를 담당한다.
|
||||
-- 과거 Snapshot을 현재 source나 현재 projection에서 재계산하지 않는다.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE publication (
|
||||
publication_id uuid PRIMARY KEY,
|
||||
source_kind varchar(30) NOT NULL
|
||||
CHECK (source_kind IN ('CASE', 'REFERENCE', 'QUESTION', 'PROJECT_DECISION')),
|
||||
source_id uuid NOT NULL,
|
||||
status varchar(20) NOT NULL
|
||||
CHECK (status IN ('PUBLISHED', 'UNPUBLISHED')),
|
||||
published_version bigint NOT NULL CHECK (published_version >= 0),
|
||||
-- Publication 자체의 optimistic concurrency 토큰.
|
||||
-- unpublish는 expectedPublicationRevision으로 이 값을 검증한다.
|
||||
publication_revision bigint NOT NULL DEFAULT 1 CHECK (publication_revision >= 1),
|
||||
latest_event_id uuid NOT NULL,
|
||||
public_path varchar(500) NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT uq_publication_source UNIQUE (source_kind, source_id)
|
||||
);
|
||||
|
||||
CREATE TABLE publication_event (
|
||||
publication_event_id uuid PRIMARY KEY,
|
||||
publication_id uuid NOT NULL REFERENCES publication(publication_id),
|
||||
source_kind varchar(30) NOT NULL
|
||||
CHECK (source_kind IN ('CASE', 'REFERENCE', 'QUESTION', 'PROJECT_DECISION')),
|
||||
source_id uuid NOT NULL,
|
||||
event_type varchar(20) NOT NULL
|
||||
CHECK (event_type IN ('PUBLISHED', 'REPUBLISHED', 'UNPUBLISHED')),
|
||||
published_version bigint NOT NULL CHECK (published_version >= 0),
|
||||
-- UNPUBLISHED Event는 자체 snapshot을 만들지 않고 마지막 공개 Snapshot을 참조한다.
|
||||
source_published_event_id uuid REFERENCES publication_event(publication_event_id),
|
||||
occurred_at timestamptz NOT NULL DEFAULT now(),
|
||||
-- Publish 재시도가 중복 Event를 만들지 않도록 최초 요청의 idempotency key를 남긴다.
|
||||
idempotency_key varchar(200),
|
||||
created_by varchar(255) NOT NULL,
|
||||
CONSTRAINT ck_publication_event_source_ref CHECK (
|
||||
(event_type = 'UNPUBLISHED' AND source_published_event_id IS NOT NULL)
|
||||
OR (event_type <> 'UNPUBLISHED' AND source_published_event_id IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
-- Event row는 생성 후 수정하지 않는다. UPDATE/DELETE 차단은 권한과 application
|
||||
-- rule로 강제하며, 필요하면 운영에서 REVOKE UPDATE, DELETE로 보강한다.
|
||||
|
||||
-- 첫 게시는 publication(latest_event_id) -> publication_event -> publication UPDATE
|
||||
-- 순서로 한 transaction 안에서 처리된다. 순환 참조를 허용하기 위해 지연 검사한다.
|
||||
ALTER TABLE publication
|
||||
ADD CONSTRAINT fk_publication_latest_event
|
||||
FOREIGN KEY (latest_event_id)
|
||||
REFERENCES publication_event(publication_event_id)
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
CREATE TABLE publication_snapshot (
|
||||
publication_event_id uuid PRIMARY KEY
|
||||
REFERENCES publication_event(publication_event_id),
|
||||
render_model jsonb NOT NULL CHECK (jsonb_typeof(render_model) = 'object'),
|
||||
content_format_version varchar(50) NOT NULL,
|
||||
renderer_contract_version varchar(50) NOT NULL,
|
||||
-- 게시 시점에 사용된 Asset의 assetKey/delivery path/치수를 고정한다.
|
||||
-- 이후 Asset이 교체되어도 과거 Snapshot의 표현은 변하지 않는다.
|
||||
asset_manifest jsonb NOT NULL DEFAULT '[]'::jsonb
|
||||
CHECK (jsonb_typeof(asset_manifest) = 'array'),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Indexes
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CREATE INDEX idx_document_management
|
||||
ON document(document_type, workflow_status, updated_at DESC);
|
||||
|
||||
CREATE INDEX idx_document_topic
|
||||
ON document(primary_topic_id, document_type, updated_at DESC);
|
||||
|
||||
CREATE INDEX idx_question_status
|
||||
ON open_question(question_status, updated_at DESC);
|
||||
|
||||
CREATE INDEX idx_question_topic
|
||||
ON open_question(primary_topic_id, question_status, updated_at DESC);
|
||||
|
||||
CREATE INDEX idx_question_update_timeline
|
||||
ON question_update(question_id, occurred_at ASC, sequence_no ASC);
|
||||
|
||||
CREATE INDEX idx_project_phase
|
||||
ON project(phase, updated_at DESC);
|
||||
|
||||
CREATE INDEX idx_project_decision
|
||||
ON project_decision(project_id, decision_status, decided_at DESC);
|
||||
|
||||
CREATE INDEX idx_project_activity
|
||||
ON project_activity(project_id, visibility, occurred_at DESC);
|
||||
|
||||
CREATE INDEX idx_asset_status
|
||||
ON asset(management_status, created_at DESC);
|
||||
|
||||
CREATE INDEX idx_asset_checksum
|
||||
ON asset(checksum_sha256);
|
||||
|
||||
CREATE INDEX idx_asset_reference_owner
|
||||
ON asset_reference(owner_type, owner_id, reference_scope);
|
||||
|
||||
CREATE INDEX idx_asset_reference_asset
|
||||
ON asset_reference(asset_id, reference_scope);
|
||||
|
||||
CREATE INDEX idx_public_latest
|
||||
ON public_resource_projection(latest_index_at DESC, resource_type, resource_id)
|
||||
WHERE publication_state = 'ACTIVE'
|
||||
AND visibility = 'PUBLIC'
|
||||
AND latest_index_at IS NOT NULL;
|
||||
|
||||
CREATE INDEX idx_public_topic
|
||||
ON public_resource_projection(primary_topic_id, resource_type, published_at DESC)
|
||||
WHERE publication_state = 'ACTIVE'
|
||||
AND visibility = 'PUBLIC';
|
||||
|
||||
CREATE INDEX idx_public_type
|
||||
ON public_resource_projection(resource_type, visibility, published_at DESC)
|
||||
WHERE publication_state = 'ACTIVE';
|
||||
|
||||
CREATE INDEX idx_public_projection_search_trgm
|
||||
ON public_resource_projection
|
||||
USING gin (search_text gin_trgm_ops)
|
||||
WHERE publication_state = 'ACTIVE'
|
||||
AND visibility = 'PUBLIC';
|
||||
|
||||
CREATE INDEX idx_public_project_link_lookup
|
||||
ON public_resource_project_link(project_id, relation_type, resource_type);
|
||||
|
||||
-- Studio workflow artifacts -------------------------------------------------
|
||||
|
||||
-- 특정 version에 대한 최신 Validation 조회 (nextAction 계산의 핵심 경로)
|
||||
CREATE INDEX idx_studio_validation_source
|
||||
ON studio_validation(source_kind, source_id, validated_version, validated_at DESC);
|
||||
|
||||
-- 특정 version에 대한 최신 Preview 조회
|
||||
CREATE INDEX idx_studio_preview_source
|
||||
ON studio_preview(source_kind, source_id, source_version, created_at DESC);
|
||||
|
||||
-- 만료 Preview 정리 배치
|
||||
CREATE INDEX idx_studio_preview_expiry
|
||||
ON studio_preview(expires_at);
|
||||
|
||||
-- Publication history -------------------------------------------------------
|
||||
|
||||
-- 한 문서의 게시 이력 (occurredAt DESC, publicationEventId DESC 정렬 계약과 일치)
|
||||
CREATE INDEX idx_publication_event_publication
|
||||
ON publication_event(publication_id, occurred_at DESC, publication_event_id DESC);
|
||||
|
||||
-- 전체 게시 기록 화면과 source 기준 조회
|
||||
CREATE INDEX idx_publication_event_source
|
||||
ON publication_event(source_kind, source_id, occurred_at DESC);
|
||||
+1
-1
@@ -44,7 +44,7 @@ class PostgreSqlMigrationIntegrationTest {
|
||||
.migrate();
|
||||
|
||||
assertThat(appliedVersions(postgres, "flyway_schema_history"))
|
||||
.containsExactly("1", "3", "4", "5", "6");
|
||||
.containsExactly("1", "3", "4", "5", "6", "7");
|
||||
|
||||
Flyway coreStream =
|
||||
Flyway.configure()
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.techlog;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.sql.DataSource;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.DockerClientFactory;
|
||||
import org.testcontainers.postgresql.PostgreSQLContainer;
|
||||
|
||||
/**
|
||||
* V7이 실제 PostgreSQL에 적용되는지, 그리고 순환 FK가 deferrable로 선언됐는지 본다. H2로는 검증할 수 없다 — deferrable 제약이 벤더 의미이기
|
||||
* 때문이다.
|
||||
*
|
||||
* <p>이 모듈(persistence-jpa)에는 {@code @SpringBootConfiguration}이 없다 — Boot 메인 클래스는 app-bootstrap 모듈에
|
||||
* 있고, 이 모듈의 postgresqlIntegrationTest 소스셋 classpath에는 포함되지 않는다. 그래서 {@code @SpringBootTest}로 컨텍스트를
|
||||
* 띄울 수 없고, 이 패키지의 형제인 {@code readiness.PostgreSqlMigrationIntegrationTest}와 같은 방식 — Testcontainers
|
||||
* 위에서 순수 Flyway API를 직접 구동 — 을 쓴다. 컨테이너는 매번 완전히 빈 상태로 시작하므로, 기존 V1/V3/V4/V5/V6 다음에 V7이 얹히는 전체 체인이
|
||||
* 클린 DB에 처음부터 적용되는 경로를 그대로 검증한다.
|
||||
*/
|
||||
class TechLogSchemaMigrationTest {
|
||||
|
||||
private static final String IMAGE =
|
||||
System.getProperty("jpa.evidence.postgresql.image", "postgres:16-alpine");
|
||||
|
||||
private static PostgreSQLContainer postgres;
|
||||
private static HikariDataSource dataSource;
|
||||
|
||||
@BeforeAll
|
||||
static void migrateFreshDatabase() {
|
||||
if (!DockerClientFactory.instance().isDockerAvailable()) {
|
||||
throw new IllegalStateException(
|
||||
"Docker is required for the Tech Log schema migration test; skipping is forbidden");
|
||||
}
|
||||
postgres = new PostgreSQLContainer(IMAGE).withReuse(false);
|
||||
postgres.start();
|
||||
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl(postgres.getJdbcUrl());
|
||||
config.setUsername(postgres.getUsername());
|
||||
config.setPassword(postgres.getPassword());
|
||||
config.setMaximumPoolSize(5);
|
||||
config.setMinimumIdle(1);
|
||||
dataSource = new HikariDataSource(config);
|
||||
|
||||
// classpath:db/migration/postgresql only — the same location
|
||||
// PostgreSqlPersistenceConfig's FlywayConfigurationCustomizer pins the application to. Using
|
||||
// the full "classpath:db/migration" tree here would also pick up the unrelated jpa/* streams
|
||||
// (each starting their own V1) and collide.
|
||||
Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.locations("classpath:db/migration/postgresql")
|
||||
.table("flyway_schema_history")
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopPostgreSql() {
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
if (postgres != null) {
|
||||
postgres.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsEveryTechLogTable() throws Exception {
|
||||
List<String> expected =
|
||||
List.of(
|
||||
"topic",
|
||||
"tag",
|
||||
"document",
|
||||
"case_detail",
|
||||
"reference_detail",
|
||||
"document_tag",
|
||||
"document_relation",
|
||||
"open_question",
|
||||
"question_point",
|
||||
"question_update",
|
||||
"question_tag",
|
||||
"question_document_link",
|
||||
"project",
|
||||
"project_decision",
|
||||
"project_document_link",
|
||||
"project_question_link",
|
||||
"project_activity",
|
||||
"asset",
|
||||
"asset_reference",
|
||||
"studio_validation",
|
||||
"studio_preview",
|
||||
"publication",
|
||||
"publication_event",
|
||||
"publication_snapshot",
|
||||
"public_resource_projection",
|
||||
"public_route",
|
||||
"public_resource_tag",
|
||||
"public_resource_project_link");
|
||||
|
||||
List<String> actual = new ArrayList<>();
|
||||
try (Connection connection = dataSource().getConnection();
|
||||
ResultSet rs =
|
||||
connection
|
||||
.createStatement()
|
||||
.executeQuery(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")) {
|
||||
while (rs.next()) {
|
||||
actual.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
assertThat(actual).containsAll(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotCreateAStudioIdempotencyTable() throws Exception {
|
||||
try (Connection connection = dataSource().getConnection();
|
||||
ResultSet rs =
|
||||
connection
|
||||
.createStatement()
|
||||
.executeQuery(
|
||||
"SELECT count(*) FROM information_schema.tables "
|
||||
+ "WHERE table_schema = 'public' AND table_name = 'studio_idempotency'")) {
|
||||
rs.next();
|
||||
assertThat(rs.getInt(1)).isZero();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicationLatestEventForeignKeyIsDeferrable() throws Exception {
|
||||
try (Connection connection = dataSource().getConnection();
|
||||
ResultSet rs =
|
||||
connection
|
||||
.createStatement()
|
||||
.executeQuery(
|
||||
"SELECT condeferrable, condeferred FROM pg_constraint "
|
||||
+ "WHERE conname = 'fk_publication_latest_event'")) {
|
||||
assertThat(rs.next()).as("fk_publication_latest_event 제약이 있어야 한다").isTrue();
|
||||
assertThat(rs.getBoolean(1)).as("deferrable").isTrue();
|
||||
assertThat(rs.getBoolean(2)).as("initially deferred").isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
private static DataSource dataSource() {
|
||||
return dataSource;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package dev.caskeleton.adapter.outbound.persistence.techlog.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryType;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogPageView;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.testcontainers.DockerClientFactory;
|
||||
import org.testcontainers.postgresql.PostgreSQLContainer;
|
||||
|
||||
/**
|
||||
* 이 모듈(persistence-jpa)에는 {@code @SpringBootConfiguration}이 없다 — Boot 메인 클래스는 app-bootstrap 모듈에 있고,
|
||||
* 이 모듈의 postgresqlIntegrationTest 소스셋 classpath에는 포함되지 않는다. 그래서 브리프의 {@code @SpringBootTest}로는
|
||||
* 컨텍스트를 띄울 수 없다({@code TechLogSchemaMigrationTest}가 같은 문제를 겪었다). 이 테스트도 같은 형제 패턴 — Testcontainers
|
||||
* 위에서 순수 Flyway로 V7까지 적용한 뒤, {@code JdbcClient}와 어댑터를 직접 조립 — 을 쓴다. Spring 컨테이너가 없어도 {@code
|
||||
* JdbcCatalogQueryAdapter}는 생성자 인자로 {@code JdbcClient} 하나만 받으므로 문제가 없다.
|
||||
*/
|
||||
class JdbcCatalogQueryAdapterTest {
|
||||
|
||||
private static final String IMAGE =
|
||||
System.getProperty("jpa.evidence.postgresql.image", "postgres:16-alpine");
|
||||
|
||||
private static PostgreSQLContainer postgres;
|
||||
private static HikariDataSource dataSource;
|
||||
private static JdbcClient jdbcClient;
|
||||
private static JdbcCatalogQueryAdapter adapter;
|
||||
|
||||
@BeforeAll
|
||||
static void migrateFreshDatabase() {
|
||||
if (!DockerClientFactory.instance().isDockerAvailable()) {
|
||||
throw new IllegalStateException(
|
||||
"Docker is required for the catalog query adapter test; skipping is forbidden");
|
||||
}
|
||||
postgres = new PostgreSQLContainer(IMAGE).withReuse(false);
|
||||
postgres.start();
|
||||
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl(postgres.getJdbcUrl());
|
||||
config.setUsername(postgres.getUsername());
|
||||
config.setPassword(postgres.getPassword());
|
||||
config.setMaximumPoolSize(5);
|
||||
config.setMinimumIdle(1);
|
||||
dataSource = new HikariDataSource(config);
|
||||
|
||||
Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.locations("classpath:db/migration/postgresql")
|
||||
.table("flyway_schema_history")
|
||||
.baselineOnMigrate(false)
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
|
||||
jdbcClient = JdbcClient.create(dataSource);
|
||||
adapter = new JdbcCatalogQueryAdapter(jdbcClient);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopPostgreSql() {
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
if (postgres != null) {
|
||||
postgres.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void findsTopicsByPrefix() {
|
||||
jdbcClient
|
||||
.sql(
|
||||
"INSERT INTO topic (id, name, normalized_name, slug, created_by, updated_by) "
|
||||
+ "VALUES (gen_random_uuid(), 'Kafka', 'kafka', 'kafka', 'test', 'test')")
|
||||
.update();
|
||||
|
||||
CatalogPageView page = adapter.search(CatalogEntryType.TOPIC, "kaf", null, 20);
|
||||
|
||||
assertThat(page.items()).hasSize(1);
|
||||
assertThat(page.items().get(0).label()).isEqualTo("Kafka");
|
||||
assertThat(page.items().get(0).dependencyRevision()).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsAnEmptyPageWhenNothingMatches() {
|
||||
CatalogPageView page = adapter.search(CatalogEntryType.TOPIC, "zzzz-none", null, 20);
|
||||
|
||||
assertThat(page.items()).isEmpty();
|
||||
assertThat(page.nextCursor()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Review Important 1: {@code searchProjects} (JdbcCatalogQueryAdapter) had never run against a
|
||||
* real database — {@code project} has a different column shape than {@code topic} (slug/name/
|
||||
* workflow_status vs. name/normalized_name/slug/status), so a column typo or bad bind would only
|
||||
* have surfaced in production. {@code project}'s NOT-NULL-without-default columns are {@code id},
|
||||
* {@code name}, {@code created_by}, {@code updated_by} (V7__techlog_core.sql CREATE TABLE
|
||||
* project) — everything else has a DEFAULT or is nullable, so the minimal INSERT below is valid.
|
||||
*/
|
||||
@Test
|
||||
void findsProjectsByPrefix() {
|
||||
jdbcClient
|
||||
.sql(
|
||||
"INSERT INTO project (id, name, created_by, updated_by) "
|
||||
+ "VALUES (gen_random_uuid(), 'Payments Platform', 'test', 'test')")
|
||||
.update();
|
||||
|
||||
CatalogPageView page = adapter.search(CatalogEntryType.PROJECT, "pay", null, 20);
|
||||
|
||||
assertThat(page.items()).hasSize(1);
|
||||
assertThat(page.items().get(0).id()).isNotNull();
|
||||
assertThat(page.items().get(0).label()).isEqualTo("Payments Platform");
|
||||
assertThat(page.items().get(0).kind()).isEqualTo("PROJECT");
|
||||
assertThat(page.items().get(0).dependencyRevision()).isNotBlank();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@ sourceSets {
|
||||
functionalTest {
|
||||
java.srcDir 'src/functionalTest/java'
|
||||
resources.srcDir 'src/functionalTest/resources'
|
||||
// feature-techlog-studio-backend Task 10 — StudioContractDriftTest reuses
|
||||
// RepositoryContractResources (test-sourceSet-owned, see
|
||||
// dev.caskeleton.bootstrap.contract.support) for fail-closed repo-root resolution instead
|
||||
// of a hand-rolled relative Path.of(..), matching the sibling contract tests' convention.
|
||||
compileClasspath += sourceSets.test.output
|
||||
runtimeClasspath += sourceSets.test.output
|
||||
}
|
||||
conditionalTransportTest {
|
||||
java.srcDir 'src/conditionalTransportTest/java'
|
||||
@@ -121,6 +127,33 @@ dependencies {
|
||||
functionalTestImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
functionalTestImplementation 'org.assertj:assertj-core'
|
||||
functionalTestRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
// feature-techlog-studio-backend Task 10 — StudioContractDriftTest boots a minimal Studio web
|
||||
// slice (real StudioSessionController/StudioCatalogController, no persistence/messaging/cache)
|
||||
// to diff springdoc's published /api/v1/studio/** surface against studio-v1.yaml. Only the two
|
||||
// modules the slice actually needs — deliberately not the full app-bootstrap runtime graph, so
|
||||
// no DataSource/Flyway/Redis auto-configuration is even on this classpath to exclude.
|
||||
functionalTestImplementation project(':adapter:inbound:web')
|
||||
functionalTestImplementation project(':application-core')
|
||||
// test-only: @SpringBootTest/MockMvc/@AutoConfigureMockMvc — mirrors the root build.gradle
|
||||
// subprojects{} pair every non-core module already gets on its ordinary `test` sourceSet.
|
||||
functionalTestImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
functionalTestImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
|
||||
// test-only: classic Jackson (2.x) ObjectMapper/JsonNode to read /v3/api-docs and convert the
|
||||
// SnakeYaml-parsed contract into a comparable tree. adapter-inbound-web/application-core pull
|
||||
// this in only as a project-dependency `implementation` (hidden from a consumer's
|
||||
// compileClasspath by Gradle's api/implementation split), so it must be declared directly here
|
||||
// — mirrors the existing `testImplementation 'org.springframework.boot:spring-boot-starter-json'`
|
||||
// pattern below for app-bootstrap's own `test` sourceSet.
|
||||
functionalTestImplementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
// test-only: org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration (excluded
|
||||
// below) is part of spring-boot-security, only pulled in transitively by spring-boot-starter-security
|
||||
// — same api/implementation-hiding reason as jackson-databind above. app-bootstrap declares this
|
||||
// on `implementation` for its own main/test sourceSets, which functionalTest does not extend.
|
||||
functionalTestImplementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
// test-only: parses config/openapi/studio-v1.yaml with the same library StudioErrorRegistryTest
|
||||
// already uses for docs/registries/error-codes.yaml — avoids adding jackson-dataformat-yaml
|
||||
// (present only on runtimeClasspath repo-wide, transitively via springdoc, not compileClasspath).
|
||||
functionalTestImplementation 'org.yaml:snakeyaml'
|
||||
// Explicit qualification-only composition. These projects remain absent from main
|
||||
// api/implementation/compileOnly/runtimeOnly and therefore from both shipped runtime graphs.
|
||||
conditionalTransportTestImplementation project(':adapter:inbound:graphql')
|
||||
@@ -166,13 +199,22 @@ sampleOffQualification.configure {
|
||||
|
||||
tasks.register('functionalTest', Test) {
|
||||
group = 'verification'
|
||||
description = 'Runs isolated Gradle TestKit contracts for repository build behavior.'
|
||||
description = 'Runs isolated Gradle TestKit contracts for repository build behavior, plus the ' +
|
||||
'feature-techlog-studio-backend Studio contract drift gate.'
|
||||
testClassesDirs = sourceSets.functionalTest.output.classesDirs
|
||||
classpath = sourceSets.functionalTest.runtimeClasspath
|
||||
useJUnitPlatform()
|
||||
failOnNoDiscoveredTests = true
|
||||
shouldRunAfter tasks.named('test')
|
||||
jvmArgs '-Duser.timezone=UTC'
|
||||
// feature-techlog-studio-backend Task 10 — StudioContractDriftTest boots a real (minimal)
|
||||
// Spring Boot context that needs Logback, on the same classpath as gradleTestKit() (whose own
|
||||
// SLF4J provider — org.gradle.internal.logging.slf4j.OutputEventListenerBackedLoggerContext —
|
||||
// wins classpath scanning over the real one). Spring Boot's LogbackLoggingSystem then finds
|
||||
// Logback's jar present but the bound ILoggerFactory is Gradle's fake context, and fails fast
|
||||
// with IllegalStateException before the context even starts. LoggingSystem=none skips Boot's
|
||||
// logging bootstrap entirely — this task doesn't assert on log output, so there is nothing lost.
|
||||
systemProperty 'org.springframework.boot.logging.LoggingSystem', 'none'
|
||||
}
|
||||
|
||||
def conditionalTransportCompositionQualification = registerStrictQualificationTest(
|
||||
|
||||
+108
-108
@@ -2,26 +2,26 @@
|
||||
# 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,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,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
ch.qos.logback:logback-core:1.5.21=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
ch.qos.logback:logback-classic:1.5.21=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
ch.qos.logback:logback-core:1.5.21=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.approvaltests:approvaltests-util:31.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.approvaltests:approvaltests:31.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.ethlo.time:itu:1.14.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml:classmate:1.7.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.fasterxml:classmate:1.7.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,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.kevinstern:software-and-algorithms:1.0=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,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,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
com.github.spotbugs:spotbugs:4.10.2=spotbugs
|
||||
com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
com.google.android:annotations:4.1.1.4=conditionalTransportTestRuntimeClasspath
|
||||
@@ -29,10 +29,10 @@ com.google.api.grpc:proto-google-common-protos:2.41.0=conditionalTransportTestRu
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.2=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,conditionalTransportTestRuntimeClasspath,sampleOffTestCompileClasspath,spotbugs,testCompileClasspath
|
||||
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,spotbugs,testCompileClasspath
|
||||
com.google.code.gson:gson:2.13.2=conditionalTransportTestRuntimeClasspath,spotbugs
|
||||
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
com.google.errorprone:error_prone_annotations:2.41.0=conditionalTransportTestRuntimeClasspath,spotbugs
|
||||
com.google.errorprone:error_prone_annotations:2.47.0=checkstyle
|
||||
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
@@ -54,11 +54,11 @@ com.graphql-java:graphql-java:25.0=conditionalTransportTestRuntimeClasspath
|
||||
com.graphql-java:java-dataloader:6.0.0=conditionalTransportTestRuntimeClasspath
|
||||
com.h2database:h2:2.4.240=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
|
||||
com.jayway.jsonpath:json-path:2.9.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.jayway.jsonpath:json-path:2.9.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.networknt:json-schema-validator:3.0.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
com.nimbusds:content-type:2.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.nimbusds:lang-tag:1.7=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.nimbusds:oauth2-oidc-sdk:11.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
|
||||
com.squareup.okhttp3:okhttp-jvm:5.2.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
@@ -71,14 +71,14 @@ com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=sampleOffTestRuntimeClassp
|
||||
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.vaadin.external.google:android-json:0.0.20131108.vaadin1=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
com.zaxxer:HikariCP:7.0.2=productionRuntimeClasspath,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-collections:commons-collections:3.2.2=checkstyle
|
||||
commons-io:commons-io:2.20.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
commons-io:commons-io:2.21.0=spotbugs
|
||||
commons-logging:commons-logging:1.3.5=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
commons-logging:commons-logging:1.3.5=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
info.picocli:picocli:4.7.7=checkstyle
|
||||
io.github.cdimascio:dotenv-java:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
@@ -101,10 +101,10 @@ io.grpc:grpc-stub:1.68.1=conditionalTransportTestRuntimeClasspath
|
||||
io.grpc:grpc-util:1.68.1=conditionalTransportTestRuntimeClasspath
|
||||
io.lettuce:lettuce-core:6.8.1.RELEASE=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.micrometer:context-propagation:1.2.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-commons:1.16.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-commons:1.16.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-core:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-jakarta9:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-observation:1.16.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-observation:1.16.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-registry-prometheus:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-tracing-bridge-otel:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.micrometer:micrometer-tracing:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
@@ -147,39 +147,39 @@ io.opentelemetry:opentelemetry-sdk:1.55.0=compileClasspath,productionRuntimeClas
|
||||
io.perfmark:perfmark-api:0.27.0=conditionalTransportTestRuntimeClasspath
|
||||
io.projectreactor.netty:reactor-netty-core:1.3.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.projectreactor.netty:reactor-netty-http:1.3.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.projectreactor:reactor-core:3.8.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.projectreactor:reactor-core:3.8.0=conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.prometheus:prometheus-metrics-config:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.prometheus:prometheus-metrics-core:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.prometheus:prometheus-metrics-exposition-formats:1.4.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.prometheus:prometheus-metrics-exposition-textformats:1.4.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.prometheus:prometheus-metrics-model:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.prometheus:prometheus-metrics-tracer-common:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-core-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-models-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
jakarta.activation:jakarta.activation-api:2.1.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-core-jakarta:2.2.38=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
io.swagger.core.v3:swagger-models-jakarta:2.2.38=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
jakarta.activation:jakarta.activation-api:2.1.4=functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
jakarta.persistence:jakarta.persistence-api:3.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.transaction:jakarta.transaction-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,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.xml.bind:jakarta.xml.bind-api:4.0.4=functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
jaxen:jaxen:2.0.0=spotbugs
|
||||
me.paulschwarz:spring-dotenv:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.bytebuddy:byte-buddy-agent:1.17.8=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.bytebuddy:byte-buddy-agent:1.17.8=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.bytebuddy:byte-buddy:1.17.8=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,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=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.minidev:json-smart:2.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.minidev:accessors-smart:2.6.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.minidev:json-smart:2.6.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
|
||||
org.antlr:antlr4-runtime:4.13.2=checkstyle,productionRuntimeClasspath,runtimeClasspath,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,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.commons:commons-lang3:3.20.0=checkstyle,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,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.client5:httpclient5:5.5.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
@@ -188,21 +188,21 @@ org.apache.httpcomponents.core5:httpcore5:5.3.6=productionRuntimeClasspath,runti
|
||||
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
|
||||
org.apache.httpcomponents:httpcore:4.4.16=checkstyle
|
||||
org.apache.kafka:kafka-clients:4.1.1=sampleOffTestCompileClasspath,testCompileClasspath
|
||||
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
|
||||
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,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:11.0.14=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.apache.xbean:xbean-reflect:3.7=checkstyle
|
||||
org.apiguardian:apiguardian-api:1.1.2=conditionalTransportTestCompileClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
org.aspectj:aspectjweaver:1.9.25=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.assertj:assertj-core:3.27.6=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.awaitility:awaitility:4.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.awaitility:awaitility:4.3.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.checkerframework:checker-qual:3.42.0=conditionalTransportTestRuntimeClasspath
|
||||
org.checkerframework:checker-qual:3.49.5=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.codehaus.mojo:animal-sniffer-annotations:1.24=conditionalTransportTestRuntimeClasspath
|
||||
@@ -224,17 +224,17 @@ org.flywaydb:flyway-database-postgresql:11.14.1=productionRuntimeClasspath,runti
|
||||
org.glassfish.jaxb:jaxb-core:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.glassfish.jaxb:jaxb-runtime:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.glassfish.jaxb:txw2:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.hamcrest:hamcrest:3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hamcrest:hamcrest:3.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.hibernate.models:hibernate-models:1.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.hibernate.orm:hibernate-core:7.1.8.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.javassist:javassist:3.28.0-GA=checkstyle
|
||||
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.jetbrains:annotations:13.0=productionRuntimeClasspath,runtimeClasspath
|
||||
org.jetbrains:annotations:17.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,conditionalTransportTestAnnotationProcessor,conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestAnnotationProcessor,functionalTestCompileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestAnnotationProcessor,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,conditionalTransportTestAnnotationProcessor,conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestAnnotationProcessor,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestAnnotationProcessor,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:6.0.1=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:6.0.1=conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-params:6.0.1=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
@@ -246,35 +246,35 @@ org.junit.platform:junit-platform-testkit:6.0.1=sampleOffTestCompileClasspath,sa
|
||||
org.junit:junit-bom:6.0.1=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:6.1.0=spotbugs
|
||||
org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.mockito:mockito-core:5.20.0=mockitoAgent,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,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.mockito:mockito-core:5.20.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,mockitoAgent,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.mockito:mockito-junit-jupiter:5.20.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.objenesis:objenesis:3.3=functionalTestRuntimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.openapitools:jackson-databind-nullable:0.2.6=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.opentest4j:opentest4j:1.3.0=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,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.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.resource:1.0.0=compileClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
|
||||
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,functionalTestCompileClasspath,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=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.ow2.asm:asm:9.7.1=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.pcollections:pcollections:4.0.1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
|
||||
org.postgresql:postgresql:42.7.8=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.reactivestreams:reactive-streams:1.0.4=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.reactivestreams:reactive-streams:1.0.4=conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,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,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-api:2.0.17=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
|
||||
org.skyscreamer:jsonassert:1.5.3=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-api:2.0.17=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
|
||||
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
|
||||
org.springdoc:springdoc-openapi-starter-common:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-common:3.0.0=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-actuator-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,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
|
||||
@@ -283,9 +283,9 @@ org.springframework.boot:spring-boot-flyway:4.0.0=compileClasspath,productionRun
|
||||
org.springframework.boot:spring-boot-graphql:4.0.0=conditionalTransportTestRuntimeClasspath
|
||||
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=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-jackson:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-http-client:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-http-converter:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-jackson:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,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
|
||||
@@ -293,73 +293,73 @@ org.springframework.boot:spring-boot-jpa:4.0.0=productionRuntimeClasspath,runtim
|
||||
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=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-restclient:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-resttestclient:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.0=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-security:4.0.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-servlet:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,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-starter-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
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-graphql:4.0.0=conditionalTransportTestRuntimeClasspath
|
||||
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=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jackson:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-jdbc:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-json:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,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-oauth2-resource-server:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
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=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-web:4.0.0=conditionalTransportTestRuntimeClasspath,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=conditionalTransportTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.0=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-security:4.0.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-test:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-web:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter-websocket:4.0.0=conditionalTransportTestRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,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=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-test:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-tomcat:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,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,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-web-server:4.0.0=conditionalTransportTestRuntimeClasspath,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=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-validation:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-web-server:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-webmvc-test:4.0.0=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-webmvc:4.0.0=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot-websocket:4.0.0=conditionalTransportTestRuntimeClasspath
|
||||
org.springframework.boot:spring-boot:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.boot:spring-boot:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.cloud:spring-cloud-context:4.1.4=sampleOffTestCompileClasspath,testCompileClasspath
|
||||
org.springframework.data:spring-data-commons:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.data:spring-data-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.graphql:spring-graphql:2.0.0=conditionalTransportTestRuntimeClasspath
|
||||
org.springframework.integration:spring-integration-core:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.integration:spring-integration-jdbc:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-config:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-core:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-config:7.0.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-core:7.0.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-client:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-jose:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-jose:7.0.0=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-test:7.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-web:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.session:spring-session-core:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aop:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.security:spring-security-web:7.0.0=compileClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework.session:spring-session-core:4.0.0=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aop:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-aspects:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-beans:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-context:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-core:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-expression:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-beans:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-context:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-core:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-expression:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-jdbc:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-messaging:7.0.1=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
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-test:7.0.1=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-tx:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-web:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webflux:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webmvc:7.0.1=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-web:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webflux:7.0.1=functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-webmvc:7.0.1=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.springframework:spring-websocket:7.0.1=conditionalTransportTestRuntimeClasspath,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
|
||||
@@ -367,10 +367,10 @@ org.testcontainers:testcontainers-junit-jupiter:2.0.2=sampleOffTestCompileClassp
|
||||
org.testcontainers:testcontainers-postgresql:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.testcontainers:testcontainers:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
|
||||
org.xmlunit:xmlunit-core:2.10.4=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.yaml:snakeyaml:2.5=compileClasspath,conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.xmlunit:xmlunit-core:2.10.4=functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
org.yaml:snakeyaml:2.5=compileClasspath,conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
redis.clients.authentication:redis-authx-core:0.1.1-beta2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-core:3.0.2=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-databind:3.0.2=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson:jackson-bom:3.0.2=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-core:3.0.2=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson.core:jackson-databind:3.0.2=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
tools.jackson:jackson-bom:3.0.2=conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
empty=developmentOnly,testAndDevelopmentOnly
|
||||
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
package dev.caskeleton.bootstrap.contract;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
|
||||
import dev.caskeleton.adapter.inbound.web.settings.SecuritySettings;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.studio.controller.StudioCatalogController;
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.studio.controller.StudioSessionController;
|
||||
import dev.caskeleton.application.techlog.studio.port.out.CatalogQueryPort;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryType;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogPageView;
|
||||
import dev.caskeleton.application.techlog.studio.service.ListCatalogUseCase;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.bootstrap.contract.support.RepositoryContractResources;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* feature-techlog-studio-backend Task 10 — the drift gate spec §5.5 calls for: springdoc's
|
||||
* published {@code /v3/api-docs} is diffed against the vendored {@code
|
||||
* config/openapi/studio-v1.yaml} for every {@code /api/v1/studio/**} path it actually exposes.
|
||||
* Only <em>implemented</em> operations are checked (direction is "published ⊆ contract", never the
|
||||
* reverse), so this stays green as slices 2-5 add the other 17 operations — <b>on one condition</b>:
|
||||
* the new controllers must live somewhere under {@code dev.caskeleton.adapter.inbound.web.techlog},
|
||||
* the package {@link ContractSurface.ContractSurfaceApp} and {@link EnvelopeWrapping.EnvelopeApp}
|
||||
* {@code @ComponentScan}. A controller placed there is picked up automatically, with no edit to this
|
||||
* file. A controller placed <em>outside</em> that package tree is invisible to both minimal contexts
|
||||
* — springdoc never sees it, so this gate stays green even if its path/method/operationId contradicts
|
||||
* the contract — and the {@code @ComponentScan} base package below must be widened (or the new
|
||||
* controller moved) before this gate can be trusted again. (An earlier draft of this class named the
|
||||
* two controllers directly via {@code @Import} instead of scanning; that hardcoded list had exactly
|
||||
* this blind spot — confirmed by temporarily reintroducing it and observing a controller with an
|
||||
* out-of-contract mapping pass silently, see task-10-report.md.) This test also fails the moment an
|
||||
* in-scan controller's method name drifts from its {@code operationId} or ships an endpoint outside
|
||||
* the contract.
|
||||
*
|
||||
* <h2>Why a hand-built minimal context rather than {@code CaSkeletonApplication}</h2>
|
||||
*
|
||||
* <p>This repository's own tests never boot the full app under test: {@code
|
||||
* FileserverRoundTripContractTest} and {@code ActuatorSecurityHttpTest} (both in this module's
|
||||
* {@code src/test}) spell out why — {@code application.yml} resolves ~50 {@code ${...}} 자리표시자
|
||||
* from {@code src/.env} (datasource, OIDC issuer, Redis, messaging, ...), so a full boot drags in
|
||||
* infrastructure a contract-shape test has nothing to say about. This test follows the same
|
||||
* playbook: a throwaway {@code @SpringBootConfiguration} that {@code @ComponentScan}s the studio web
|
||||
* package (so real production controllers like {@link StudioSessionController} and {@link
|
||||
* StudioCatalogController} are picked up the same way the real app's component scan finds them —
|
||||
* see the class-level "why scan, not @Import" note above), with {@link SecurityAutoConfiguration}
|
||||
* excluded and MockMvc filters off ({@code addFilters = false}) — the exclude/addFilters combination
|
||||
* {@code EnvelopeBodyAdviceTest} and {@code NoResourceFoundErrorHandlingTest} (adapter-inbound-web's
|
||||
* own test sourceSet) already use for this class of test.
|
||||
*
|
||||
* <p>Because this functionalTest module depends only on {@code :adapter:inbound:web} and {@code
|
||||
* :application-core} (not {@code :adapter:outbound:persistence-jpa}, cache, or messaging), none of
|
||||
* the DataSource/Flyway/Redis auto-configuration classes are even on the classpath for {@code
|
||||
* @EnableAutoConfiguration} to attempt — there is nothing to exclude for them, unlike {@code
|
||||
* ActuatorSecurityHttpTest}'s explicit JPA/Flyway exclude list.
|
||||
*
|
||||
* <p>{@code @SpringBootTest} (full, unsliced {@code @EnableAutoConfiguration}) is used instead of
|
||||
* {@code @WebMvcTest}: springdoc's own auto-configuration is a third-party {@code
|
||||
* AutoConfiguration.imports} entry, not part of Boot's curated {@code @WebMvcTest} slice allowlist,
|
||||
* so {@code /v3/api-docs} would not be exposed under a sliced test. A full, unsliced context that
|
||||
* only sees two modules' worth of dependencies keeps the cost bounded to "start web MVC + springdoc"
|
||||
* without paying for DB/security infrastructure.
|
||||
*
|
||||
* <h2>Why two nested contexts instead of one</h2>
|
||||
*
|
||||
* <p>The obvious design is one shared {@code @SpringBootTest} context for both tests. That does not
|
||||
* work here, and the reason is worth recording: springdoc's {@code /v3/api-docs} handler
|
||||
* ({@code OpenApiWebMvcResource.openapiJson}) returns raw {@code byte[]} — it serializes the OpenAPI
|
||||
* model itself and hands Spring MVC already-encoded bytes. {@link EnvelopeBodyAdvice#supports}
|
||||
* returns {@code true} unconditionally (by design — it wraps every controller response in the real
|
||||
* app, not just Studio's), so if it is on the classpath of *that* request it rewrites the body from
|
||||
* {@code byte[]} to {@code Envelope<byte[]>} — but Spring MVC picks the {@code HttpMessageConverter}
|
||||
* from the *original* return type before the advice runs, so {@code ByteArrayHttpMessageConverter}
|
||||
* (already selected for {@code byte[]}) is then asked to write an {@code Envelope}, and
|
||||
* {@code writeInternal} throws {@code ClassCastException: Envelope cannot be cast to [B}. This
|
||||
* reproduced with a full stack trace during this task (see task-10-report.md) — it is a real,
|
||||
* pre-existing defect in shared skeleton code ({@code EnvelopeBodyAdvice} is not Studio-owned and
|
||||
* not part of this task's brief), not an artifact of this test's plumbing: any app that boots both
|
||||
* springdoc and {@code EnvelopeBodyAdvice} together and serves {@code /v3/api-docs} unauthenticated
|
||||
* would hit the same crash. Fixing that advice is out of scope for a contract-regression test, so
|
||||
* {@link ContractSurface} boots a context *without* {@link EnvelopeBodyAdvice} (springdoc doesn't
|
||||
* invoke it for anything test 1 checks anyway — introspection is pure reflection over the mapping),
|
||||
* and {@link EnvelopeWrapping} boots a separate context *with* it, hitting
|
||||
* {@link StudioCatalogController} instead, whose {@code CatalogPage} return type is an ordinary POJO
|
||||
* that the same JSON converter handles before and after wrapping.
|
||||
*
|
||||
* <h2>Why {@link ListCatalogUseCase} is real, not mocked</h2>
|
||||
*
|
||||
* <p>{@link StudioCatalogController}'s constructor takes the concrete (non-interface) {@code
|
||||
* ListCatalogUseCase}, so there is no seam to substitute a fake at that boundary. Its own two
|
||||
* constructor collaborators, {@link CatalogQueryPort} and {@link TransactionPort}, <em>are</em>
|
||||
* interfaces (application ports), so this test wires trivial in-memory implementations of those
|
||||
* instead of pulling in a real persistence adapter — springdoc never invokes a controller method to
|
||||
* build {@code /v3/api-docs} (pure reflection over the mapping/return-type shape), and the envelope
|
||||
* test only needs the query to return successfully, not to hold meaningful catalog data.
|
||||
*
|
||||
* <h2>Second test: envelope wrapping, without real DB/auth infrastructure</h2>
|
||||
*
|
||||
* <p>The brief's original template hits the endpoint through {@code TestRestTemplate} against a
|
||||
* fully DB+auth-backed app; this functionalTest sourceSet has neither (confirmed: before this task
|
||||
* it declared only {@code gradleTestKit()} + JUnit + AssertJ, no Spring dependency at all). Rather
|
||||
* than skip the envelope assertion or force real persistence/security infrastructure into a
|
||||
* contract-shape test, {@link EnvelopeWrapping} proves the same regression the brief wants — {@link
|
||||
* EnvelopeBodyAdvice} still wraps {@link StudioCatalogController}'s response — against a minimal
|
||||
* slice. Envelope wrapping is a {@code ResponseBodyAdvice} concern that is orthogonal to persistence
|
||||
* and authentication, so stubbing those out does not weaken what the assertion proves, and no
|
||||
* production security surface changes: {@code SECURITY_PUBLIC_PATHS} is untouched, and this slice
|
||||
* simply never wires a {@code SecurityFilterChain} at all (same as the two adapter-inbound-web
|
||||
* precedents cited above), rather than widening what unauthenticated callers may reach in the real
|
||||
* app.
|
||||
*/
|
||||
class StudioContractDriftTest {
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = ContractSurface.ContractSurfaceApp.class)
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
class ContractSurface {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
/**
|
||||
* "구현된 것만 검사" 방향 고정: 계약(19개 operation)이 아니라 published(springdoc이 실제로 내놓는 것, 지금은 2개)를
|
||||
* 순회한다. 슬라이스 2~5가 나머지 17개를 추가해도 이 순회 방향 덕분에 이 테스트는 그대로 통과한다 — 반대로 순회했다면 미구현 operation마다
|
||||
* 매번 실패했을 것이다.
|
||||
*/
|
||||
@Test
|
||||
void publishedStudioOperationsMatchTheContract() throws Exception {
|
||||
JsonNode contract = readContract();
|
||||
JsonNode published = readPublishedApiDocs();
|
||||
|
||||
List<String> problems = new ArrayList<>();
|
||||
JsonNode publishedPaths = published.path("paths");
|
||||
for (Map.Entry<String, JsonNode> path : publishedPaths.properties()) {
|
||||
if (!path.getKey().startsWith("/api/v1/studio/")) {
|
||||
continue;
|
||||
}
|
||||
JsonNode contractPath = contract.path("paths").path(path.getKey());
|
||||
if (contractPath.isMissingNode()) {
|
||||
problems.add("계약에 없는 path: " + path.getKey());
|
||||
continue;
|
||||
}
|
||||
for (Map.Entry<String, JsonNode> method : path.getValue().properties()) {
|
||||
JsonNode contractOp = contractPath.path(method.getKey());
|
||||
if (contractOp.isMissingNode()) {
|
||||
problems.add("계약에 없는 method: " + method.getKey() + " " + path.getKey());
|
||||
continue;
|
||||
}
|
||||
String publishedId = method.getValue().path("operationId").asText("");
|
||||
String contractId = contractOp.path("operationId").asText("");
|
||||
if (!publishedId.equals(contractId)) {
|
||||
problems.add(
|
||||
"operationId 불일치 "
|
||||
+ method.getKey()
|
||||
+ " "
|
||||
+ path.getKey()
|
||||
+ ": published="
|
||||
+ publishedId
|
||||
+ " contract="
|
||||
+ contractId);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(problems).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* SnakeYaml(이미 {@code StudioErrorRegistryTest}가 error-codes.yaml에 쓰는 라이브러리)로 읽은 뒤 {@code
|
||||
* ObjectMapper#valueToTree}로 {@link JsonNode}로 옮긴다 — {@code jackson-dataformat-yaml}을 새 컴파일
|
||||
* 의존으로 끌어오지 않고 기존 라이브러리 조합만으로 브리프 템플릿과 같은 {@code JsonNode} 기반 대조 로직을 쓸 수 있다({@code
|
||||
* jackson-dataformat-yaml}은 이 모듈의 {@code compileClasspath}가 아니라 {@code runtimeClasspath}에만
|
||||
* 전이적으로 있었다 — springdoc이 YAML 응답을 만들 때만 필요해서다).
|
||||
*/
|
||||
private static JsonNode readContract() throws Exception {
|
||||
Path contractFile =
|
||||
RepositoryContractResources.fromSystemProperty()
|
||||
.requireTrackedFile("src/config/openapi/studio-v1.yaml");
|
||||
Map<String, Object> contractYaml;
|
||||
try (InputStream in = Files.newInputStream(contractFile)) {
|
||||
contractYaml = new Yaml().load(in);
|
||||
}
|
||||
return new ObjectMapper().valueToTree(contractYaml);
|
||||
}
|
||||
|
||||
private JsonNode readPublishedApiDocs() throws Exception {
|
||||
String body =
|
||||
mvc.perform(get("/v3/api-docs"))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
return new ObjectMapper().readTree(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code @Import}로 컨트롤러를 하나씩 나열하지 않고 {@code @ComponentScan}으로 studio web 패키지 전체를 스캔한다.
|
||||
* 나열 방식은 슬라이스 2~5가 새 컨트롤러를 추가해도 이 파일을 고치지 않는 한 published 표면에 안 잡히는 채로 green을 유지하는
|
||||
* 함정이 있었다 — 드리프트가 "없는" 게 아니라 게이트가 "못 보는" 상태였다. 리뷰에서 이 패키지 아래에 계약 밖 매핑을 가진 임시
|
||||
* 컨트롤러를 하나 추가해(어떤 {@code @Import}/{@code @ComponentScan} 목록에도 안 넣고) 실측으로 확인했다 — 옛 {@code
|
||||
* @Import} 목록으로는 이 테스트가 통과, 이 {@code @ComponentScan}으로는 실패. 재현 절차와 두 결과 모두
|
||||
* task-10-report.md의 "자동 포함 성질 RED 검증" 절에 남아 있다. {@code EnvelopeBodyAdvice}는 이 패키지 밖({@code
|
||||
* ...web.envelope})이라 스캔에 안 걸린다 — 일부러 두지 않는다(클래스 javadoc "Why two nested contexts" 참조).
|
||||
* springdoc은 리플렉션만 하므로 이 test1엔 애초에 관여하지 않는다.
|
||||
*/
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
|
||||
@ComponentScan("dev.caskeleton.adapter.inbound.web.techlog")
|
||||
static class ContractSurfaceApp {
|
||||
|
||||
@Bean
|
||||
SecuritySettings securitySettings() {
|
||||
return StudioContractDriftTest.securitySettingsForTest();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ListCatalogUseCase listCatalogUseCase() {
|
||||
return StudioContractDriftTest.listCatalogUseCaseForTest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(classes = EnvelopeWrapping.EnvelopeApp.class)
|
||||
@AutoConfigureMockMvc(addFilters = false)
|
||||
class EnvelopeWrapping {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void everyStudioResponseIsWrappedInTheEnvelope() throws Exception {
|
||||
String body =
|
||||
mvc.perform(get("/api/v1/studio/catalog").param("type", "TOPIC"))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
assertThat(body).contains("\"success\"").contains("\"data\"").contains("\"meta\"");
|
||||
assertThat(body).doesNotContain("\"data\":{\"success\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code adapter-inbound-web}에 실제 애플리케이션이 없어({@code CaSkeletonApplication}은 app-bootstrap
|
||||
* 소유) {@code @SpringBootTest}가 부트스트랩할 {@code @SpringBootConfiguration}이 필요하다 — {@code
|
||||
* StudioSessionEnvelopeTest}(adapter-inbound-web 자체 테스트)가 쓰는 것과 같은 이유의 같은 패턴. {@link
|
||||
* ContractSurface.ContractSurfaceApp}과 마찬가지로 {@code @ComponentScan}으로 studio web 패키지를 스캔하고,
|
||||
* {@link EnvelopeBodyAdvice}만 별도로 {@code @Import}한다(스캔 범위 밖 패키지라서) — 이 컨텍스트는 {@code
|
||||
* /v3/api-docs}를 두드리지 않으므로 클래스 javadoc이 설명하는 {@code byte[]} 크래시를 겪지 않는다.
|
||||
*/
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
|
||||
@ComponentScan("dev.caskeleton.adapter.inbound.web.techlog")
|
||||
@Import(EnvelopeBodyAdvice.class)
|
||||
static class EnvelopeApp {
|
||||
|
||||
@Bean
|
||||
SecuritySettings securitySettings() {
|
||||
return StudioContractDriftTest.securitySettingsForTest();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ListCatalogUseCase listCatalogUseCase() {
|
||||
return StudioContractDriftTest.listCatalogUseCaseForTest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** {@code csrfHeaderName}이 계약 const {@code X-CSRF-TOKEN}과 다르면 {@link StudioSessionController}의 생성자가 즉시 실패한다. */
|
||||
private static SecuritySettings securitySettingsForTest() {
|
||||
SecuritySettings.SessionCookieSettings session =
|
||||
new SecuritySettings.SessionCookieSettings(
|
||||
null, null, null, null, null, null, "X-CSRF-TOKEN");
|
||||
return new SecuritySettings(
|
||||
SecuritySettings.AuthenticationMode.JWT, "https://issuer.example", null, List.of(), session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 영속성 어댑터 대신 최소 stub 포트로 구성한 진짜 {@link ListCatalogUseCase}. springdoc은 컨트롤러 메서드를 호출하지 않고
|
||||
* 리플렉션만 하므로 첫 번째 테스트에는 아예 관여하지 않고, 두 번째 테스트(봉투 확인)는 결과 내용이 아니라 감싸는 모양만 보므로 빈 목록으로 충분하다.
|
||||
*/
|
||||
private static ListCatalogUseCase listCatalogUseCaseForTest() {
|
||||
return new ListCatalogUseCase(new StubCatalogQueryPort(), new PassThroughTransactionPort());
|
||||
}
|
||||
|
||||
private static final class StubCatalogQueryPort implements CatalogQueryPort {
|
||||
@Override
|
||||
public CatalogPageView search(CatalogEntryType type, String query, String cursor, int limit) {
|
||||
return new CatalogPageView(List.of(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/** 실제 트랜잭션 관리자 없이 액션을 곧장 실행한다 — 이 슬라이스에는 커밋/롤백할 트랜잭션 리소스가 없다. */
|
||||
private static final class PassThroughTransactionPort implements TransactionPort {
|
||||
@Override
|
||||
public <T> T inWrite(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inRootWrite(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inRead(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inNew(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package dev.caskeleton.bootstrap.techlog;
|
||||
|
||||
import dev.caskeleton.application.techlog.studio.port.out.CatalogQueryPort;
|
||||
import dev.caskeleton.application.techlog.studio.service.ListCatalogUseCase;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/** Tech Log Studio 조립. application-core는 Spring을 보지 않으므로 여기서 배선한다. */
|
||||
@Configuration
|
||||
public class TechLogStudioConfig {
|
||||
|
||||
@Bean
|
||||
ListCatalogUseCase listCatalogUseCase(
|
||||
CatalogQueryPort catalogQueryPort, TransactionPort transactionPort) {
|
||||
return new ListCatalogUseCase(catalogQueryPort, transactionPort);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,11 @@
|
||||
# a value this file can know. What belongs here is the shape dev must have whatever the operator
|
||||
# sets: the vendor and the schema owner.
|
||||
#
|
||||
# Both keys below restate the repository default rather than change it, so adding this file moves
|
||||
# no behaviour. That is the point — the moment dev and prod diverge from local, the difference has
|
||||
# a declared home instead of being implied by whatever the environment happened to inject.
|
||||
# The flyway/persistence keys below restate the repository default rather than change it, so they
|
||||
# move no behaviour on their own. That is the point — the moment dev and prod diverge from local,
|
||||
# the difference has a declared home instead of being implied by whatever the environment happened
|
||||
# to inject. The security.session key further down is the one exception: it genuinely overrides the
|
||||
# template default for Tech Log Studio's contract. See the comment there for why.
|
||||
# =============================================================================
|
||||
|
||||
spring:
|
||||
@@ -20,3 +22,18 @@ spring:
|
||||
ca-skeleton:
|
||||
persistence:
|
||||
vendor: postgresql
|
||||
security:
|
||||
# Studio's contract (studio-v1.yaml StudioSession.csrfHeaderName) fixes this header name as a
|
||||
# `const`. The template default is X-XSRF-TOKEN; Studio needs X-CSRF-TOKEN to match.
|
||||
#
|
||||
# auth-mode intentionally stays at the repository default (jwt) here. Task 8's brief proposed
|
||||
# switching it to redis-session, but src/app-bootstrap's AuthenticationModeCompositionConfig
|
||||
# requires a complete Redis session repository/filter pair (`redisVersionedSessionRepository`,
|
||||
# `springSessionRepositoryFilter`) once that mode is active, and neither bean exists in this
|
||||
# branch yet — the Redis session infrastructure is out of this task's scope (its design package
|
||||
# was removed from the working tree ahead of this task; see AGENTS.md / task-8 report). Setting
|
||||
# auth-mode: redis-session here would make a real `--spring.profiles.active=dev` boot fail the
|
||||
# composition validator with "Redis Session repository/filter is incomplete". This csrf-header-
|
||||
# name override is independent of auth-mode and safe on its own.
|
||||
session:
|
||||
csrf-header-name: X-CSRF-TOKEN
|
||||
|
||||
@@ -143,6 +143,15 @@ ca-skeleton:
|
||||
issuer-uri: http://localhost:8081/realms/ca-skeleton
|
||||
audience: ca-skeleton-api
|
||||
public-paths: /api/healthcheck
|
||||
# Studio's contract (studio-v1.yaml StudioSession.csrfHeaderName) fixes this header name as a
|
||||
# `const`. The template default is X-XSRF-TOKEN (application.yml:498, restated verbatim by
|
||||
# src/.env:125, the profile src/.env:8 activates) — StudioSessionController's constructor
|
||||
# rejects any other value with IllegalStateException, and because that controller is an
|
||||
# unconditional @RestController bean, the failure is a BeanCreationException that fails context
|
||||
# refresh and kills the whole process, not just Studio. See application-dev.yml's identical
|
||||
# override for the full rationale.
|
||||
session:
|
||||
csrf-header-name: X-CSRF-TOKEN
|
||||
cors:
|
||||
enabled: true
|
||||
allowed-origins: http://localhost:3000
|
||||
|
||||
@@ -26,3 +26,13 @@ spring:
|
||||
ca-skeleton:
|
||||
persistence:
|
||||
vendor: postgresql
|
||||
security:
|
||||
# Studio's contract (studio-v1.yaml StudioSession.csrfHeaderName) fixes this header name as a
|
||||
# `const`. The template default is X-XSRF-TOKEN; Studio needs X-CSRF-TOKEN to match.
|
||||
# StudioSessionController's constructor rejects any other configured value with
|
||||
# IllegalStateException, and because that controller is an unconditional @RestController bean,
|
||||
# the failure is a BeanCreationException that fails context refresh and kills the whole
|
||||
# process on this profile, not just Studio. See application-dev.yml's identical override for
|
||||
# the full rationale.
|
||||
session:
|
||||
csrf-header-name: X-CSRF-TOKEN
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package dev.caskeleton.bootstrap.architecture;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.adapter.inbound.web.techlog.StudioClientSafeMessages;
|
||||
import dev.caskeleton.application.techlog.error.StudioError;
|
||||
import dev.caskeleton.bootstrap.contract.support.RepositoryContractResources;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* feature-techlog-studio-backend — pins {@link StudioError} against {@code
|
||||
* docs/registries/error-codes.yaml} (the SSOT the contract and log tooling read) on three axes:
|
||||
*
|
||||
* <ol>
|
||||
* <li>every {@link StudioError} constant has a matching registry row (code presence);
|
||||
* <li>that row's {@code category}/{@code http_status}/{@code retryable} match the enum's
|
||||
* declared values exactly — a standing drift gate. Nothing else in the suite checks this for
|
||||
* {@link StudioError}: {@code ErrorCodeRegistryMappingTest} (a different branch's template)
|
||||
* only walks {@code OperationalError}. Its absence let the {@code VALIDATION_FAILED} ↔
|
||||
* {@code OperationalError.VALIDATION_FAILED} code-name collision ship once already (see
|
||||
* task-5-report.md Fix Round 1) — this closes that gap for good;
|
||||
* <li>{@link StudioClientSafeMessages#forError(StudioError)}'s text matches the row's {@code
|
||||
* client_safe_message} exactly — the single source of truth for {@code error.message} is the
|
||||
* registry, and code drifting from it must fail here rather than silently changing what
|
||||
* clients see.
|
||||
* </ol>
|
||||
*
|
||||
* <p>Uses {@link RepositoryContractResources} (the same repository-root resolver the sibling
|
||||
* registry contract tests in {@code dev.caskeleton.bootstrap.contract} use) rather than a
|
||||
* hand-rolled relative {@code Path.of("..", "..", ...)}, since Gradle's test working directory is
|
||||
* not guaranteed to be the module directory the brief's naive relative path assumed. Parses the
|
||||
* registry with SnakeYaml — the same library/pattern {@code ErrorCodeRegistryMappingTest} and
|
||||
* {@code RunbookCoverageContractTest} already use in this suite — rather than line-scanning, since
|
||||
* this test needs structured field access (category/http_status/retryable/client_safe_message),
|
||||
* not just the {@code code:} key.
|
||||
*/
|
||||
class StudioErrorRegistryTest {
|
||||
|
||||
private static Map<String, Map<String, Object>> registryRowsByCode;
|
||||
|
||||
@BeforeAll
|
||||
@SuppressWarnings("unchecked")
|
||||
static void loadRegistry() throws Exception {
|
||||
Path registry =
|
||||
RepositoryContractResources.fromSystemProperty()
|
||||
.requireTrackedFile("docs/registries/error-codes.yaml");
|
||||
registryRowsByCode = new LinkedHashMap<>();
|
||||
try (InputStream in = Files.newInputStream(registry)) {
|
||||
Map<String, Object> root = new Yaml().load(in);
|
||||
List<Map<String, Object>> errors = (List<Map<String, Object>>) root.get("errors");
|
||||
for (Map<String, Object> row : errors) {
|
||||
registryRowsByCode.put((String) row.get("code"), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyStudioErrorHasARegistryRow() {
|
||||
Set<String> declared =
|
||||
Arrays.stream(StudioError.values()).map(StudioError::code).collect(Collectors.toSet());
|
||||
|
||||
assertThat(registryRowsByCode.keySet()).containsAll(declared);
|
||||
}
|
||||
|
||||
/**
|
||||
* The value-drift gate: every {@link StudioError}'s registry row must carry the exact same
|
||||
* category/http_status/retryable the enum declares. {@code PAYLOAD_TOO_LARGE}/{@code
|
||||
* UNSUPPORTED_MEDIA_TYPE} reuse a pre-existing {@code feature-api-contract-baseline} row instead
|
||||
* of a Studio-owned one (task-5-report.md §5) — this still holds them to the same standard.
|
||||
*/
|
||||
@Test
|
||||
void everyStudioErrorRowMatchesCategoryHttpStatusAndRetryable() {
|
||||
for (StudioError error : StudioError.values()) {
|
||||
Map<String, Object> row = registryRowsByCode.get(error.code());
|
||||
assertThat(row).as("registry row for %s", error.code()).isNotNull();
|
||||
|
||||
assertThat(row.get("category"))
|
||||
.as("category for %s", error.code())
|
||||
.isEqualTo(error.category().name());
|
||||
assertThat(((Number) row.get("http_status")).intValue())
|
||||
.as("http_status for %s", error.code())
|
||||
.isEqualTo(error.httpStatus());
|
||||
assertThat(row.get("retryable"))
|
||||
.as("retryable for %s", error.code())
|
||||
.isEqualTo(error.retryable());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code StudioClientSafeMessages} must never drift from the registry's {@code
|
||||
* client_safe_message} — that column is the single source of truth for what {@code
|
||||
* error.message} clients see (task-5-report.md Important 1).
|
||||
*/
|
||||
@Test
|
||||
void everyStudioErrorClientSafeMessageMatchesRegistry() {
|
||||
for (StudioError error : StudioError.values()) {
|
||||
Map<String, Object> row = registryRowsByCode.get(error.code());
|
||||
assertThat(row).as("registry row for %s", error.code()).isNotNull();
|
||||
|
||||
assertThat(StudioClientSafeMessages.forError(error))
|
||||
.as("client_safe_message for %s", error.code())
|
||||
.isEqualTo(row.get("client_safe_message"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* final whole-branch review B3: {@code StudioErrorTest.declaresExactlyTheTwentyThreeContractCodes}
|
||||
* only counts ({@code hasSize(23)}) — it never reads the contract, so a rename or a 1:1 code
|
||||
* substitution on either side (enum or {@code studio-v1.yaml}) leaves the count at 23 and passes.
|
||||
* This is the gate that reads {@code src/config/openapi/studio-v1.yaml}'s {@code
|
||||
* components.schemas.ApiError.properties.code.enum} and requires the two sets to be identical in
|
||||
* both directions — a code present only in the contract, or only in the enum, fails here. This
|
||||
* drift already happened once for real (Task 5's vendor copy carrying stale names) and a human
|
||||
* caught it, not a gate; this closes that gap.
|
||||
*/
|
||||
@Test
|
||||
void enumMatchesContractCodeSetExactly() throws Exception {
|
||||
Path contract =
|
||||
RepositoryContractResources.fromSystemProperty()
|
||||
.requireTrackedFile("src/config/openapi/studio-v1.yaml");
|
||||
Set<String> contractCodes = contractApiErrorCodes(contract);
|
||||
|
||||
Set<String> enumCodes =
|
||||
Arrays.stream(StudioError.values()).map(StudioError::code).collect(Collectors.toSet());
|
||||
|
||||
assertThat(enumCodes)
|
||||
.as("StudioError enum vs studio-v1.yaml ApiError.code enum")
|
||||
.containsExactlyInAnyOrderElementsOf(contractCodes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Set<String> contractApiErrorCodes(Path contract) throws IOException {
|
||||
try (InputStream in = Files.newInputStream(contract)) {
|
||||
Map<String, Object> root = new Yaml().load(in);
|
||||
Map<String, Object> components = (Map<String, Object>) root.get("components");
|
||||
Map<String, Object> schemas = (Map<String, Object>) components.get("schemas");
|
||||
Map<String, Object> apiError = (Map<String, Object>) schemas.get("ApiError");
|
||||
Map<String, Object> properties = (Map<String, Object>) apiError.get("properties");
|
||||
Map<String, Object> code = (Map<String, Object>) properties.get("code");
|
||||
List<String> enumValues = (List<String>) code.get("enum");
|
||||
return Set.copyOf(enumValues);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* final whole-branch review B3: {@code src/config/openapi/studio-v1.yaml} is a vendored copy of
|
||||
* the design package's contract (MANIFEST.sha256's {@code # source:} line records where from) and
|
||||
* had zero consumers before this test — nothing detected a local edit to the vendor copy drifting
|
||||
* from the hash the manifest recorded at vendoring time. This makes {@code MANIFEST.sha256} an
|
||||
* actual tamper/drift gate rather than a file nobody reads.
|
||||
*/
|
||||
@Test
|
||||
void vendoredContractMatchesTheRecordedManifestHash() throws Exception {
|
||||
RepositoryContractResources resources = RepositoryContractResources.fromSystemProperty();
|
||||
Path contract = resources.requireTrackedFile("src/config/openapi/studio-v1.yaml");
|
||||
Path manifest = resources.requireTrackedFile("src/config/openapi/MANIFEST.sha256");
|
||||
|
||||
String recordedHash = recordedSha256(manifest, "studio-v1.yaml");
|
||||
String actualHash = sha256Hex(contract);
|
||||
|
||||
assertThat(actualHash)
|
||||
.as(
|
||||
"src/config/openapi/studio-v1.yaml sha256 must match the value MANIFEST.sha256 recorded"
|
||||
+ " for it (local edit or vendoring drift)")
|
||||
.isEqualTo(recordedHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses lines shaped {@code <hex sha256> <filename>}, skipping {@code #}-prefixed comment
|
||||
* lines such as MANIFEST.sha256's {@code # source: ...} provenance line.
|
||||
*/
|
||||
private static String recordedSha256(Path manifest, String filename) throws IOException {
|
||||
return Files.readAllLines(manifest).stream()
|
||||
.map(String::strip)
|
||||
.filter(line -> !line.isEmpty() && !line.startsWith("#"))
|
||||
.filter(line -> line.endsWith(filename))
|
||||
.map(line -> line.substring(0, line.indexOf(' ')).strip())
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new IllegalStateException(
|
||||
"MANIFEST.sha256 has no hash row for " + filename + ": " + manifest));
|
||||
}
|
||||
|
||||
private static String sha256Hex(Path file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(Files.readAllBytes(file));
|
||||
StringBuilder hex = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package dev.caskeleton.bootstrap.architecture;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
|
||||
/**
|
||||
* 설계 08장의 bounded context 경계를 빌드로 강제한다. Gradle leaf를 늘리지 않고
|
||||
* 패키지로 나눴으므로(spec D1/D2) 경계는 이 규칙이 유일한 방어선이다.
|
||||
*/
|
||||
@AnalyzeClasses(packages = "dev.caskeleton", importOptions = ProductionClassImportOption.class)
|
||||
class TechLogBoundaryArchTest {
|
||||
|
||||
// 새 techlog context를 추가할 때 손봐야 할 지점 (fix round 1에서 asset/publication 규칙이
|
||||
// 계획에서 통째로 빠졌던 것이 바로 이 체크리스트를 세워두지 않아서였다 — spec §4.3이 규칙
|
||||
// 개수·내용의 원본이고, 이 클래스는 그것의 실행 가능한 사본일 뿐이다):
|
||||
// (a) 그 context 전용 형제 비의존 규칙(XXX_DOES_NOT_DEPEND_ON_SIBLING_CONTEXTS)을 새로
|
||||
// 추가한다.
|
||||
// (b) 기존 형제 규칙들(CONTENT/INQUIRY/PROJECT/ASSET_DOES_NOT_DEPEND_ON_SIBLING_CONTEXTS)의
|
||||
// dependOnClassesThat().resideInAnyPackage(...) 금지 목록에 새 context를 추가한다.
|
||||
// (c) STUDIO_FACADE_ONLY_TOUCHES_INBOUND_PORTS의 금지 목록(service/port.out)에 새
|
||||
// context의 service·port.out 패키지를 추가한다.
|
||||
// (d) NO_CONTEXT_DEPENDS_ON_STUDIO_FACADE의 대상 목록(that().resideInAnyPackage(...))에
|
||||
// 새 context를 추가한다.
|
||||
// 새 context가 도메인 엔터티를 갖고 다른 context가 그것을 직접 변조하면 안 되는 경우
|
||||
// (publication과 같은 성격) NO_CONTEXT_DEPENDS_ON_PUBLICATION_DOMAIN과 같은 모양의
|
||||
// 전용 규칙도 검토한다.
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule CONTENT_DOES_NOT_DEPEND_ON_SIBLING_CONTEXTS =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("..techlog.content..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("..techlog.inquiry..", "..techlog.project..", "..techlog.asset..")
|
||||
.as("techlog.content는 형제 context에 의존하지 않는다")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule INQUIRY_DOES_NOT_DEPEND_ON_SIBLING_CONTEXTS =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("..techlog.inquiry..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("..techlog.content..", "..techlog.project..", "..techlog.asset..")
|
||||
.as("techlog.inquiry는 형제 context에 의존하지 않는다")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule PROJECT_DOES_NOT_DEPEND_ON_SIBLING_CONTEXTS =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("..techlog.project..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("..techlog.content..", "..techlog.inquiry..", "..techlog.asset..")
|
||||
.as("techlog.project는 형제 context에 의존하지 않는다")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule ASSET_DOES_NOT_DEPEND_ON_SIBLING_CONTEXTS =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("..techlog.asset..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("..techlog.content..", "..techlog.inquiry..", "..techlog.project..")
|
||||
.as(
|
||||
"spec §4.3 규칙1 ASSET_DOES_NOT_DEPEND_ON_SIBLING_CONTEXTS: techlog.asset는 "
|
||||
+ "형제 context(content/inquiry/project)에 의존하지 않는다 — fix round 1: "
|
||||
+ "content/inquiry/project 세 방향은 이미 asset을 형제 금지 목록에 넣어 막고 "
|
||||
+ "있었지만 asset 자신이 형제를 참조하는 반대 방향이 계획에서 빠져 있었다")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule STUDIO_FACADE_ONLY_TOUCHES_INBOUND_PORTS =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("..application.techlog.studio..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage(
|
||||
"..application.techlog.content.service..",
|
||||
"..application.techlog.content.port.out..",
|
||||
"..application.techlog.inquiry.service..",
|
||||
"..application.techlog.inquiry.port.out..",
|
||||
"..application.techlog.project.service..",
|
||||
"..application.techlog.project.port.out..",
|
||||
"..application.techlog.asset.service..",
|
||||
"..application.techlog.asset.port.out..",
|
||||
"..application.techlog.publication.service..",
|
||||
"..application.techlog.publication.port.out..",
|
||||
"..domain.techlog..")
|
||||
.as("studio facade는 타 context의 port.in만 호출한다 (domain·service·port.out 직접 접근 금지)")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_CONTEXT_DEPENDS_ON_STUDIO_FACADE =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAnyPackage(
|
||||
"..techlog.content..",
|
||||
"..techlog.inquiry..",
|
||||
"..techlog.project..",
|
||||
"..techlog.asset..",
|
||||
"..techlog.publication..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("..application.techlog.studio..")
|
||||
.as("도메인 context는 studio facade에 역방향 의존하지 않는다")
|
||||
.allowEmptyShould(true);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_CONTEXT_DEPENDS_ON_PUBLICATION_DOMAIN =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAnyPackage(
|
||||
"..domain.techlog.content..",
|
||||
"..domain.techlog.inquiry..",
|
||||
"..domain.techlog.project..",
|
||||
"..domain.techlog.asset..",
|
||||
"..domain.techlog.identity..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("..domain.techlog.publication..")
|
||||
.as(
|
||||
"spec §4.3 규칙4 NO_CONTEXT_DEPENDS_ON_PUBLICATION_DOMAIN: "
|
||||
+ "domain.techlog.publication을 제외한 어떤 domain 패키지도 Publication을 "
|
||||
+ "직접 변경하지 않는다. ArchUnit은 '직접 변경'이라는 동작을 정적으로 표현할 "
|
||||
+ "수 없으므로, 타 domain context가 publication 도메인 패키지에 의존하는 것 "
|
||||
+ "자체를 금지하는 보수적 근사로 대신한다 — 위 형제 규칙들(techlog.content/"
|
||||
+ "inquiry/project/asset)도 전면 금지이므로 일관된 강도다. PublicationStatus "
|
||||
+ "같은 타입을 타 context가 읽어야 하는 정당한 필요가 생기면 그 타입을 공유 "
|
||||
+ "위치로 옮기거나 port로 노출하는 것이 옳은 해법이지 이 경계를 뚫는 것이 "
|
||||
+ "아니다")
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package dev.caskeleton.bootstrap.contract;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.SafeConstructor;
|
||||
|
||||
/**
|
||||
* final whole-branch review B1: pins {@code ca-skeleton.security.session.csrf-header-name} to the
|
||||
* contract {@code const} ({@code studio-v1.yaml StudioSession.csrfHeaderName}, config/openapi/
|
||||
* studio-v1.yaml:832) for every profile that can actually boot the process — {@code local}, {@code
|
||||
* dev}, {@code prod}.
|
||||
*
|
||||
* <p>{@code StudioSessionController}'s constructor throws {@code IllegalStateException} when the
|
||||
* configured header name disagrees with the contract const. That controller is an unconditional
|
||||
* {@code @RestController} bean, so the constructor failure becomes a {@code BeanCreationException}
|
||||
* during context refresh — the process never starts, taking healthcheck/actuator/fileserver down
|
||||
* with it. Only {@code application-dev.yml} declared the override before this fix;
|
||||
* {@code application-local.yml} (the profile {@code src/.env:8} actually activates) and
|
||||
* {@code application-prod.yml} both resolved to the template default {@code X-XSRF-TOKEN}
|
||||
* (application.yml:498's inline default, restated verbatim by {@code src/.env:125}), so both
|
||||
* profiles could not boot.
|
||||
*
|
||||
* <p>File-assertion contract test rather than a booted context, for the same reason {@link
|
||||
* ProfileSeparationContractTest} gives: booting the real composition root is not possible in this
|
||||
* source set (see that class's javadoc). This test follows its pattern and location.
|
||||
*/
|
||||
class StudioSessionCsrfHeaderProfileContractTest {
|
||||
|
||||
private static final Path REPOSITORY_ROOT = repositoryRoot();
|
||||
|
||||
/** studio-v1.yaml {@code StudioSession.csrfHeaderName} — config/openapi/studio-v1.yaml:832. */
|
||||
private static final String CONTRACT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"local", "dev", "prod"})
|
||||
void everyBootableProfileResolvesTheContractCsrfHeaderName(String profile) throws IOException {
|
||||
assertThat(csrfHeaderNameOf(profile(profile)))
|
||||
.as(
|
||||
"application-%s.yml must set ca-skeleton.security.session.csrf-header-name to \"%s\""
|
||||
+ " (studio-v1.yaml StudioSession.csrfHeaderName const) — otherwise"
|
||||
+ " StudioSessionController's constructor throws IllegalStateException and the"
|
||||
+ " whole process fails to boot on this profile",
|
||||
profile, CONTRACT_CSRF_HEADER_NAME)
|
||||
.isEqualTo(CONTRACT_CSRF_HEADER_NAME);
|
||||
}
|
||||
|
||||
private static String csrfHeaderNameOf(Map<?, ?> configuration) {
|
||||
Map<?, ?> session =
|
||||
child(child(child(configuration, "ca-skeleton"), "security"), "session");
|
||||
Object value = session == null ? null : session.get("csrf-header-name");
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private static Map<?, ?> child(Map<?, ?> owner, String key) {
|
||||
if (owner == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = owner.get(key);
|
||||
return value instanceof Map<?, ?> map ? map : null;
|
||||
}
|
||||
|
||||
private static Map<?, ?> profile(String profile) throws IOException {
|
||||
return yaml("application-" + profile + ".yml");
|
||||
}
|
||||
|
||||
private static Map<?, ?> yaml(String resource) throws IOException {
|
||||
String source =
|
||||
Files.readString(
|
||||
REPOSITORY_ROOT.resolve("src/app-bootstrap/src/main/resources").resolve(resource));
|
||||
LoaderOptions options = new LoaderOptions();
|
||||
options.setAllowDuplicateKeys(false);
|
||||
Object loaded = new Yaml(new SafeConstructor(options)).load(source);
|
||||
assertThat(loaded).as("%s must parse as a YAML mapping", resource).isInstanceOf(Map.class);
|
||||
return (Map<?, ?>) loaded;
|
||||
}
|
||||
|
||||
private static Path repositoryRoot() {
|
||||
for (Path path = Paths.get("").toAbsolutePath(); path != null; path = path.getParent()) {
|
||||
if (Files.isRegularFile(path.resolve("AGENTS.md"))
|
||||
&& Files.isRegularFile(path.resolve("src/settings.gradle"))) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"repository root not found from " + Paths.get("").toAbsolutePath());
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package dev.caskeleton.application.techlog.error;
|
||||
|
||||
import dev.caskeleton.shared.error.ApiErrorCode;
|
||||
import dev.caskeleton.shared.error.Category;
|
||||
|
||||
/**
|
||||
* Studio 계약(`studio-v1.yaml`)의 `ApiError.code` enum 23종. 계약과 1:1이며 여기서 코드를 늘리거나 줄이면 계약과
|
||||
* `docs/registries/error-codes.yaml`을 함께 고쳐야 한다.
|
||||
*/
|
||||
public enum StudioError implements ApiErrorCode {
|
||||
AUTHENTICATION_REQUIRED(Category.AUTH, 401, false),
|
||||
STUDIO_ACCESS_DENIED(Category.AUTHZ, 403, false),
|
||||
DOCUMENT_NOT_FOUND(Category.NOT_FOUND, 404, false),
|
||||
VERSION_CONFLICT(Category.CONFLICT, 409, false),
|
||||
REQUEST_VALIDATION_FAILED(Category.VALIDATION, 422, false),
|
||||
DOCUMENT_VALIDATION_FAILED(Category.VALIDATION, 422, false),
|
||||
VALIDATION_STALE(Category.CONFLICT, 409, false),
|
||||
PREVIEW_NOT_FOUND(Category.NOT_FOUND, 404, false),
|
||||
PREVIEW_STALE(Category.CONFLICT, 409, false),
|
||||
PREVIEW_EXPIRED(Category.CONFLICT, 409, false),
|
||||
PUBLICATION_NOT_FOUND(Category.NOT_FOUND, 404, false),
|
||||
PUBLICATION_CONFLICT(Category.CONFLICT, 409, false),
|
||||
PUBLICATION_EVENT_NOT_FOUND(Category.NOT_FOUND, 404, false),
|
||||
PUBLICATION_SNAPSHOT_NOT_FOUND(Category.NOT_FOUND, 404, false),
|
||||
WARNING_ACKNOWLEDGEMENT_REQUIRED(Category.VALIDATION, 422, false),
|
||||
IDEMPOTENCY_KEY_REUSED(Category.CONFLICT, 409, false),
|
||||
ASSET_NOT_FOUND(Category.NOT_FOUND, 404, false),
|
||||
ASSET_NOT_READY(Category.CONFLICT, 409, false),
|
||||
ASSET_IN_USE(Category.CONFLICT, 409, false),
|
||||
ASSET_QUARANTINED(Category.DATA_INTEGRITY, 409, false),
|
||||
PAYLOAD_TOO_LARGE(Category.VALIDATION, 413, false),
|
||||
UNSUPPORTED_MEDIA_TYPE(Category.VALIDATION, 415, false),
|
||||
STUDIO_UNAVAILABLE(Category.TRANSIENT_DEPENDENCY, 503, true);
|
||||
|
||||
private final Category category;
|
||||
private final int httpStatus;
|
||||
private final boolean retryable;
|
||||
|
||||
StudioError(Category category, int httpStatus, boolean retryable) {
|
||||
this.category = category;
|
||||
this.httpStatus = httpStatus;
|
||||
this.retryable = retryable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String code() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Category category() {
|
||||
return category;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int httpStatus() {
|
||||
return httpStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retryable() {
|
||||
return retryable;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package dev.caskeleton.application.techlog.error;
|
||||
|
||||
import dev.caskeleton.shared.error.ApiErrorCarrier;
|
||||
import dev.caskeleton.shared.error.ApiErrorCode;
|
||||
|
||||
/**
|
||||
* Studio use case와 facade가 던지는 유일한 실패 표현. 전송 계층은 {@link ApiErrorCarrier}만 보고 봉투로 옮기므로 application이
|
||||
* HTTP를 알 필요가 없다.
|
||||
*
|
||||
* <p>{@code details}는 계약의 {@code ApiError.details}에 그대로 실린다 — {@code VERSION_CONFLICT}면 최신 문서,
|
||||
* {@code PUBLICATION_CONFLICT}면 최신 Publication.
|
||||
*/
|
||||
public final class StudioException extends RuntimeException implements ApiErrorCarrier {
|
||||
|
||||
private final transient StudioError error;
|
||||
private final transient Object details;
|
||||
|
||||
private StudioException(StudioError error, String message, Object details) {
|
||||
super(message);
|
||||
this.error = error;
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public static StudioException of(StudioError error, String message) {
|
||||
return new StudioException(error, message, null);
|
||||
}
|
||||
|
||||
public static StudioException withDetails(StudioError error, String message, Object details) {
|
||||
return new StudioException(error, message, details);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiErrorCode errorCode() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public StudioError studioError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public Object details() {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.studio.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryType;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogPageView;
|
||||
|
||||
/** Studio catalog는 도메인 Aggregate를 재구성하지 않는다. 전용 read 포트로 union query를 돌린다 (설계 08장 §4). */
|
||||
@FunctionalInterface
|
||||
public interface CatalogQueryPort {
|
||||
|
||||
CatalogPageView search(CatalogEntryType type, String query, String cursor, int limit);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package dev.caskeleton.application.techlog.studio.query;
|
||||
|
||||
/** 계약 `CatalogEntryType`과 1:1. API enum이 그대로 application 용어다. */
|
||||
public enum CatalogEntryType {
|
||||
TOPIC,
|
||||
PROJECT,
|
||||
RELATION,
|
||||
EVIDENCE
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.studio.query;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 계약 `CatalogEntry`의 application 표현. {@code kind}와 {@code publicPath}는 TOPIC/PROJECT에는 없으므로 null이다.
|
||||
*/
|
||||
public record CatalogEntryView(
|
||||
UUID id,
|
||||
CatalogEntryType type,
|
||||
String label,
|
||||
String kind,
|
||||
String publicPath,
|
||||
String dependencyRevision) {}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package dev.caskeleton.application.techlog.studio.query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record CatalogPageView(List<CatalogEntryView> items, String nextCursor) {
|
||||
|
||||
public CatalogPageView {
|
||||
items = List.copyOf(items);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package dev.caskeleton.application.techlog.studio.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
public record ListCatalogQuery(CatalogEntryType type, String query, String cursor, int limit)
|
||||
implements Query {}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package dev.caskeleton.application.techlog.studio.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.error.StudioError;
|
||||
import dev.caskeleton.application.techlog.error.StudioException;
|
||||
import dev.caskeleton.application.techlog.studio.port.out.CatalogQueryPort;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogPageView;
|
||||
import dev.caskeleton.application.techlog.studio.query.ListCatalogQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
|
||||
/**
|
||||
* Studio catalog 조회. 도메인 상태를 바꾸지 않으므로 read-only다.
|
||||
*
|
||||
* <p>브리프 원안은 {@code TransactionPort}를 배선하지 않았지만, {@code
|
||||
* CleanArchitectureTest.USE_CASE_CAPABILITY_MATCHES_TRANSACTION_PORT_BOUNDARY}(feature
|
||||
* -domain-feature-onboarding-contract D4)가 READ_REPOSITORY+READ_ONLY 조합에 {@code
|
||||
* TransactionPort.inRead(...)}를 직접 호출하도록 정적으로 강제한다. {@link
|
||||
* dev.caskeleton.application.notification.NotificationOperationsSnapshotUseCase}와 같은 패턴이다.
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ListCatalogUseCase implements QueryUseCase<ListCatalogQuery, CatalogPageView> {
|
||||
|
||||
private static final int MAX_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* studio-v1.yaml {@code components.parameters.Query.schema.maxLength}
|
||||
* (config/openapi/studio-v1.yaml:579).
|
||||
*/
|
||||
private static final int MAX_QUERY_LENGTH = 100;
|
||||
|
||||
/**
|
||||
* studio-v1.yaml {@code components.parameters.Cursor.schema.minLength}
|
||||
* (config/openapi/studio-v1.yaml:580).
|
||||
*/
|
||||
private static final int MIN_CURSOR_LENGTH = 1;
|
||||
|
||||
/**
|
||||
* studio-v1.yaml {@code components.parameters.Cursor.schema.maxLength}
|
||||
* (config/openapi/studio-v1.yaml:580).
|
||||
*/
|
||||
private static final int MAX_CURSOR_LENGTH = 2000;
|
||||
|
||||
private final CatalogQueryPort catalogQueryPort;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ListCatalogUseCase(CatalogQueryPort catalogQueryPort, TransactionPort transactions) {
|
||||
this.catalogQueryPort = catalogQueryPort;
|
||||
this.transactions = transactions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CatalogPageView handle(ListCatalogQuery input) {
|
||||
if (input.type() == null) {
|
||||
throw StudioException.of(StudioError.REQUEST_VALIDATION_FAILED, "type is required");
|
||||
}
|
||||
if (input.limit() < 1 || input.limit() > MAX_LIMIT) {
|
||||
throw StudioException.of(
|
||||
StudioError.REQUEST_VALIDATION_FAILED, "limit must be between 1 and " + MAX_LIMIT);
|
||||
}
|
||||
if (input.query() != null && input.query().length() > MAX_QUERY_LENGTH) {
|
||||
throw StudioException.of(
|
||||
StudioError.REQUEST_VALIDATION_FAILED,
|
||||
"q must be at most " + MAX_QUERY_LENGTH + " characters");
|
||||
}
|
||||
if (input.cursor() != null
|
||||
&& (input.cursor().length() < MIN_CURSOR_LENGTH
|
||||
|| input.cursor().length() > MAX_CURSOR_LENGTH)) {
|
||||
throw StudioException.of(
|
||||
StudioError.REQUEST_VALIDATION_FAILED,
|
||||
"cursor must be between "
|
||||
+ MIN_CURSOR_LENGTH
|
||||
+ " and "
|
||||
+ MAX_CURSOR_LENGTH
|
||||
+ " characters");
|
||||
}
|
||||
return transactions.inRead(
|
||||
() -> catalogQueryPort.search(input.type(), input.query(), input.cursor(), input.limit()));
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package dev.caskeleton.application.techlog.error;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import dev.caskeleton.shared.error.Category;
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StudioErrorTest {
|
||||
|
||||
/**
|
||||
* final whole-branch review B3: renamed from {@code declaresExactlyTheTwentyThreeContractCodes} —
|
||||
* this test never reads {@code studio-v1.yaml}, only counts the enum, so "contract codes" was a
|
||||
* false claim (a rename or 1:1 substitution on either side leaves the count at 23 and this still
|
||||
* passes). The actual contract-vs-enum set-equality gate is {@code
|
||||
* StudioErrorRegistryTest#enumMatchesContractCodeSetExactly}; this test stays as a cheap "did the
|
||||
* count change" tripwire.
|
||||
*/
|
||||
@Test
|
||||
void declaresExactlyTwentyThreeCodes() {
|
||||
assertThat(StudioError.values()).hasSize(23);
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyCodeCarriesACategoryAndAClientFacingStatus() {
|
||||
Arrays.stream(StudioError.values())
|
||||
.forEach(
|
||||
error -> {
|
||||
assertThat(error.code()).matches("[A-Z][A-Z0-9_]*");
|
||||
assertThat(error.category()).isNotNull();
|
||||
assertThat(error.httpStatus()).isBetween(400, 599);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void versionConflictIsAFourZeroNineConflict() {
|
||||
assertThat(StudioError.VERSION_CONFLICT.httpStatus()).isEqualTo(409);
|
||||
assertThat(StudioError.VERSION_CONFLICT.category()).isEqualTo(Category.CONFLICT);
|
||||
assertThat(StudioError.VERSION_CONFLICT.retryable()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void studioUnavailableIsRetryable() {
|
||||
assertThat(StudioError.STUDIO_UNAVAILABLE.httpStatus()).isEqualTo(503);
|
||||
assertThat(StudioError.STUDIO_UNAVAILABLE.category()).isEqualTo(Category.TRANSIENT_DEPENDENCY);
|
||||
assertThat(StudioError.STUDIO_UNAVAILABLE.retryable()).isTrue();
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package dev.caskeleton.application.techlog.studio.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import dev.caskeleton.application.techlog.error.StudioException;
|
||||
import dev.caskeleton.application.techlog.studio.port.out.CatalogQueryPort;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryType;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogEntryView;
|
||||
import dev.caskeleton.application.techlog.studio.query.CatalogPageView;
|
||||
import dev.caskeleton.application.techlog.studio.query.ListCatalogQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ListCatalogUseCaseTest {
|
||||
|
||||
private static CatalogQueryPort portReturning(CatalogPageView page) {
|
||||
return (type, query, cursor, limit) -> page;
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsWhateverThePortFound() {
|
||||
CatalogEntryView entry =
|
||||
new CatalogEntryView(
|
||||
UUID.randomUUID(), CatalogEntryType.TOPIC, "Kafka", null, null, "rev-1");
|
||||
ListCatalogUseCase useCase =
|
||||
new ListCatalogUseCase(
|
||||
portReturning(new CatalogPageView(List.of(entry), null)), new DirectTransactions());
|
||||
|
||||
CatalogPageView page =
|
||||
useCase.handle(new ListCatalogQuery(CatalogEntryType.TOPIC, "ka", null, 20));
|
||||
|
||||
assertThat(page.items()).containsExactly(entry);
|
||||
assertThat(page.nextCursor()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsALimitAboveTheContractCeiling() {
|
||||
ListCatalogUseCase useCase =
|
||||
new ListCatalogUseCase(
|
||||
portReturning(new CatalogPageView(List.of(), null)), new DirectTransactions());
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> useCase.handle(new ListCatalogQuery(CatalogEntryType.TOPIC, null, null, 101)))
|
||||
.isInstanceOf(StudioException.class)
|
||||
.hasMessageContaining("limit");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAMissingType() {
|
||||
ListCatalogUseCase useCase =
|
||||
new ListCatalogUseCase(
|
||||
portReturning(new CatalogPageView(List.of(), null)), new DirectTransactions());
|
||||
|
||||
assertThatThrownBy(() -> useCase.handle(new ListCatalogQuery(null, null, null, 20)))
|
||||
.isInstanceOf(StudioException.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* studio-v1.yaml {@code components.parameters.Query} — {@code schema: { type: string, maxLength:
|
||||
* 100 } } (src/config/openapi/studio-v1.yaml:579).
|
||||
*/
|
||||
@Test
|
||||
void rejectsAQueryLongerThanTheContractCeiling() {
|
||||
ListCatalogUseCase useCase =
|
||||
new ListCatalogUseCase(
|
||||
portReturning(new CatalogPageView(List.of(), null)), new DirectTransactions());
|
||||
String tooLong = "q".repeat(101);
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> useCase.handle(new ListCatalogQuery(CatalogEntryType.TOPIC, tooLong, null, 20)))
|
||||
.isInstanceOf(StudioException.class)
|
||||
.hasMessageContaining("q");
|
||||
}
|
||||
|
||||
/**
|
||||
* studio-v1.yaml {@code components.parameters.Cursor} — {@code schema: { type: string, minLength:
|
||||
* 1, maxLength: 2000 } } (src/config/openapi/studio-v1.yaml:580).
|
||||
*/
|
||||
@Test
|
||||
void rejectsAnEmptyCursor() {
|
||||
ListCatalogUseCase useCase =
|
||||
new ListCatalogUseCase(
|
||||
portReturning(new CatalogPageView(List.of(), null)), new DirectTransactions());
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> useCase.handle(new ListCatalogQuery(CatalogEntryType.TOPIC, null, "", 20)))
|
||||
.isInstanceOf(StudioException.class)
|
||||
.hasMessageContaining("cursor");
|
||||
}
|
||||
|
||||
/** Same contract field as {@link #rejectsAnEmptyCursor()}; upper bound instead of lower. */
|
||||
@Test
|
||||
void rejectsACursorLongerThanTheContractCeiling() {
|
||||
ListCatalogUseCase useCase =
|
||||
new ListCatalogUseCase(
|
||||
portReturning(new CatalogPageView(List.of(), null)), new DirectTransactions());
|
||||
String tooLong = "c".repeat(2001);
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> useCase.handle(new ListCatalogQuery(CatalogEntryType.TOPIC, null, tooLong, 20)))
|
||||
.isInstanceOf(StudioException.class)
|
||||
.hasMessageContaining("cursor");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code CleanArchitectureTest.USE_CASE_CAPABILITY_MATCHES_TRANSACTION_PORT_BOUNDARY}가
|
||||
* READ_REPOSITORY+READ_ONLY use case에 {@code TransactionPort.inRead(...)} 직접 호출을 요구하므로, {@code
|
||||
* ListCatalogUseCase}는 생성자에 {@link TransactionPort}를 받는다. 같은 모양의 fake는 {@code
|
||||
* NotificationOperationsSnapshotUseCaseTest.TrackingTransactions}를 참고했다 — 여기서는 검증 없이 그대로 통과시키기만
|
||||
* 하면 된다.
|
||||
*/
|
||||
private static final class DirectTransactions implements TransactionPort {
|
||||
|
||||
@Override
|
||||
public <T> T inWrite(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inRootWrite(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inRead(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T inNew(Supplier<T> action) {
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ plugins {
|
||||
id 'com.diffplug.spotless' version '8.6.0' apply false // D1 formatter (google-java-format)
|
||||
id 'com.github.spotbugs' version '6.5.6' apply false // D3 bytecode bug finder (+ D4 FindSecBugs)
|
||||
id 'net.ltgt.errorprone' version '5.1.0' apply false // D5 compile-time checker
|
||||
// Task 4 — Studio 계약(studio-v1.yaml)에서 DTO만 생성한다(ADR-004/ADR-006).
|
||||
id 'org.openapi.generator' version '7.18.0' apply false
|
||||
}
|
||||
|
||||
// feature-build-release-supply-chain-contract D1/D9 — every archive carries an exact SemVer
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# source: tech-log-design-package contracts/openapi/studio-v1.yaml @ b20d7a2 (feature/response-envelope-adr-006)
|
||||
6cae9924403d0761f401643a022980b8e04183eea0d890c143c9fbbbbc7431e4 studio-v1.yaml
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,14 @@
|
||||
<Source name="~.*[\\/]generated[\\/].*"/>
|
||||
</Match>
|
||||
|
||||
<!-- Task 4 — Studio 계약(studio-v1.yaml)에서 openapi-generator가 만드는 DTO는
|
||||
손으로 고치지 않으므로 SpotBugs 대상이 아니다. 위 소스-경로 기반 제외
|
||||
(build/generated/**)가 이미 넓게 걸리지만, 패키지 기준으로도 명시해 어떤
|
||||
이유로 이 패키지를 뺐는지 분명히 남긴다. -->
|
||||
<Match>
|
||||
<Package name="~dev\.caskeleton\.adapter\.inbound\.web\.techlog\.studio\.api\.model.*"/>
|
||||
</Match>
|
||||
|
||||
<!-- SPRING_CSRF_PROTECTION_DISABLED on the SecurityConfig classes is intentional: this is a
|
||||
stateless JWT bearer-token API (no session cookies / no ambient cookie auth), so CSRF
|
||||
protection is deliberately disabled per the security baseline. FindSecBugs flags it for
|
||||
@@ -43,4 +51,20 @@
|
||||
<Source name="DefaultTypingFixture.java"/>
|
||||
</Match>
|
||||
|
||||
<!-- Task 9 — StudioSessionCsrfDisabledTest$SecurityTestConfig reproduces the JWT auth-mode's
|
||||
csrf(csrf -> csrf.disable()) exactly as SecurityConfig.filterChain's JWT branch does, in
|
||||
order to pin a real regression: CsrfTokenArgumentResolver (Spring Security 7.0.0) is
|
||||
registered unconditionally by @EnableWebSecurity and casts the CsrfToken request
|
||||
attribute without a null check, so when CSRF protection is off (no CsrfFilter, no
|
||||
attribute) the controller's CsrfToken parameter is always null and
|
||||
StudioSessionController.getStudioSession must report STUDIO_UNAVAILABLE rather than
|
||||
crash. Removing csrf.disable() here would stop exercising that regression and defeat the
|
||||
test's purpose. Scoped to the exact nested fixture class only (narrower than the outer
|
||||
test class, since that is precisely where the finding is reported), same shape as the
|
||||
GraphqlHttpBoundaryQualificationTest exception above. -->
|
||||
<Match>
|
||||
<Bug pattern="SPRING_CSRF_PROTECTION_DISABLED"/>
|
||||
<Class name="dev.caskeleton.adapter.inbound.web.techlog.studio.controller.StudioSessionCsrfDisabledTest$SecurityTestConfig"/>
|
||||
</Match>
|
||||
|
||||
</FindBugsFilter>
|
||||
|
||||
Reference in New Issue
Block a user