diff --git a/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogController.java b/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogController.java
index 6490190..6ccdf4c 100644
--- a/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogController.java
+++ b/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogController.java
@@ -29,7 +29,13 @@ public class StudioCatalogController {
this.listCatalog = listCatalog;
}
- @GetMapping("/api/v1/studio/catalog")
+ /**
+ * 경로에 {@code /api} 를 쓰지 않는다 — {@code PresentationWebConfig} 가 {@code
+ * ca-skeleton.presentation.api-base-path}("/api") 를 모든 컨트롤러 매핑에 붙인다. 이 저장소의 다른 컨트롤러들(healthcheck,
+ * /v1/uploads, /v1/files)과 같은 규칙이며, 여기에 {@code /api} 를 다시 쓰면 실제 경로가 {@code /api/api/v1/...} 로 밀려
+ * 계약(studio-v1.yaml, {@code servers: "/"})이 선언한 주소에서 사라진다.
+ */
+ @GetMapping("/v1/studio/catalog")
public CatalogPage listStudioCatalog(
@RequestParam("type") CatalogEntryType type,
@RequestParam(value = "q", required = false) String q,
diff --git a/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionController.java b/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionController.java
index 2068679..3f0e585 100644
--- a/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionController.java
+++ b/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionController.java
@@ -55,7 +55,13 @@ public class StudioSessionController {
this.csrfHeaderName = configured;
}
- @GetMapping("/api/v1/studio/session")
+ /**
+ * 경로에 {@code /api} 를 쓰지 않는다 — {@code PresentationWebConfig} 가 {@code
+ * ca-skeleton.presentation.api-base-path}("/api") 를 모든 컨트롤러 매핑에 붙인다. 이 저장소의 다른 컨트롤러들(healthcheck,
+ * /v1/uploads, /v1/files)과 같은 규칙이며, 여기에 {@code /api} 를 다시 쓰면 실제 경로가 {@code /api/api/v1/...} 로 밀려
+ * 계약(studio-v1.yaml, {@code servers: "/"})이 선언한 주소에서 사라진다.
+ */
+ @GetMapping("/v1/studio/session")
public StudioSession getStudioSession(
@AuthenticationPrincipal AuthenticatedPrincipal principal, CsrfToken csrfToken) {
if (csrfToken == null) {
diff --git a/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogBindingErrorEnvelopeTest.java b/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogBindingErrorEnvelopeTest.java
index ecc264e..cee2c3e 100644
--- a/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogBindingErrorEnvelopeTest.java
+++ b/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioCatalogBindingErrorEnvelopeTest.java
@@ -5,8 +5,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
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.config.PresentationWebConfig;
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
import dev.caskeleton.adapter.inbound.web.error.GlobalExceptionHandler;
+import dev.caskeleton.adapter.inbound.web.settings.PresentationSettings;
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;
@@ -58,6 +60,7 @@ import org.springframework.test.web.servlet.MockMvc;
excludeAutoConfiguration = SecurityAutoConfiguration.class)
@AutoConfigureMockMvc(addFilters = false)
@Import({
+ PresentationWebConfig.class,
StudioCatalogController.class,
StudioExceptionHandler.class,
GlobalExceptionHandler.class,
@@ -98,6 +101,16 @@ class StudioCatalogBindingErrorEnvelopeTest {
static class TestBeans {
+ /**
+ * PresentationWebConfig 가 이 값으로 모든 컨트롤러 매핑에 "/api" 를 붙인다. 실제 앱의 PRESENTATION_API_BASE_PATH 와 같은
+ * 값이라, 아래 테스트들이 호출하는 /api/v1/... 은 컨트롤러가 선언한 /v1/... 에 prefix 가 적용된 결과다 — 컨트롤러가 "/api" 를 다시
+ * 선언하면 /api/api/... 로 밀려 이 테스트들이 404 로 깨진다.
+ */
+ @Bean
+ PresentationSettings presentationSettings() {
+ return new PresentationSettings("/api");
+ }
+
@Bean
ListCatalogUseCase listCatalogUseCase() {
CatalogQueryPort neverInvoked =
diff --git a/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionCsrfDisabledTest.java b/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionCsrfDisabledTest.java
index aff5c33..41e6995 100644
--- a/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionCsrfDisabledTest.java
+++ b/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionCsrfDisabledTest.java
@@ -6,9 +6,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
+import dev.caskeleton.adapter.inbound.web.config.PresentationWebConfig;
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.PresentationSettings;
import dev.caskeleton.adapter.inbound.web.settings.SecuritySettings;
import dev.caskeleton.adapter.inbound.web.techlog.StudioExceptionHandler;
import java.util.List;
@@ -46,6 +48,7 @@ import org.springframework.test.web.servlet.MockMvc;
*/
@WebMvcTest(controllers = StudioSessionController.class)
@Import({
+ PresentationWebConfig.class,
StudioSessionController.class,
EnvelopeBodyAdvice.class,
StudioExceptionHandler.class,
@@ -89,6 +92,16 @@ class StudioSessionCsrfDisabledTest {
@EnableWebSecurity
static class SecurityTestConfig {
+ /**
+ * PresentationWebConfig 가 이 값으로 모든 컨트롤러 매핑에 "/api" 를 붙인다. 실제 앱의 PRESENTATION_API_BASE_PATH 와 같은
+ * 값이라, 아래 테스트들이 호출하는 /api/v1/... 은 컨트롤러가 선언한 /v1/... 에 prefix 가 적용된 결과다 — 컨트롤러가 "/api" 를 다시
+ * 선언하면 /api/api/... 로 밀려 이 테스트들이 404 로 깨진다.
+ */
+ @Bean
+ PresentationSettings presentationSettings() {
+ return new PresentationSettings("/api");
+ }
+
@Bean
SecurityFilterChain csrfDisabledFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
diff --git a/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionEnvelopeTest.java b/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionEnvelopeTest.java
index f42b732..242afe8 100644
--- a/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionEnvelopeTest.java
+++ b/src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/techlog/studio/controller/StudioSessionEnvelopeTest.java
@@ -7,8 +7,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
+import dev.caskeleton.adapter.inbound.web.config.PresentationWebConfig;
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
import dev.caskeleton.adapter.inbound.web.observability.MdcKeys;
+import dev.caskeleton.adapter.inbound.web.settings.PresentationSettings;
import dev.caskeleton.adapter.inbound.web.settings.SecuritySettings;
import java.util.List;
import java.util.Set;
@@ -68,6 +70,7 @@ import org.springframework.test.web.servlet.MockMvc;
*/
@WebMvcTest(controllers = StudioSessionController.class)
@Import({
+ PresentationWebConfig.class,
StudioSessionController.class,
EnvelopeBodyAdvice.class,
StudioSessionEnvelopeTest.SecurityTestConfig.class
@@ -111,6 +114,16 @@ class StudioSessionEnvelopeTest {
@EnableWebSecurity
static class SecurityTestConfig {
+ /**
+ * PresentationWebConfig 가 이 값으로 모든 컨트롤러 매핑에 "/api" 를 붙인다. 실제 앱의 PRESENTATION_API_BASE_PATH 와 같은
+ * 값이라, 아래 테스트들이 호출하는 /api/v1/... 은 컨트롤러가 선언한 /v1/... 에 prefix 가 적용된 결과다 — 컨트롤러가 "/api" 를 다시
+ * 선언하면 /api/api/... 로 밀려 이 테스트들이 404 로 깨진다.
+ */
+ @Bean
+ PresentationSettings presentationSettings() {
+ return new PresentationSettings("/api");
+ }
+
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceVendorSettings.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceVendorSettings.java
index eba19cb..e08dbc9 100644
--- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceVendorSettings.java
+++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceVendorSettings.java
@@ -12,9 +12,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
*
*
Binding to an enum is what makes an unknown vendor a startup failure. With a raw string the
* two {@code @ConditionalOnProperty} vendor configurations would both stay off, and the first
- * missing SPI bean would surface as a {@code NoSuchBeanDefinitionException} naming
- * {@code OutboxClaimRepository} — a symptom several layers away from the misspelled value that
- * caused it.
+ * missing SPI bean would surface as a {@code NoSuchBeanDefinitionException} naming {@code
+ * OutboxClaimRepository} — a symptom several layers away from the misspelled value that caused it.
*/
@ConfigurationProperties(prefix = PersistenceVendorSettings.PREFIX)
public record PersistenceVendorSettings(Vendor vendor) {
diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2IdempotencyClaimRepository.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2IdempotencyClaimRepository.java
index bcfa66c..7c7cac5 100644
--- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2IdempotencyClaimRepository.java
+++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2IdempotencyClaimRepository.java
@@ -12,8 +12,8 @@ import org.jspecify.annotations.Nullable;
* H2 atomic scope claim.
*
*
H2 has no {@code INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING}, so the PostgreSQL
- * statement does not port. The standard {@code MERGE ... USING} does, and carries the same
- * meaning in one statement:
+ * statement does not port. The standard {@code MERGE ... USING} does, and carries the same meaning
+ * in one statement:
*
*
* - no row for the scope → {@code WHEN NOT MATCHED} inserts the claim (1 row);
diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
index ef7fd5c..b17781e 100644
--- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
+++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
@@ -15,12 +15,12 @@ import org.springframework.jdbc.core.JdbcOperations;
*
- Session scope, not transaction scope. PostgreSQL takes {@code set_config(..., true)}
* — a value that reverts at transaction end. H2's {@code SET} is session-wide and outlives
* the transaction on a pooled connection. It is not left stale in practice because the
- * transaction port applies these before every transaction, so each one overwrites the last;
- * a connection borrowed outside that path keeps the previous transaction's guard.
- *
- No idle-in-transaction guard. H2 has no counterpart to
- * {@code idle_in_transaction_session_timeout}, so that budget cannot be pushed into the
- * database here. It is left to the caller-side deadline the transaction port already
- * enforces, rather than silently reported as applied.
+ * transaction port applies these before every transaction, so each one overwrites the last; a
+ * connection borrowed outside that path keeps the previous transaction's guard.
+ *
- No idle-in-transaction guard. H2 has no counterpart to {@code
+ * idle_in_transaction_session_timeout}, so that budget cannot be pushed into the database
+ * here. It is left to the caller-side deadline the transaction port already enforces, rather
+ * than silently reported as applied.
*
*
* The millisecond values are inlined because H2's {@code SET} takes no bind parameter. They
diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
index 2c1c70a..e59f4cd 100644
--- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
+++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
@@ -15,13 +15,13 @@ import org.springframework.jdbc.core.JdbcOperations;
/**
* H2 vendor persistence configuration — the same four SPI beans the PostgreSQL vendor registers,
- * implemented against H2. Selected by {@code ca-skeleton.persistence.vendor=h2}, which the
- * {@code local} profile sets.
+ * implemented against H2. Selected by {@code ca-skeleton.persistence.vendor=h2}, which the {@code
+ * local} profile sets.
*
*
No Flyway location customizer, deliberately. The PostgreSQL vendor points Flyway at
* {@code classpath:db/migration/postgresql}; there is no H2 equivalent tree, because the local
- * profile turns Flyway off and lets Hibernate derive the schema from the entities. Two
- * consequences worth stating out loud:
+ * profile turns Flyway off and lets Hibernate derive the schema from the entities. Two consequences
+ * worth stating out loud:
*
*
* - Tables that exist only in migrations — the capability schema registry, the polling-delivery
@@ -30,12 +30,12 @@ import org.springframework.jdbc.core.JdbcOperations;
* there will fail on a missing table rather than silently misbehave.
*
- A fork that enables Flyway while this vendor is selected gets no location override, so
* Flyway falls back to {@code classpath:db/migration} and walks the whole tree — including
- * PostgreSQL DDL H2 cannot parse. Such a fork should register its own
- * {@code FlywayConfigurationCustomizer} naming an H2 location.
+ * PostgreSQL DDL H2 cannot parse. Such a fork should register its own {@code
+ * FlywayConfigurationCustomizer} naming an H2 location.
*
*
- * Local therefore verifies wiring and behaviour, not migrations. Migration and vendor-concurrency
- * fidelity stay with the real-PostgreSQL integration suites.
+ *
Local therefore verifies wiring and behaviour, not migrations. Migration and
+ * vendor-concurrency fidelity stay with the real-PostgreSQL integration suites.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(
diff --git a/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/h2/H2ClaimSqlTest.java b/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/h2/H2ClaimSqlTest.java
index a18dd28..8ada120 100644
--- a/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/h2/H2ClaimSqlTest.java
+++ b/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/h2/H2ClaimSqlTest.java
@@ -31,8 +31,8 @@ import org.springframework.transaction.support.TransactionTemplate;
* USING}, and only an execution proves that the substitution kept the three outcomes intact.
*
*
In-memory and process-local, so this stays an ordinary unit test: no container, no network,
- * nothing to skip when Docker is absent. Real-PostgreSQL fidelity remains the job of the
- * {@code postgresqlIntegrationTest} source set.
+ * nothing to skip when Docker is absent. Real-PostgreSQL fidelity remains the job of the {@code
+ * postgresqlIntegrationTest} source set.
*/
class H2ClaimSqlTest {
@@ -142,8 +142,9 @@ class H2ClaimSqlTest {
List claimed = claimEligible(now, 10);
- assertThat(claimed).extracting(OutboxEventEntity::getEventId).containsExactly("evt-old",
- "evt-other");
+ assertThat(claimed)
+ .extracting(OutboxEventEntity::getEventId)
+ .containsExactly("evt-old", "evt-other");
}
@Test
diff --git a/src/app-bootstrap/src/functionalTest/java/dev/caskeleton/bootstrap/contract/StudioContractDriftTest.java b/src/app-bootstrap/src/functionalTest/java/dev/caskeleton/bootstrap/contract/StudioContractDriftTest.java
index 8d4a876..74b5f1e 100644
--- a/src/app-bootstrap/src/functionalTest/java/dev/caskeleton/bootstrap/contract/StudioContractDriftTest.java
+++ b/src/app-bootstrap/src/functionalTest/java/dev/caskeleton/bootstrap/contract/StudioContractDriftTest.java
@@ -6,7 +6,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.caskeleton.adapter.inbound.web.config.PresentationWebConfig;
import dev.caskeleton.adapter.inbound.web.envelope.EnvelopeBodyAdvice;
+import dev.caskeleton.adapter.inbound.web.settings.PresentationSettings;
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;
@@ -40,72 +42,73 @@ 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 implemented operations are checked (direction is "published ⊆ contract", never the
- * reverse), so this stays green as slices 2-5 add the other 17 operations — on one condition:
- * 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 outside 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.
+ * config/openapi/studio-v1.yaml} for every {@code /api/v1/studio/**} path it actually exposes. Only
+ * implemented operations are checked (direction is "published ⊆ contract", never the
+ * reverse), so this stays green as slices 2-5 add the other 17 operations — on one
+ * condition: 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 outside 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.
*
* Why a hand-built minimal context rather than {@code CaSkeletonApplication}
*
* 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
+ * {@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
+ * 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.
+ * 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.
*
*
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
+ * 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.
*
*
{@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.
+ * only sees two modules' worth of dependencies keeps the cost bounded to "start web MVC +
+ * springdoc" without paying for DB/security infrastructure.
*
*
Why two nested contexts instead of one
*
* 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} — 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.
+ * 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} — 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.
*
* Why {@link ListCatalogUseCase} is real, not mocked
*
@@ -125,12 +128,12 @@ import org.yaml.snakeyaml.Yaml;
* 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.
+ * 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 {
@@ -142,9 +145,8 @@ class StudioContractDriftTest {
@Autowired private MockMvc mvc;
/**
- * "구현된 것만 검사" 방향 고정: 계약(19개 operation)이 아니라 published(springdoc이 실제로 내놓는 것, 지금은 2개)를
- * 순회한다. 슬라이스 2~5가 나머지 17개를 추가해도 이 순회 방향 덕분에 이 테스트는 그대로 통과한다 — 반대로 순회했다면 미구현 operation마다
- * 매번 실패했을 것이다.
+ * "구현된 것만 검사" 방향 고정: 계약(19개 operation)이 아니라 published(springdoc이 실제로 내놓는 것, 지금은 2개)를 순회한다. 슬라이스
+ * 2~5가 나머지 17개를 추가해도 이 순회 방향 덕분에 이 테스트는 그대로 통과한다 — 반대로 순회했다면 미구현 operation마다 매번 실패했을 것이다.
*/
@Test
void publishedStudioOperationsMatchTheContract() throws Exception {
@@ -152,11 +154,13 @@ class StudioContractDriftTest {
JsonNode published = readPublishedApiDocs();
List problems = new ArrayList<>();
+ List compared = new ArrayList<>();
JsonNode publishedPaths = published.path("paths");
for (Map.Entry path : publishedPaths.properties()) {
if (!path.getKey().startsWith("/api/v1/studio/")) {
continue;
}
+ compared.add(path.getKey());
JsonNode contractPath = contract.path("paths").path(path.getKey());
if (contractPath.isMissingNode()) {
problems.add("계약에 없는 path: " + path.getKey());
@@ -184,6 +188,16 @@ class StudioContractDriftTest {
}
}
assertThat(problems).isEmpty();
+ // 이 순회는 "/api/v1/studio/" 로 시작하는 published path 만 본다. prefix 배선이 빠지면
+ // 모든 경로가 필터에 걸러져 아무것도 비교하지 않은 채 green 이 된다 — 게이트가 드리프트를
+ // "못 보는" 상태와 "없는" 상태가 구별되지 않는다. 그래서 최소 하나는 실제로 대조했음을 함께 고정한다.
+ assertThat(compared)
+ .as(
+ "published 표면에서 /api/v1/studio/ 경로를 하나도 대조하지 못했다 —"
+ + " PresentationWebConfig 의 api-base-path 배선이 빠졌는지 확인하라."
+ + " published paths="
+ + publishedPaths.properties().stream().map(Map.Entry::getKey).toList())
+ .isNotEmpty();
}
/**
@@ -204,9 +218,14 @@ class StudioContractDriftTest {
return new ObjectMapper().valueToTree(contractYaml);
}
+ /**
+ * {@code /api/v3/api-docs} — springdoc 의 엔드포인트에도 {@link PresentationWebConfig} 의 {@code
+ * api-base-path} 가 붙는다. 실제 앱에서 published 문서가 실제로 서비스되는 주소이며, 여기서 {@code /v3/api-docs} 를 두드리면
+ * 404 가 난다.
+ */
private JsonNode readPublishedApiDocs() throws Exception {
String body =
- mvc.perform(get("/v3/api-docs"))
+ mvc.perform(get("/api/v3/api-docs"))
.andExpect(status().isOk())
.andReturn()
.getResponse()
@@ -215,20 +234,30 @@ class StudioContractDriftTest {
}
/**
- * {@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엔 애초에 관여하지 않는다.
+ * {@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")
+ @Import(PresentationWebConfig.class)
static class ContractSurfaceApp {
+ /**
+ * 실제 앱의 {@code PRESENTATION_API_BASE_PATH}. {@link PresentationWebConfig} 가 이 값을 모든 컨트롤러 매핑에
+ * 붙이므로, published 표면의 경로는 컨트롤러가 선언한 {@code /v1/studio/...} 이 아니라 계약이 선언한 {@code
+ * /api/v1/studio/...} 여야 한다.
+ */
+ @Bean
+ PresentationSettings presentationSettings() {
+ return new PresentationSettings("/api");
+ }
+
@Bean
SecuritySettings securitySettings() {
return StudioContractDriftTest.securitySettingsForTest();
@@ -262,8 +291,8 @@ class StudioContractDriftTest {
}
/**
- * {@code adapter-inbound-web}에 실제 애플리케이션이 없어({@code CaSkeletonApplication}은 app-bootstrap
- * 소유) {@code @SpringBootTest}가 부트스트랩할 {@code @SpringBootConfiguration}이 필요하다 — {@code
+ * {@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
@@ -272,9 +301,19 @@ class StudioContractDriftTest {
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
@ComponentScan("dev.caskeleton.adapter.inbound.web.techlog")
- @Import(EnvelopeBodyAdvice.class)
+ @Import({EnvelopeBodyAdvice.class, PresentationWebConfig.class})
static class EnvelopeApp {
+ /**
+ * 실제 앱의 {@code PRESENTATION_API_BASE_PATH}. {@link PresentationWebConfig} 가 이 값을 모든 컨트롤러 매핑에
+ * 붙이므로, published 표면의 경로는 컨트롤러가 선언한 {@code /v1/studio/...} 이 아니라 계약이 선언한 {@code
+ * /api/v1/studio/...} 여야 한다.
+ */
+ @Bean
+ PresentationSettings presentationSettings() {
+ return new PresentationSettings("/api");
+ }
+
@Bean
SecuritySettings securitySettings() {
return StudioContractDriftTest.securitySettingsForTest();
@@ -287,18 +326,25 @@ class StudioContractDriftTest {
}
}
- /** {@code csrfHeaderName}이 계약 const {@code X-CSRF-TOKEN}과 다르면 {@link StudioSessionController}의 생성자가 즉시 실패한다. */
+ /**
+ * {@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);
+ SecuritySettings.AuthenticationMode.JWT,
+ "https://issuer.example",
+ null,
+ List.of(),
+ session);
}
/**
- * 실제 영속성 어댑터 대신 최소 stub 포트로 구성한 진짜 {@link ListCatalogUseCase}. springdoc은 컨트롤러 메서드를 호출하지 않고
- * 리플렉션만 하므로 첫 번째 테스트에는 아예 관여하지 않고, 두 번째 테스트(봉투 확인)는 결과 내용이 아니라 감싸는 모양만 보므로 빈 목록으로 충분하다.
+ * 실제 영속성 어댑터 대신 최소 stub 포트로 구성한 진짜 {@link ListCatalogUseCase}. springdoc은 컨트롤러 메서드를 호출하지 않고 리플렉션만
+ * 하므로 첫 번째 테스트에는 아예 관여하지 않고, 두 번째 테스트(봉투 확인)는 결과 내용이 아니라 감싸는 모양만 보므로 빈 목록으로 충분하다.
*/
private static ListCatalogUseCase listCatalogUseCaseForTest() {
return new ListCatalogUseCase(new StubCatalogQueryPort(), new PassThroughTransactionPort());
diff --git a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/StudioErrorRegistryTest.java b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/StudioErrorRegistryTest.java
index 08b8e6a..9721c05 100644
--- a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/StudioErrorRegistryTest.java
+++ b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/StudioErrorRegistryTest.java
@@ -27,11 +27,11 @@ import org.yaml.snakeyaml.Yaml;
*
*
* - every {@link StudioError} constant has a matching registry row (code presence);
- *
- 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
+ *
- 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;
*
- {@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
@@ -45,8 +45,8 @@ import org.yaml.snakeyaml.Yaml;
* 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.
+ * this test needs structured field access (category/http_status/retryable/client_safe_message), not
+ * just the {@code code:} key.
*/
class StudioErrorRegistryTest {
@@ -102,8 +102,8 @@ class StudioErrorRegistryTest {
/**
* {@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).
+ * 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() {
@@ -118,14 +118,14 @@ class StudioErrorRegistryTest {
}
/**
- * 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.
+ * 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 {
@@ -180,8 +180,8 @@ class StudioErrorRegistryTest {
}
/**
- * Parses lines shaped {@code }, skipping {@code #}-prefixed comment
- * lines such as MANIFEST.sha256's {@code # source: ...} provenance line.
+ * Parses lines shaped {@code }, 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()
diff --git a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/TechLogBoundaryArchTest.java b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/TechLogBoundaryArchTest.java
index 4821656..7d8b223 100644
--- a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/TechLogBoundaryArchTest.java
+++ b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/TechLogBoundaryArchTest.java
@@ -7,8 +7,8 @@ import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
/**
- * 설계 08장의 bounded context 경계를 빌드로 강제한다. Gradle leaf를 늘리지 않고
- * 패키지로 나눴으므로(spec D1/D2) 경계는 이 규칙이 유일한 방어선이다.
+ * 설계 08장의 bounded context 경계를 빌드로 강제한다. Gradle leaf를 늘리지 않고 패키지로 나눴으므로(spec D1/D2) 경계는 이 규칙이 유일한
+ * 방어선이다.
*/
@AnalyzeClasses(packages = "dev.caskeleton", importOptions = ProductionClassImportOption.class)
class TechLogBoundaryArchTest {
diff --git a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/ProfileSeparationContractTest.java b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/ProfileSeparationContractTest.java
index 026e7f7..396cffe 100644
--- a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/ProfileSeparationContractTest.java
+++ b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/ProfileSeparationContractTest.java
@@ -30,11 +30,11 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
* composition root inside this source set is not currently possible — the component scan that makes
* {@code CaSkeletonApplication} the composition root also finds the nested {@code @Configuration}
* classes that dozens of tests here declare, and they collide. The behaviour behind these files is
- * covered where it can be: {@code H2ClaimSqlTest} runs the H2 statements against a real H2,
- * {@code PersistenceVendorSelectionTest} covers the selector, and
- * {@code PersistenceVendorProdSafetyValidatorTest} covers the prod refusal. What is left, and what
- * this test guards, is the wiring between them drifting — a profile quietly changing vendor, or
- * local regaining a migration expectation it cannot satisfy.
+ * covered where it can be: {@code H2ClaimSqlTest} runs the H2 statements against a real H2, {@code
+ * PersistenceVendorSelectionTest} covers the selector, and {@code
+ * PersistenceVendorProdSafetyValidatorTest} covers the prod refusal. What is left, and what this
+ * test guards, is the wiring between them drifting — a profile quietly changing vendor, or local
+ * regaining a migration expectation it cannot satisfy.
*/
class ProfileSeparationContractTest {
@@ -136,7 +136,8 @@ class ProfileSeparationContractTest {
return path;
}
}
- throw new IllegalStateException("repository root not found from " + Paths.get("").toAbsolutePath());
+ throw new IllegalStateException(
+ "repository root not found from " + Paths.get("").toAbsolutePath());
}
/**
@@ -197,7 +198,9 @@ class ProfileSeparationContractTest {
private record Placeholder(String variable, String inlineDefault) {}
- /** Property path → the {@code ${VAR}} or {@code ${VAR:default}} application.yml resolves it from. */
+ /**
+ * Property path → the {@code ${VAR}} or {@code ${VAR:default}} application.yml resolves it from.
+ */
private static Map placeholders() throws IOException {
Pattern syntax = Pattern.compile("^\\$\\{([A-Z0-9_]+)(?::(.*))?}$");
Map found = new LinkedHashMap<>();
diff --git a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/StudioSessionCsrfHeaderProfileContractTest.java b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/StudioSessionCsrfHeaderProfileContractTest.java
index e2d241c..2cbb28f 100644
--- a/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/StudioSessionCsrfHeaderProfileContractTest.java
+++ b/src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/StudioSessionCsrfHeaderProfileContractTest.java
@@ -23,9 +23,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
* 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}
+ * 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.
*
@@ -54,8 +54,7 @@ class StudioSessionCsrfHeaderProfileContractTest {
}
private static String csrfHeaderNameOf(Map, ?> configuration) {
- Map, ?> session =
- child(child(child(configuration, "ca-skeleton"), "security"), "session");
+ 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();
}