diff --git a/ordinis-app/pom.xml b/ordinis-app/pom.xml
index 1382792..7f8f213 100644
--- a/ordinis-app/pom.xml
+++ b/ordinis-app/pom.xml
@@ -54,12 +54,48 @@
Использовать через extends либо composition в @SpringBootTest классах. + */ +public final class E2ESupport { + + // Используем postgis image — у нас есть geometry колонки в dictionary_records. + // db-init.sql создаёт postgis + btree_gist (Liquibase 0001 проверяет их). + static final PostgreSQLContainer> POSTGRES = new PostgreSQLContainer<>( + DockerImageName.parse("postgis/postgis:18-3.6") + .asCompatibleSubstituteFor("postgres")) + .withDatabaseName("ordinis_test") + .withUsername("ordinis_test") + .withPassword("test_password") + .withInitScript("db-init.sql") + .withReuse(true); + + // Confluent Kafka — confluentinc/cp-kafka работает out-of-the-box. + // Confluent 7.6.0 включает в Kafka 3.6 — kraft mode. + @SuppressWarnings("resource") + static final KafkaContainer KAFKA = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:7.6.0")) + .withReuse(true); + + static { + POSTGRES.start(); + KAFKA.start(); + } + + private E2ESupport() {} + + /** + * Прокидывает DB + Kafka URLs в Spring Boot context. Вызывать из + * {@code @DynamicPropertySource}. + */ + public static void registerProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", POSTGRES::getJdbcUrl); + registry.add("spring.datasource.username", POSTGRES::getUsername); + registry.add("spring.datasource.password", POSTGRES::getPassword); + registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver"); + + registry.add("spring.kafka.bootstrap-servers", KAFKA::getBootstrapServers); + + // Liquibase использует тот же datasource — отдельные креды не нужны. + registry.add("spring.liquibase.url", POSTGRES::getJdbcUrl); + registry.add("spring.liquibase.user", POSTGRES::getUsername); + registry.add("spring.liquibase.password", POSTGRES::getPassword); + } +} diff --git a/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java new file mode 100644 index 0000000..e70846b --- /dev/null +++ b/ordinis-app/src/test/java/cloud/nstart/terravault/ordinis/app/e2e/SmokeE2ETest.java @@ -0,0 +1,129 @@ +package cloud.nstart.terravault.ordinis.app.e2e; + +import cloud.nstart.terravault.ordinis.domain.audit.AuditLogRepository; +import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; + +import java.util.List; + +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.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Smoke e2e: Spring Boot context поднимается с реальным Postgres + Kafka + * (Testcontainers), создаём dictionary через POST /api/v1/dictionaries, + * добавляем record через POST .../records, проверяем что: + * - Запись персистнулась в Postgres (через repo) + * - audit_log получил CREATE-entry + * - GET /api/v1/dictionaries/{name}/records/{key} возвращает её обратно + * + *
Этап 1 покрытия. Bitemporal flows / idempotency / outbox→Kafka — Этап 2.
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@AutoConfigureMockMvc
+@ActiveProfiles("test")
+class SmokeE2ETest {
+
+ @DynamicPropertySource
+ static void props(DynamicPropertyRegistry registry) {
+ E2ESupport.registerProperties(registry);
+ }
+
+ @Autowired MockMvc mvc;
+ @Autowired ObjectMapper om;
+ @Autowired DictionaryRecordRepository recordRepo;
+ @Autowired AuditLogRepository auditRepo;
+
+ @Test
+ void contextLoadsAndCreatesRecord_endToEnd() throws Exception {
+ // Уникальный suffix чтобы при testcontainers reuse=true тест был идемпотентен.
+ String dictName = "smoke_" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
+ long auditCountBefore = auditRepo.count();
+
+ // 1. Создать dictionary
+ JsonNode schema = om.readTree("""
+ {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "additionalProperties": false,
+ "x-id-source": "code",
+ "required": ["code", "label"],
+ "properties": {
+ "code": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]{0,31}$", "x-unique": true },
+ "label": { "type": "string", "minLength": 1 }
+ }
+ }
+ """);
+
+ var createDictBody = om.createObjectNode()
+ .put("name", dictName)
+ .put("scope", "PUBLIC")
+ .put("displayName", "Smoke test dict")
+ .put("schemaVersion", "1.0.0")
+ .put("bundle", "test")
+ .set("schemaJson", schema);
+
+ mvc.perform(post("/api/v1/dictionaries")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(om.writeValueAsBytes(createDictBody)))
+ .andExpect(status().is2xxSuccessful());
+
+ // 2. Создать record. CreateRecordRequest.businessKey @NotBlank — для
+ // x-id-source-схем фронт деривит и передаёт явно. Бэк потом проверит
+ // совпадение с data.code (см. resolveBusinessKey).
+ var createRecBody = om.createObjectNode();
+ createRecBody.put("businessKey", "ALPHA");
+ createRecBody.set("data", om.createObjectNode()
+ .put("code", "ALPHA")
+ .put("label", "Alpha record"));
+
+ MvcResult recResult = mvc.perform(post("/api/v1/dictionaries/{name}/records", dictName)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(om.writeValueAsBytes(createRecBody)))
+ .andExpect(status().is2xxSuccessful())
+ .andExpect(jsonPath("$.businessKey").value("ALPHA"))
+ .andReturn();
+
+ JsonNode recResp = om.readTree(recResult.getResponse().getContentAsString());
+ String recordId = recResp.get("id").asText();
+ assertThat(recordId).isNotBlank();
+
+ // 3. Проверить что запись в репо
+ var allForKey = recordRepo.findAll().stream()
+ .filter(r -> "ALPHA".equals(r.getBusinessKey()))
+ .toList();
+ assertThat(allForKey).hasSize(1);
+ assertThat(allForKey.get(0).getDataScope().name()).isEqualTo("PUBLIC");
+
+ // 4. GET через writer (read-api контроллер живёт в отдельном модуле,
+ // здесь admin-write путь)
+ mvc.perform(get("/api/v1/dictionaries/{name}/records/{key}", dictName, "ALPHA"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.businessKey").value("ALPHA"))
+ .andExpect(jsonPath("$.data.label").value("Alpha record"));
+
+ // 5. audit_log пополнился (≥1 CREATE entry).
+ long auditCountAfter = auditRepo.count();
+ assertThat(auditCountAfter).isGreaterThan(auditCountBefore);
+ var newEntries = auditRepo.findAll().stream()
+ .filter(a -> dictName.equals(a.getDictionaryName()))
+ .filter(a -> "CREATE".equals(a.getAction()))
+ .toList();
+ assertThat(newEntries).isNotEmpty();
+ assertThat(newEntries.get(0).getBusinessKey()).isEqualTo("ALPHA");
+ }
+}
diff --git a/ordinis-app/src/test/resources/application-test.yml b/ordinis-app/src/test/resources/application-test.yml
new file mode 100644
index 0000000..2729ca9
--- /dev/null
+++ b/ordinis-app/src/test/resources/application-test.yml
@@ -0,0 +1,58 @@
+# Профиль для @SpringBootTest e2e тестов. Тушит весь cloud-config / vault /
+# OTel / outbox publishing — оставляем чистый Spring Boot + Postgres + Kafka
+# Testcontainers.
+
+spring:
+ cloud:
+ config:
+ enabled: false
+ vault:
+ enabled: false
+ autoconfigure:
+ exclude:
+ - org.springframework.cloud.vault.config.VaultAutoConfiguration
+ - org.springframework.cloud.vault.config.VaultObservationAutoConfiguration
+ - org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration
+ jpa:
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
+ hibernate.jdbc.lob.non_contextual_creation: true
+ open-in-view: false
+ liquibase:
+ change-log: db/changelog/master.xml
+ enabled: true
+ kafka:
+ producer:
+ acks: all
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.apache.kafka.common.serialization.StringSerializer
+
+ordinis:
+ auth:
+ issuer-uri: ""
+ require-authentication: false
+ allow-query-scope: true
+ outbox:
+ enabled: ${ORDINIS_OUTBOX_ENABLED:false}
+ bundle:
+ cuod:
+ auto-import: ${ORDINIS_BUNDLE_AUTOIMPORT:false}
+
+otel:
+ sdk:
+ disabled: true
+
+management:
+ metrics:
+ export:
+ prometheus:
+ enabled: false
+
+logging:
+ level:
+ root: WARN
+ cloud.nstart.terravault.ordinis: INFO
+ liquibase: WARN
+ org.hibernate.SQL: WARN
diff --git a/ordinis-app/src/test/resources/db-init.sql b/ordinis-app/src/test/resources/db-init.sql
new file mode 100644
index 0000000..3b42d1f
--- /dev/null
+++ b/ordinis-app/src/test/resources/db-init.sql
@@ -0,0 +1,6 @@
+-- Init SQL для testcontainer Postgres. Liquibase 0001-extensions.xml
+-- ожидает postgis + btree_gist уже созданными (в прод-кластере CNPG initdb
+-- их создаёт). В тестах создаём вручную.
+
+CREATE EXTENSION IF NOT EXISTS postgis;
+CREATE EXTENSION IF NOT EXISTS btree_gist;
diff --git a/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/audit/AuditLog.java b/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/audit/AuditLog.java
index 63b1c75..8e2bff2 100644
--- a/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/audit/AuditLog.java
+++ b/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/audit/AuditLog.java
@@ -71,7 +71,7 @@ public class AuditLog {
@Column(name = "request_id", length = 64)
private String requestId;
- @Column(name = "ip_address", columnDefinition = "inet")
+ @Column(name = "ip_address", length = 64)
private String ipAddress;
@Column(name = "user_agent", columnDefinition = "TEXT")
diff --git a/ordinis-migrations/src/main/resources/db/changelog/changes/0012-audit-log-ipaddress-varchar.xml b/ordinis-migrations/src/main/resources/db/changelog/changes/0012-audit-log-ipaddress-varchar.xml
new file mode 100644
index 0000000..328a301
--- /dev/null
+++ b/ordinis-migrations/src/main/resources/db/changelog/changes/0012-audit-log-ipaddress-varchar.xml
@@ -0,0 +1,35 @@
+
+