fix: serve Studio at the contract path instead of /api/api/v1/...
PresentationWebConfig prefixes every controller mapping with
ca-skeleton.presentation.api-base-path ("/api"), which is why every other
controller in this repository declares its path without it — healthcheck
is "/healthcheck", uploads are "/v1/uploads", files are "/v1/files". The
two Studio controllers declared "/api/v1/studio/..." instead, so the
prefix landed on top of a path that already had it and both operations
were served at /api/api/v1/studio/... — nowhere near the address
studio-v1.yaml declares (servers: "/", paths: /api/v1/studio/...). The
frontend calls the contract path, so nothing connected.
Verified against a running backend on PostgreSQL behind a real Keycloak:
/api/v1/studio/catalog?type=TOPIC 200, 3 items (was 404)
/api/api/v1/studio/catalog?type=TOPIC 404 (was 200)
Why the tests were green while production was broken: the three slice
tests and both nested apps in StudioContractDriftTest build contexts that
never include PresentationWebConfig, so no prefix was applied and the
controllers' literal "/api/v1/..." matched. They now import it and supply
the same "/api" the real app uses, which makes the paths they exercise the
effective ones. Re-introducing the bug fails five of them.
StudioContractDriftTest needed two more repairs to stay meaningful:
springdoc's own endpoint is prefixed too, so the published document is
read from /api/v3/api-docs; and publishedStudioOperationsMatchTheContract
skips any path not starting with /api/v1/studio/, so a missing prefix
would have made it compare nothing and pass. It now asserts it compared at
least one path — a gate that cannot see drift is not the same as no drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab0447a0f9
commit
e615c24152
+2
-3
@@ -12,9 +12,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
*
|
||||
* <p>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) {
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ import org.jspecify.annotations.Nullable;
|
||||
* H2 atomic scope claim.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>no row for the scope → {@code WHEN NOT MATCHED} inserts the claim (1 row);
|
||||
|
||||
+6
-6
@@ -15,12 +15,12 @@ import org.springframework.jdbc.core.JdbcOperations;
|
||||
* <li><b>Session scope, not transaction scope.</b> 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.
|
||||
* <li><b>No idle-in-transaction guard.</b> 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.
|
||||
* <li><b>No idle-in-transaction guard.</b> 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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>The millisecond values are inlined because H2's {@code SET} takes no bind parameter. They
|
||||
|
||||
+8
-8
@@ -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.
|
||||
*
|
||||
* <p><b>No Flyway location customizer, deliberately.</b> 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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Local therefore verifies wiring and behaviour, not migrations. Migration and vendor-concurrency
|
||||
* fidelity stay with the real-PostgreSQL integration suites.
|
||||
* <p>Local therefore verifies wiring and behaviour, not migrations. Migration and
|
||||
* vendor-concurrency fidelity stay with the real-PostgreSQL integration suites.
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty(
|
||||
|
||||
+5
-4
@@ -31,8 +31,8 @@ import org.springframework.transaction.support.TransactionTemplate;
|
||||
* USING}, and only an execution proves that the substitution kept the three outcomes intact.
|
||||
*
|
||||
* <p>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<OutboxEventEntity> 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
|
||||
|
||||
Reference in New Issue
Block a user