From e454a6b1ae65ba44062d1b6a3128306ab74ec371 Mon Sep 17 00:00:00 2001
From: Jan Zickermann <jan.zickermann@dataport.de>
Date: Thu, 20 Feb 2025 14:28:41 +0100
Subject: [PATCH] OZG-4097 configuration: Add startup configuration validation

---
 .../osiv2/config/Osi2PostfachProperties.java  |  16 +-
 .../osiv2/config/Osi2PropertiesValidator.java |  47 ++++++
 .../transfer/PostfachApiFacadeService.java    |   3 +-
 .../osiv2/OsiPostfachRemoteServiceITCase.java |   3 -
 .../config/Osi2PropertiesValidatorTest.java   | 142 ++++++++++++++++++
 5 files changed, 205 insertions(+), 6 deletions(-)
 create mode 100644 src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidator.java
 create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidatorTest.java

diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java
index 0329d8c..2ed0945 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java
@@ -3,6 +3,9 @@ package de.ozgcloud.nachrichten.postfach.osiv2.config;
 import java.util.List;
 
 import jakarta.annotation.Nullable;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -32,10 +35,16 @@ public class Osi2PostfachProperties {
 	public static class AuthConfiguration {
 		public static final String PREFIX = Osi2PostfachProperties.PREFIX + ".auth";
 
+		@NotBlank
 		private String clientId;
+		@NotBlank
 		private String clientSecret;
-		private List<String> scope;
+		@NotNull
+		@Valid
+		private List<@NotBlank String> scope;
+		@NotBlank
 		private String tokenUri;
+		@NotBlank
 		private String resource;
 	}
 
@@ -46,8 +55,11 @@ public class Osi2PostfachProperties {
 	public static class ApiConfiguration {
 		public static final String PREFIX = Osi2PostfachProperties.PREFIX + ".api";
 
+		@NotBlank
 		private String url;
+		@NotBlank
 		private String tenant;
+		@NotBlank
 		private String nameIdentifier;
 	}
 
@@ -64,7 +76,9 @@ public class Osi2PostfachProperties {
 
 		private boolean enabled;
 
+		@NotBlank
 		private String host;
+		@NotNull
 		private Integer port;
 
 		@Nullable
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidator.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidator.java
new file mode 100644
index 0000000..19f2454
--- /dev/null
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidator.java
@@ -0,0 +1,47 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.config;
+
+import java.util.Set;
+
+import javax.annotation.PostConstruct;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validator;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled;
+import lombok.RequiredArgsConstructor;
+
+@ServiceIfOsi2Enabled
+@RequiredArgsConstructor
+public class Osi2PropertiesValidator {
+
+	private final Osi2PostfachProperties.AuthConfiguration authConfiguration;
+	private final Osi2PostfachProperties.ApiConfiguration apiConfiguration;
+	private final Osi2PostfachProperties.ProxyConfiguration proxyConfiguration;
+	private final Validator validator;
+
+	@PostConstruct
+	public void validateConfiguration() {
+		validateConfiguration(authConfiguration);
+		validateConfiguration(apiConfiguration);
+		if (proxyConfiguration.isEnabled()) {
+			validateConfiguration(proxyConfiguration);
+		}
+	}
+
+	private <T> void validateConfiguration(T configuration) {
+		var violations = validator.validate(configuration);
+		if (!violations.isEmpty()) {
+			throw new TechnicalException(
+					"%s is invalid: %s".formatted(configuration.getClass().getSimpleName(), formatConstraintValidation(violations)));
+		}
+	}
+
+	private <T> String formatConstraintValidation(Set<ConstraintViolation<T>> constraints) {
+		return constraints.stream()
+				.map(violation -> String.format("%s: %s", violation.getPropertyPath(), violation.getMessage()))
+				.reduce((a, b) -> a + ", " + b)
+				.orElse("");
+	}
+}
+
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java
index 5f29ced..989ae00 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java
@@ -15,8 +15,8 @@ import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi;
 import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi;
 import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo;
-import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
 import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment;
+import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 
@@ -64,7 +64,6 @@ public class PostfachApiFacadeService {
 		return responseMapper.toMessage(messageReply);
 	}
 
-
 	public void deleteMessage(final String messageId) {
 		messageExchangeApi.deleteMessage(UUID.fromString(messageId));
 	}
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
index cf4e572..1460c96 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
@@ -12,9 +12,7 @@ import java.util.function.Function;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.RegisterExtension;
-import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.ActiveProfiles;
@@ -48,7 +46,6 @@ import lombok.SneakyThrows;
 @TestPropertySource(properties = {
 		"ozgcloud.osiv2.proxy.enabled=false",
 })
-@ExtendWith(MockitoExtension.class)
 class OsiPostfachRemoteServiceITCase {
 
 	@RegisterExtension
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidatorTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidatorTest.java
new file mode 100644
index 0000000..0a22469
--- /dev/null
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PropertiesValidatorTest.java
@@ -0,0 +1,142 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.config;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+
+class Osi2PropertiesValidatorTest {
+
+	private static final Validator VALIDATOR;
+
+	static {
+		try (var factory = Validation.buildDefaultValidatorFactory()) {
+			VALIDATOR = factory.getValidator();
+		}
+	}
+
+	@DisplayName("validate configuration")
+	@Nested
+	class TestValidateConfiguration {
+
+		@DisplayName("should return if is valid")
+		@Test
+		void shouldReturnIfIsValid() {
+			var validator = new Osi2PropertiesValidator(
+					createAuthConfiguration(),
+					createApiConfiguration(),
+					createProxyConfiguration(),
+					VALIDATOR
+			);
+
+			assertThatCode(validator::validateConfiguration).doesNotThrowAnyException();
+		}
+
+		@DisplayName("should return if is valid with disabled proxy")
+		@Test
+		void shouldReturnIfIsValidWithDisabledProxy() {
+			var validator = new Osi2PropertiesValidator(
+					createAuthConfiguration(),
+					createApiConfiguration(),
+					createDisabledProxyConfiguration(),
+					VALIDATOR
+			);
+
+			assertThatCode(validator::validateConfiguration).doesNotThrowAnyException();
+		}
+
+		static Stream<Arguments> invalidValidatorConfigurations() {
+			return Stream.of(
+					Arguments.of(
+							new Osi2PostfachProperties.AuthConfiguration(),
+							createApiConfiguration(),
+							createProxyConfiguration()
+					),
+					Arguments.of(
+							createAuthConfiguration(),
+							new Osi2PostfachProperties.ApiConfiguration(),
+							createProxyConfiguration()
+					),
+					Arguments.of(
+							createAuthConfiguration(),
+							createApiConfiguration(),
+							createInvalidProxyConfiguration()
+					)
+			);
+		}
+
+		@DisplayName("should throw exception if is invalid")
+		@ParameterizedTest
+		@MethodSource("invalidValidatorConfigurations")
+		void shouldThrowExceptionIfIsInvalid(
+				Osi2PostfachProperties.AuthConfiguration authConfiguration,
+				Osi2PostfachProperties.ApiConfiguration apiConfiguration,
+				Osi2PostfachProperties.ProxyConfiguration proxyConfiguration
+		) {
+			var validator = new Osi2PropertiesValidator(
+					authConfiguration,
+					apiConfiguration,
+					proxyConfiguration,
+					VALIDATOR
+			);
+
+			assertThatThrownBy(validator::validateConfiguration)
+					.isInstanceOf(TechnicalException.class)
+					.hasMessageContaining("is invalid");
+		}
+
+		private static Osi2PostfachProperties.ApiConfiguration createApiConfiguration() {
+			var conf = new Osi2PostfachProperties.ApiConfiguration();
+			conf.setUrl("http://localhost:8080");
+			conf.setTenant("tenant");
+			conf.setNameIdentifier("abc");
+			return conf;
+		}
+
+		private static Osi2PostfachProperties.AuthConfiguration createAuthConfiguration() {
+			var conf = new Osi2PostfachProperties.AuthConfiguration();
+			conf.setClientId("clientId");
+			conf.setClientSecret("clientSecret");
+			conf.setScope(List.of("scope"));
+			conf.setTokenUri("http://localhost:8081");
+			conf.setResource("resource");
+			return conf;
+		}
+
+		private static Osi2PostfachProperties.ProxyConfiguration createProxyConfiguration() {
+			var conf = new Osi2PostfachProperties.ProxyConfiguration();
+			conf.setEnabled(true);
+			conf.setHost("localhost");
+			conf.setPort(8080);
+			return conf;
+		}
+
+		private static Osi2PostfachProperties.ProxyConfiguration createDisabledProxyConfiguration() {
+			var conf = new Osi2PostfachProperties.ProxyConfiguration();
+			conf.setEnabled(false);
+			return conf;
+		}
+
+		private static Osi2PostfachProperties.ProxyConfiguration createInvalidProxyConfiguration() {
+			var conf = new Osi2PostfachProperties.ProxyConfiguration();
+			conf.setEnabled(true);
+			conf.setHost("localhost");
+			conf.setPort(null);
+			return conf;
+		}
+
+	}
+
+}
\ No newline at end of file
-- 
GitLab