From a3c35c7df9f34aa77e3131a0493d9a5d5ca56139 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:37:00 +0200 Subject: [PATCH] feat(outbound): request + response + test-result DTOs with Bean Validation Co-Authored-By: Claude Opus 4.7 (1M context) --- .../outbound/dto/OutboundConnectionDto.java | 31 ++++++++++++ .../dto/OutboundConnectionRequest.java | 37 ++++++++++++++ .../dto/OutboundConnectionTestResult.java | 12 +++++ ...tboundConnectionRequestValidationTest.java | 49 +++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionDto.java create mode 100644 cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionRequest.java create mode 100644 cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionTestResult.java create mode 100644 cameleer-server-app/src/test/java/com/cameleer/server/app/outbound/OutboundConnectionRequestValidationTest.java diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionDto.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionDto.java new file mode 100644 index 00000000..07a0aa9f --- /dev/null +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionDto.java @@ -0,0 +1,31 @@ +package com.cameleer.server.app.outbound.dto; + +import com.cameleer.server.core.http.TrustMode; +import com.cameleer.server.core.outbound.OutboundAuthKind; +import com.cameleer.server.core.outbound.OutboundConnection; +import com.cameleer.server.core.outbound.OutboundMethod; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public record OutboundConnectionDto( + UUID id, String name, String description, String url, + OutboundMethod method, Map defaultHeaders, String defaultBodyTmpl, + TrustMode tlsTrustMode, List tlsCaPemPaths, + boolean hmacSecretSet, + OutboundAuthKind authKind, + List allowedEnvironmentIds, + Instant createdAt, String createdBy, Instant updatedAt, String updatedBy +) { + public static OutboundConnectionDto from(OutboundConnection c) { + return new OutboundConnectionDto( + c.id(), c.name(), c.description(), c.url(), c.method(), + c.defaultHeaders(), c.defaultBodyTmpl(), + c.tlsTrustMode(), c.tlsCaPemPaths(), + c.hmacSecretCiphertext() != null, + c.auth().kind(), c.allowedEnvironmentIds(), + c.createdAt(), c.createdBy(), c.updatedAt(), c.updatedBy()); + } +} diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionRequest.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionRequest.java new file mode 100644 index 00000000..2a5897b2 --- /dev/null +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionRequest.java @@ -0,0 +1,37 @@ +package com.cameleer.server.app.outbound.dto; + +import com.cameleer.server.core.http.TrustMode; +import com.cameleer.server.core.outbound.OutboundAuth; +import com.cameleer.server.core.outbound.OutboundMethod; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public record OutboundConnectionRequest( + @NotBlank @Size(max = 100) String name, + @Size(max = 2000) String description, + @NotBlank @Pattern(regexp = "^https://.+", message = "URL must be HTTPS") String url, + @NotNull OutboundMethod method, + Map defaultHeaders, + String defaultBodyTmpl, + @NotNull TrustMode tlsTrustMode, + List tlsCaPemPaths, + String hmacSecret, + @NotNull @Valid OutboundAuth auth, + List allowedEnvironmentIds +) { + public OutboundConnectionRequest { + defaultHeaders = defaultHeaders == null ? Map.of() : defaultHeaders; + tlsCaPemPaths = tlsCaPemPaths == null ? List.of() : tlsCaPemPaths; + allowedEnvironmentIds = allowedEnvironmentIds == null ? List.of() : allowedEnvironmentIds; + if (tlsTrustMode == TrustMode.TRUST_PATHS && tlsCaPemPaths.isEmpty()) { + throw new IllegalArgumentException("tlsCaPemPaths must not be empty when tlsTrustMode = TRUST_PATHS"); + } + } +} diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionTestResult.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionTestResult.java new file mode 100644 index 00000000..71527ae1 --- /dev/null +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/outbound/dto/OutboundConnectionTestResult.java @@ -0,0 +1,12 @@ +package com.cameleer.server.app.outbound.dto; + +public record OutboundConnectionTestResult( + int status, + long latencyMs, + String responseSnippet, + String tlsProtocol, + String tlsCipherSuite, + String peerCertificateSubject, + Long peerCertificateExpiresAtEpochMs, + String error +) {} diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/outbound/OutboundConnectionRequestValidationTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/outbound/OutboundConnectionRequestValidationTest.java new file mode 100644 index 00000000..4ca4a8f8 --- /dev/null +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/outbound/OutboundConnectionRequestValidationTest.java @@ -0,0 +1,49 @@ +package com.cameleer.server.app.outbound; + +import com.cameleer.server.app.outbound.dto.OutboundConnectionRequest; +import com.cameleer.server.core.http.TrustMode; +import com.cameleer.server.core.outbound.OutboundAuth; +import com.cameleer.server.core.outbound.OutboundMethod; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OutboundConnectionRequestValidationTest { + private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void validRequestPasses() { + var r = new OutboundConnectionRequest("slack-ops", "desc", + "https://hooks.slack.com/x", OutboundMethod.POST, + Map.of(), null, TrustMode.SYSTEM_DEFAULT, List.of(), + null, new OutboundAuth.None(), List.of()); + assertThat(validator.validate(r)).isEmpty(); + } + + @Test + void blankNameFails() { + var r = new OutboundConnectionRequest(" ", null, "https://x", OutboundMethod.POST, + Map.of(), null, TrustMode.SYSTEM_DEFAULT, List.of(), null, new OutboundAuth.None(), List.of()); + assertThat(validator.validate(r)).anySatisfy(v -> assertThat(v.getPropertyPath().toString()).isEqualTo("name")); + } + + @Test + void nonHttpsUrlFails() { + var r = new OutboundConnectionRequest("n", null, "http://x", OutboundMethod.POST, + Map.of(), null, TrustMode.SYSTEM_DEFAULT, List.of(), null, new OutboundAuth.None(), List.of()); + assertThat(validator.validate(r)).anySatisfy(v -> assertThat(v.getPropertyPath().toString()).isEqualTo("url")); + } + + @Test + void trustPathsRequiresNonEmptyCaList() { + assertThatThrownBy(() -> new OutboundConnectionRequest("n", null, "https://x", OutboundMethod.POST, + Map.of(), null, TrustMode.TRUST_PATHS, List.of(), null, new OutboundAuth.None(), List.of())) + .isInstanceOf(IllegalArgumentException.class); + } +}