From 2d7c5948eab6340dfb06be11a375b079d7008515 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Mon, 2 Dec 2024 17:19:23 +0100
Subject: [PATCH] OZG-7000 OZG-7219 Implement feature toggles

---
 .../admin/common/FeatureToggleProperties.java | 18 ++++++
 .../admin/environment/Environment.java        |  1 +
 .../environment/EnvironmentController.java    |  5 ++
 .../ozgcloud/admin/environment/Features.java  | 12 ++++
 .../admin/environment/FeaturesMapper.java     | 12 ++++
 .../OrganisationsEinheitRootProcessor.java    | 10 ++-
 .../EnvironmentControllerTest.java            | 62 ++++++++++++-------
 .../admin/environment/FeaturesMapperTest.java | 41 ++++++++++++
 ...OrganisationsEinheitRootProcessorTest.java | 28 ++++++++-
 9 files changed, 165 insertions(+), 24 deletions(-)
 create mode 100644 src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java
 create mode 100644 src/main/java/de/ozgcloud/admin/environment/Features.java
 create mode 100644 src/main/java/de/ozgcloud/admin/environment/FeaturesMapper.java
 create mode 100644 src/test/java/de/ozgcloud/admin/environment/FeaturesMapperTest.java

diff --git a/src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java b/src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java
new file mode 100644
index 00000000..75ab7ba1
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java
@@ -0,0 +1,18 @@
+package de.ozgcloud.admin.common;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@Configuration
+@ConfigurationProperties(prefix = "ozgcloud.feature")
+public class FeatureToggleProperties {
+
+	private boolean postfach;
+	private boolean benutzerRollen;
+	private boolean organisationsEinheiten;
+}
diff --git a/src/main/java/de/ozgcloud/admin/environment/Environment.java b/src/main/java/de/ozgcloud/admin/environment/Environment.java
index 0ee03156..79c6775f 100644
--- a/src/main/java/de/ozgcloud/admin/environment/Environment.java
+++ b/src/main/java/de/ozgcloud/admin/environment/Environment.java
@@ -34,4 +34,5 @@ public class Environment {
 	private String authServer;
 	private String clientId;
 	private String realm;
+	private Features features;
 }
diff --git a/src/main/java/de/ozgcloud/admin/environment/EnvironmentController.java b/src/main/java/de/ozgcloud/admin/environment/EnvironmentController.java
index cd1c0417..ea3d58e1 100644
--- a/src/main/java/de/ozgcloud/admin/environment/EnvironmentController.java
+++ b/src/main/java/de/ozgcloud/admin/environment/EnvironmentController.java
@@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import de.ozgcloud.admin.RootController;
+import de.ozgcloud.admin.common.FeatureToggleProperties;
 import lombok.RequiredArgsConstructor;
 
 @RestController("ozgCloudEnvironmentController")
@@ -43,6 +44,9 @@ public class EnvironmentController {
 
 	private final OAuth2Properties oAuthProperties;
 
+	private final FeatureToggleProperties featureToggleProperties;
+	private final FeaturesMapper featuresMapper;
+
 	@GetMapping
 	public Environment getEnvironment() {
 		return Environment.builder()
@@ -51,6 +55,7 @@ public class EnvironmentController {
 				.authServer(oAuthProperties.getAuthServerUrl())
 				.realm(oAuthProperties.getRealm())
 				.clientId(oAuthProperties.getResource())
+				.features(featuresMapper.fromFeatureToggleProperties(featureToggleProperties))
 				.build();
 	}
 }
diff --git a/src/main/java/de/ozgcloud/admin/environment/Features.java b/src/main/java/de/ozgcloud/admin/environment/Features.java
new file mode 100644
index 00000000..1f63a6e8
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/environment/Features.java
@@ -0,0 +1,12 @@
+package de.ozgcloud.admin.environment;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class Features {
+
+	private final boolean postfach;
+	private final boolean benutzerRollen;
+}
diff --git a/src/main/java/de/ozgcloud/admin/environment/FeaturesMapper.java b/src/main/java/de/ozgcloud/admin/environment/FeaturesMapper.java
new file mode 100644
index 00000000..2a7e9d4c
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/environment/FeaturesMapper.java
@@ -0,0 +1,12 @@
+package de.ozgcloud.admin.environment;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.NullValueCheckStrategy;
+
+import de.ozgcloud.admin.common.FeatureToggleProperties;
+
+@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
+interface FeaturesMapper {
+
+	Features fromFeatureToggleProperties(FeatureToggleProperties featureToggleProperties);
+}
diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessor.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessor.java
index 41dd7c0d..1cb98fd6 100644
--- a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessor.java
+++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessor.java
@@ -30,14 +30,20 @@ import org.springframework.hateoas.server.RepresentationModelProcessor;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.admin.Root;
+import de.ozgcloud.admin.common.FeatureToggleProperties;
+import lombok.RequiredArgsConstructor;
 
 @Component
-class OrganisationsEinheitRootProcessor  implements RepresentationModelProcessor<EntityModel<Root>> {
+@RequiredArgsConstructor
+class OrganisationsEinheitRootProcessor implements RepresentationModelProcessor<EntityModel<Root>> {
 
 	static final String REL_ORGANISATIONS_EINHEITEN = "organisationsEinheiten";
 
+	private final FeatureToggleProperties featureToggleProperties;
+
 	@Override
 	public EntityModel<Root> process(EntityModel<Root> model) {
-		return model.add(linkTo(methodOn(OrganisationsEinheitController.class).getAll()).withRel(REL_ORGANISATIONS_EINHEITEN));
+		return model.addIf(featureToggleProperties.isOrganisationsEinheiten(),
+				() -> linkTo(methodOn(OrganisationsEinheitController.class).getAll()).withRel(REL_ORGANISATIONS_EINHEITEN));
 	}
 }
diff --git a/src/test/java/de/ozgcloud/admin/environment/EnvironmentControllerTest.java b/src/test/java/de/ozgcloud/admin/environment/EnvironmentControllerTest.java
index 92c63c39..f9337f41 100644
--- a/src/test/java/de/ozgcloud/admin/environment/EnvironmentControllerTest.java
+++ b/src/test/java/de/ozgcloud/admin/environment/EnvironmentControllerTest.java
@@ -40,6 +40,7 @@ import org.springframework.test.web.servlet.ResultActions;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 
 import de.ozgcloud.admin.RootController;
+import de.ozgcloud.admin.common.FeatureToggleProperties;
 import lombok.SneakyThrows;
 
 class EnvironmentControllerTest {
@@ -52,6 +53,10 @@ class EnvironmentControllerTest {
 	private ProductionProperties environmentProperties;
 	@Mock
 	private OAuth2Properties oAuth2Properties;
+	@Mock
+	private FeatureToggleProperties featureToggleProperties;
+	@Mock
+	private FeaturesMapper featuresMapper;
 
 	private MockMvc mockMvc;
 
@@ -72,29 +77,15 @@ class EnvironmentControllerTest {
 			response.andExpect(status().isOk());
 		}
 
-		@Nested
-		class TestBody {
-
-			@Test
-			@SneakyThrows
-			void shouldCallIsProduction() {
-				when(environmentProperties.isProduction()).thenReturn(true);
-
-				doRequest();
-
-				verify(environmentProperties).isProduction();
-			}
-
-			@ParameterizedTest
-			@ValueSource(booleans = { true, false })
-			@SneakyThrows
-			void shouldContainProduction(boolean production) {
-				when(environmentProperties.isProduction()).thenReturn(production);
+		@ParameterizedTest
+		@ValueSource(booleans = { true, false })
+		@SneakyThrows
+		void shouldContainProduction(boolean production) {
+			when(environmentProperties.isProduction()).thenReturn(production);
 
-				var response = doRequest();
+			var response = doRequest();
 
-				response.andExpect(jsonPath("$.production").value(production));
-			}
+			response.andExpect(jsonPath("$.production").value(production));
 		}
 
 		@Test
@@ -138,6 +129,35 @@ class EnvironmentControllerTest {
 			response.andExpect(jsonPath("$.clientId").value(clientId));
 		}
 
+		@Test
+		void shouldCallFeaturesMapper() {
+			doRequest();
+
+			verify(featuresMapper).fromFeatureToggleProperties(featureToggleProperties);
+		}
+
+		@SneakyThrows
+		@ParameterizedTest
+		@ValueSource(booleans = { true, false })
+		void shouldContainPostfach(boolean postfach) {
+			when(featuresMapper.fromFeatureToggleProperties(featureToggleProperties)).thenReturn(Features.builder().postfach(postfach).build());
+
+			var response = doRequest();
+
+			response.andExpect(jsonPath("$.features.postfach").value(postfach));
+		}
+
+		@SneakyThrows
+		@ParameterizedTest
+		@ValueSource(booleans = { true, false })
+		void shouldContainBenutzerRollen(boolean benutzerRollen) {
+			when(featuresMapper.fromFeatureToggleProperties(featureToggleProperties)).thenReturn(Features.builder().benutzerRollen(benutzerRollen).build());
+
+			var response = doRequest();
+
+			response.andExpect(jsonPath("$.features.benutzerRollen").value(benutzerRollen));
+		}
+
 		@SneakyThrows
 		private ResultActions doRequest() {
 			return mockMvc.perform(get(EnvironmentController.PATH));
diff --git a/src/test/java/de/ozgcloud/admin/environment/FeaturesMapperTest.java b/src/test/java/de/ozgcloud/admin/environment/FeaturesMapperTest.java
new file mode 100644
index 00000000..30742c8a
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/environment/FeaturesMapperTest.java
@@ -0,0 +1,41 @@
+package de.ozgcloud.admin.environment;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mapstruct.factory.Mappers;
+
+import de.ozgcloud.admin.common.FeatureToggleProperties;
+
+class FeaturesMapperTest {
+
+	private FeaturesMapper mapper = Mappers.getMapper(FeaturesMapper.class);
+
+	@Nested
+	class TestFromFeatureToggleProperties {
+
+		private final FeatureToggleProperties props = new FeatureToggleProperties();
+
+		@ParameterizedTest
+		@ValueSource(booleans = {true, false})
+		void shouldMapPostfach(boolean postfach) {
+			props.setPostfach(postfach);
+
+			var features = mapper.fromFeatureToggleProperties(props);
+
+			assertThat(features.isPostfach()).isEqualTo(postfach);
+		}
+
+		@ParameterizedTest
+		@ValueSource(booleans = {true, false})
+		void shouldMapBenutzerRollen(boolean benutzerRollen) {
+			props.setBenutzerRollen(benutzerRollen);
+
+			var features = mapper.fromFeatureToggleProperties(props);
+
+			assertThat(features.isBenutzerRollen()).isEqualTo(benutzerRollen);
+		}
+	}
+}
diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessorTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessorTest.java
index 6fbea1e4..f147d68e 100644
--- a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessorTest.java
+++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessorTest.java
@@ -24,19 +24,24 @@
 package de.ozgcloud.admin.organisationseinheit;
 
 import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
 
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
+import org.mockito.Mock;
 import org.springframework.hateoas.EntityModel;
 import org.springframework.hateoas.Link;
 
 import de.ozgcloud.admin.RootTestFactory;
+import de.ozgcloud.admin.common.FeatureToggleProperties;
 
 class OrganisationsEinheitRootProcessorTest {
 
 	@InjectMocks
 	private OrganisationsEinheitRootProcessor organisationsEinheitRootProcessor;
+	@Mock
+	private FeatureToggleProperties featureToggleProperties;
 
 	@Nested
 	class TestProcess {
@@ -45,14 +50,27 @@ class OrganisationsEinheitRootProcessorTest {
 		class OrganisationsEinheitenLinkRelation {
 
 			@Test
-			void shouldExists() {
+			void shouldExistsIfFeatureEnabled() {
+				givenFeatureIsEnabled();
+
 				var model = organisationsEinheitRootProcessor.process(EntityModel.of(RootTestFactory.create()));
 
 				assertThat(model.getLink(OrganisationsEinheitRootProcessor.REL_ORGANISATIONS_EINHEITEN)).isNotEmpty();
 			}
 
+			@Test
+			void shouldNotExistIfFeatureDisabled() {
+				givenFeatureIsDisabled();
+
+				var model = organisationsEinheitRootProcessor.process(EntityModel.of(RootTestFactory.create()));
+
+				assertThat(model.getLink(OrganisationsEinheitRootProcessor.REL_ORGANISATIONS_EINHEITEN)).isEmpty();
+			}
+
 			@Test
 			void shouldHaveHref() {
+				givenFeatureIsEnabled();
+
 				var model = organisationsEinheitRootProcessor.process(EntityModel.of(RootTestFactory.create()));
 
 				assertThat(model.getLink(OrganisationsEinheitRootProcessor.REL_ORGANISATIONS_EINHEITEN))
@@ -60,6 +78,14 @@ class OrganisationsEinheitRootProcessorTest {
 						.extracting(Link::getHref)
 						.isEqualTo(OrganisationsEinheitController.PATH);
 			}
+
+			private void givenFeatureIsEnabled() {
+				when(featureToggleProperties.isOrganisationsEinheiten()).thenReturn(true);
+			}
+
+			private void givenFeatureIsDisabled() {
+				when(featureToggleProperties.isOrganisationsEinheiten()).thenReturn(false);
+			}
 		}
 
 	}
-- 
GitLab