From a3ba22d6e15baf8d3c6d27173f1783ba21da2e9e Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Thu, 17 Oct 2024 11:51:28 +0200
Subject: [PATCH] OZG-6867 OZG-6895 Return groups with children

---
 pom.xml                                       |   2 +
 .../de/ozgcloud/admin/keycloak/Group.java     |   2 +
 .../ozgcloud/admin/keycloak/GroupMapper.java  |   3 +-
 .../admin/keycloak/KeycloakApiFacade.java     |  20 ++++
 .../admin/keycloak/KeycloakRemoteService.java |  23 +---
 .../admin/keycloak/GroupMapperTest.java       |  12 +-
 .../GroupRepresentationTestFactory.java       |  20 +++-
 .../admin/keycloak/GroupTestFactory.java      |   7 +-
 .../keycloak/KeycloakApiFacadeITCase.java     | 103 ++++++++++++++++++
 .../keycloak/KeycloakRemoteServiceITCase.java |  93 ----------------
 .../keycloak/KeycloakRemoteServiceTest.java   |  63 +++++++++++
 11 files changed, 221 insertions(+), 127 deletions(-)
 create mode 100644 src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiFacade.java
 create mode 100644 src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiFacadeITCase.java
 delete mode 100644 src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceITCase.java
 create mode 100644 src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceTest.java

diff --git a/pom.xml b/pom.xml
index cf1ce94e..6772822c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,6 +22,7 @@
 		<build.number>SET_BY_JENKINS</build.number>
 		<spring-cloud-config-server.version>4.1.2</spring-cloud-config-server.version>
 		<testcontainers-keycloak.version>3.4.0</testcontainers-keycloak.version>
+		<keycloak-admin-client.version>25.0.1</keycloak-admin-client.version>
 		<mongock.version>5.4.0</mongock.version>
 		<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
 		<mapstruct-processor.version>${mapstruct.version}</mapstruct-processor.version>
@@ -76,6 +77,7 @@
 		<dependency>
 			<groupId>org.keycloak</groupId>
 			<artifactId>keycloak-admin-client</artifactId>
+			<version>${keycloak-admin-client.version}</version>
 		</dependency>
 
 		<!-- tools -->
diff --git a/src/main/java/de/ozgcloud/admin/keycloak/Group.java b/src/main/java/de/ozgcloud/admin/keycloak/Group.java
index 4234dff3..177340cd 100644
--- a/src/main/java/de/ozgcloud/admin/keycloak/Group.java
+++ b/src/main/java/de/ozgcloud/admin/keycloak/Group.java
@@ -4,6 +4,7 @@ import java.util.List;
 
 import lombok.Builder;
 import lombok.Getter;
+import lombok.Singular;
 
 @Getter
 @Builder
@@ -11,5 +12,6 @@ public class Group {
 
 	private String name;
 	private String organisationsEinheitId;
+	@Singular
 	private List<Group> subGroups;
 }
diff --git a/src/main/java/de/ozgcloud/admin/keycloak/GroupMapper.java b/src/main/java/de/ozgcloud/admin/keycloak/GroupMapper.java
index 6a8b4996..63793265 100644
--- a/src/main/java/de/ozgcloud/admin/keycloak/GroupMapper.java
+++ b/src/main/java/de/ozgcloud/admin/keycloak/GroupMapper.java
@@ -19,8 +19,7 @@ abstract class GroupMapper {
 	public abstract List<Group> fromGroupRepresentations(List<GroupRepresentation> groupRepresentations);
 
 	@Mapping(target = "organisationsEinheitId", source = "attributes")
-	@Mapping(target = "subGroups", expression = "java(new ArrayList<Group>())")
-	abstract Group fromGroupRepresentation(GroupRepresentation groupRepresentation);
+	public abstract Group fromGroupRepresentation(GroupRepresentation groupRepresentation);
 
 	String getOrganisationsEinheitId(Map<String, List<String>> attributes) {
 		if (attributes == null) {
diff --git a/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiFacade.java b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiFacade.java
new file mode 100644
index 00000000..90640f73
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiFacade.java
@@ -0,0 +1,20 @@
+package de.ozgcloud.admin.keycloak;
+
+import java.util.List;
+
+import org.keycloak.admin.client.resource.GroupsResource;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.springframework.stereotype.Component;
+
+import lombok.RequiredArgsConstructor;
+
+@Component
+@RequiredArgsConstructor
+class KeycloakApiFacade {
+
+	private final GroupsResource groupsResource;
+
+	public List<GroupRepresentation> getAllGroups() {
+		return groupsResource.groups("", 0, Integer.MAX_VALUE, false);
+	}
+}
diff --git a/src/main/java/de/ozgcloud/admin/keycloak/KeycloakRemoteService.java b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakRemoteService.java
index 623e04f4..780e197f 100644
--- a/src/main/java/de/ozgcloud/admin/keycloak/KeycloakRemoteService.java
+++ b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakRemoteService.java
@@ -1,11 +1,7 @@
 package de.ozgcloud.admin.keycloak;
 
-import java.util.List;
 import java.util.stream.Stream;
 
-import org.keycloak.admin.client.resource.GroupResource;
-import org.keycloak.admin.client.resource.GroupsResource;
-import org.keycloak.representations.idm.GroupRepresentation;
 import org.springframework.stereotype.Service;
 
 import lombok.RequiredArgsConstructor;
@@ -14,23 +10,10 @@ import lombok.RequiredArgsConstructor;
 @RequiredArgsConstructor
 public class KeycloakRemoteService {
 
-	private final GroupsResource groupsResource;
+	private KeycloakApiFacade apiFacade;
+	private GroupMapper groupMapper;
 
 	public Stream<Group> getGroups() {
-//		 realmResource.groups().groups();
-		return Stream.empty();
-	}
-
-	// TODO: only for research
-	public List<GroupRepresentation> getGroupRepresentations() {
-		return groupsResource.groups();
-	}
-
-	public GroupResource getGroupResource(String id) {
-		return groupsResource.group(id);
-	}
-
-	public List<GroupRepresentation> getSubGroups(GroupResource parentResource) {
-		return parentResource.getSubGroups(0, parentResource.toRepresentation().getSubGroupCount().intValue(), false);
+		return groupMapper.fromGroupRepresentations(apiFacade.getAllGroups()).stream();
 	}
 }
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/GroupMapperTest.java b/src/test/java/de/ozgcloud/admin/keycloak/GroupMapperTest.java
index e5389ed1..232c6a03 100644
--- a/src/test/java/de/ozgcloud/admin/keycloak/GroupMapperTest.java
+++ b/src/test/java/de/ozgcloud/admin/keycloak/GroupMapperTest.java
@@ -62,7 +62,8 @@ class GroupMapperTest {
 
 		@BeforeEach
 		void init() {
-			doReturn(GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID).when(mapper).getOrganisationsEinheitId(groupRepresentation.getAttributes());
+			doReturn(GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID).when(mapper)
+					.getOrganisationsEinheitId(groupRepresentation.getAttributes());
 		}
 
 		@Test
@@ -87,10 +88,11 @@ class GroupMapperTest {
 		}
 
 		@Test
-		void shouldSetSubGroupsToEmptyList() {
+		void shouldSetSubGroups() {
 			var group = callMapper();
 
-			assertThat(group.getSubGroups()).isEmpty();
+			assertThat(group.getSubGroups()).usingRecursiveFieldByFieldElementComparator()
+					.containsExactlyElementsOf(GroupTestFactory.create().getSubGroups());
 		}
 
 		private Group callMapper() {
@@ -101,8 +103,8 @@ class GroupMapperTest {
 	@Nested
 	class TestFromGroupRepresentations {
 
-		private GroupRepresentation groupRepresentation = GroupRepresentationTestFactory.create();
-		private Group group = GroupTestFactory.createWithEmptySubGroups();
+		private final GroupRepresentation groupRepresentation = GroupRepresentationTestFactory.create();
+		private final Group group = GroupTestFactory.createWithEmptySubGroups();
 
 		@BeforeEach
 		void init() {
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/GroupRepresentationTestFactory.java b/src/test/java/de/ozgcloud/admin/keycloak/GroupRepresentationTestFactory.java
index 9d9df97a..bfa6ba52 100644
--- a/src/test/java/de/ozgcloud/admin/keycloak/GroupRepresentationTestFactory.java
+++ b/src/test/java/de/ozgcloud/admin/keycloak/GroupRepresentationTestFactory.java
@@ -1,5 +1,6 @@
 package de.ozgcloud.admin.keycloak;
 
+import java.util.List;
 import java.util.UUID;
 
 import org.keycloak.representations.idm.GroupRepresentation;
@@ -11,10 +12,25 @@ class GroupRepresentationTestFactory {
 	public static final String NAME = LoremIpsum.getInstance().getName();
 	public static final String ORGANISATIONS_EINHEIT_ID = UUID.randomUUID().toString();
 
+	public static final String SUB_GROUP_NAME = LoremIpsum.getInstance().getName();
+	public static final String SUB_GROUP_ORGANISATIONS_EINHEIT_ID = UUID.randomUUID().toString();
+
 	public static GroupRepresentation create() {
+		var groupRepresentation = create(NAME, ORGANISATIONS_EINHEIT_ID);
+		groupRepresentation.setSubGroups(List.of(create(SUB_GROUP_NAME, SUB_GROUP_ORGANISATIONS_EINHEIT_ID)));
+		return groupRepresentation;
+	}
+
+	public static GroupRepresentation create(String name) {
+		var groupRepresentation = create();
+		groupRepresentation.setName(name);
+		return groupRepresentation;
+	}
+
+	private static GroupRepresentation create(String name, String organisationsEinheitId) {
 		var groupRepresentation = new GroupRepresentation()
-				.singleAttribute(GroupMapper.ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, ORGANISATIONS_EINHEIT_ID);
-		groupRepresentation.setName(NAME);
+				.singleAttribute(GroupMapper.ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, organisationsEinheitId);
+		groupRepresentation.setName(name);
 		return groupRepresentation;
 	}
 }
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/GroupTestFactory.java b/src/test/java/de/ozgcloud/admin/keycloak/GroupTestFactory.java
index bd8413d0..4cef5a38 100644
--- a/src/test/java/de/ozgcloud/admin/keycloak/GroupTestFactory.java
+++ b/src/test/java/de/ozgcloud/admin/keycloak/GroupTestFactory.java
@@ -1,17 +1,14 @@
 package de.ozgcloud.admin.keycloak;
 
 import java.util.List;
-import java.util.UUID;
-
-import com.thedeanda.lorem.LoremIpsum;
 
 class GroupTestFactory {
 
 	public static final String NAME = GroupRepresentationTestFactory.NAME;
 	public static final String ORGANISATIONS_EINHEIT_ID = GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID;
 
-	public static final String SUB_GROUP_NAME = LoremIpsum.getInstance().getName();
-	public static final String SUB_GROUP_ORGANISATIONS_EINHEIT_ID = UUID.randomUUID().toString();
+	public static final String SUB_GROUP_NAME = GroupRepresentationTestFactory.SUB_GROUP_NAME;
+	public static final String SUB_GROUP_ORGANISATIONS_EINHEIT_ID = GroupRepresentationTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID;
 
 	public static Group create() {
 		return createBuilder().build();
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiFacadeITCase.java b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiFacadeITCase.java
new file mode 100644
index 00000000..31c51a7c
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiFacadeITCase.java
@@ -0,0 +1,103 @@
+package de.ozgcloud.admin.keycloak;
+
+import static de.ozgcloud.admin.keycloak.GroupMapper.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.assertj.core.groups.Tuple.tuple;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+
+import de.ozgcloud.admin.common.KeycloakInitializer;
+import de.ozgcloud.common.test.ITCase;
+
+@ITCase
+@ContextConfiguration(initializers = KeycloakInitializer.class)
+class KeycloakApiFacadeITCase {
+
+	@Autowired
+	private KeycloakApiFacade apiFacade;
+
+	@Nested
+	class TestGetAllGroups {
+
+		@Test
+		void shouldReturnAllTopLevelGroups() {
+			var groups = apiFacade.getAllGroups();
+
+			assertThat(groups).hasSize(3).extracting(GroupRepresentation::getName)
+					.containsExactlyInAnyOrder("GroupWithChild", "GroupWithoutOid", "GroupWithoutOidWithChild");
+		}
+
+		@Test
+		void shouldTopLevelGroupsHaveAttributes() {
+			var groups = apiFacade.getAllGroups();
+
+			assertThat(groups).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId)
+					.containsExactlyInAnyOrder(tuple("GroupWithChild", "GroupWithChild-oid"), tuple("GroupWithoutOid", null),
+							tuple("GroupWithoutOidWithChild", null));
+		}
+
+		@Test
+		void shouldGroupWithChildHaveLevel1Children() {
+			var groups = apiFacade.getAllGroups();
+			var groupWithChild = getTopLevelGroup(groups, "GroupWithChild");
+
+			assertThat(groupWithChild.getSubGroups()).hasSize(2).extracting(GroupRepresentation::getName)
+					.containsExactlyInAnyOrder("ChildLevel1WithChild", "ChildLevel1WithoutOid");
+		}
+
+		@Test
+		void shouldLevel1ChildrenOfGroupWithChildHaveAttributes() {
+			var groups = apiFacade.getAllGroups();
+			var groupWithChild = getTopLevelGroup(groups, "GroupWithChild");
+
+			assertThat(groupWithChild.getSubGroups()).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId)
+					.containsExactlyInAnyOrder(tuple("ChildLevel1WithChild", "ChildLevel1WithChild-oid"), tuple("ChildLevel1WithoutOid", null));
+		}
+
+		@Test
+		void shouldLevel1ChildHaveLevel2Children() {
+			var groups = apiFacade.getAllGroups();
+			var level1Children = getTopLevelGroup(groups, "GroupWithChild").getSubGroups();
+			var level1Child = getTopLevelGroup(level1Children, "ChildLevel1WithChild");
+
+			assertThat(level1Child.getSubGroups()).hasSize(1).extracting(GroupRepresentation::getName).contains("ChildLevel2");
+		}
+
+		@Test
+		void shouldGroupWithoutOidHaveLevel1Children() {
+			var groups = apiFacade.getAllGroups();
+			var groupWithoutOid = getTopLevelGroup(groups, "GroupWithoutOidWithChild");
+
+			assertThat(groupWithoutOid.getSubGroups()).hasSize(1).extracting(GroupRepresentation::getName)
+					.containsExactlyInAnyOrder("ChildLevel1");
+		}
+
+		@Test
+		void shouldLevel1ChildrenOfGroupWithoutOidHaveAttributes() {
+			var groups = apiFacade.getAllGroups();
+			var groupWithoutOid = getTopLevelGroup(groups, "GroupWithoutOidWithChild");
+
+			assertThat(groupWithoutOid.getSubGroups()).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId)
+					.containsExactlyInAnyOrder(tuple("ChildLevel1", "ChildLevel1-oid"));
+		}
+
+		private GroupRepresentation getTopLevelGroup(List<GroupRepresentation> groups, String groupName) {
+			var foundGroup = groups.stream().filter(group -> group.getName().equals(groupName)).findFirst();
+			assertThat(foundGroup).isPresent();
+			return foundGroup.get();
+		}
+
+		private String getOrganisationsEinheitId(GroupRepresentation group) {
+			var attributes = group.getAttributes();
+			return attributes.containsKey(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE) ?
+					attributes.get(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE).getFirst() :
+					null;
+		}
+	}
+}
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceITCase.java b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceITCase.java
deleted file mode 100644
index bb72d655..00000000
--- a/src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceITCase.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package de.ozgcloud.admin.keycloak;
-
-import static org.assertj.core.api.Assertions.*;
-
-import java.util.List;
-
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.keycloak.representations.idm.GroupRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-
-import de.ozgcloud.admin.common.KeycloakInitializer;
-import de.ozgcloud.common.test.ITCase;
-
-@ITCase
-@ContextConfiguration(initializers = KeycloakInitializer.class)
-class KeycloakRemoteServiceITCase {
-
-	@Autowired
-	private KeycloakRemoteService service;
-
-	@Nested
-	class TestGetGroupRepresentations {
-
-		@Test
-		void shouldReturnGroupRepresentations() {
-			var result = service.getGroupRepresentations();
-
-			assertThat(result).hasSize(3);
-		}
-
-		@Test
-		void shouldNotHaveAttributes() {
-			var result = service.getGroupRepresentations();
-
-			assertThat(result.getFirst()).extracting(GroupRepresentation::getName, GroupRepresentation::getAttributes)
-					.containsExactly("GroupWithChild", null);
-		}
-
-		@Test
-		void shouldHaveSubGroupCount() {
-			var result = service.getGroupRepresentations();
-
-			assertThat(result.getFirst()).extracting(GroupRepresentation::getName, GroupRepresentation::getSubGroupCount)
-					.containsExactly("GroupWithChild", 2L);
-		}
-
-		@Test
-		void shouldNotHaveSubGroups() {
-			var result = service.getGroupRepresentations();
-
-			assertThat(result.getFirst()).extracting(GroupRepresentation::getName, GroupRepresentation::getSubGroups)
-					.containsExactly("GroupWithChild", List.of());
-		}
-	}
-
-	@Nested
-	class TestGetGroupResource {
-
-		@Test
-		void shouldHaveAttributes() {
-			var result = service.getGroupResource("GroupWithChild-id").toRepresentation();
-
-			assertThat(result.getAttributes().get("organisationseinheitId").getFirst()).isEqualTo("GroupWithChild-oid");
-		}
-
-		@Test
-		void shouldHaveSubGroupCount() {
-			var result = service.getGroupResource("GroupWithChild-id").toRepresentation();
-
-			assertThat(result.getSubGroupCount()).isEqualTo(2);
-		}
-
-		@Test
-		void shouldNotHaveSubGroups() {
-			var result = service.getGroupResource("GroupWithChild-id").toRepresentation();
-
-			assertThat(result.getSubGroups()).isEmpty();
-		}
-	}
-
-	@Nested
-	class TestGetSubGroups {
-
-		@Test
-		void shouldReturnSubGroups() {
-			var result = service.getSubGroups(service.getGroupResource("GroupWithChild-id"));
-
-			assertThat(result).hasSize(2);
-		}
-	}
-}
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceTest.java b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceTest.java
new file mode 100644
index 00000000..bbc2a6c1
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceTest.java
@@ -0,0 +1,63 @@
+package de.ozgcloud.admin.keycloak;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+class KeycloakRemoteServiceTest {
+
+	@Mock
+	private KeycloakApiFacade apiFacade;
+	@Mock
+	private GroupMapper mapper;
+	@Spy
+	@InjectMocks
+	private KeycloakRemoteService service;
+
+	@Nested
+	class TestGetGroups {
+
+		private final List<GroupRepresentation> groupRepresentations = List.of(
+			GroupRepresentationTestFactory.create("A"), GroupRepresentationTestFactory.create("B")
+		);
+		private final List<Group> mappedGroups = List.of(
+			GroupTestFactory.createBuilder().name("A").build(), GroupTestFactory.createBuilder().name("B").build()
+		);
+
+		@BeforeEach
+		void init() {
+			when(apiFacade.getAllGroups()).thenReturn(groupRepresentations);
+			when(mapper.fromGroupRepresentations(groupRepresentations)).thenReturn(mappedGroups);
+		}
+
+		@Test
+		void shouldGetAllGroups() {
+			service.getGroups();
+
+			verify(apiFacade).getAllGroups();
+		}
+
+		@Test
+		void shouldMapGroups() {
+			service.getGroups();
+
+			verify(mapper).fromGroupRepresentations(groupRepresentations);
+		}
+
+		@Test
+		void shouldReturnMappedGroups() {
+			var groups = service.getGroups();
+
+			assertThat(groups).containsExactlyElementsOf(mappedGroups);
+		}
+	}
+}
-- 
GitLab