Skip to content
Snippets Groups Projects
Commit e4815c56 authored by OZGCloud's avatar OZGCloud
Browse files

OZG-6867 OZG-7002 Add method addGroup to KeycloakApiService

parent f63e6610
No related branches found
No related tags found
No related merge requests found
...@@ -4,17 +4,37 @@ import java.util.List; ...@@ -4,17 +4,37 @@ import java.util.List;
import org.keycloak.admin.client.resource.GroupsResource; import org.keycloak.admin.client.resource.GroupsResource;
import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.GroupRepresentation;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Service;
import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@Component @Service
@RequiredArgsConstructor @RequiredArgsConstructor
class KeycloakApiFacade { class KeycloakApiService {
private final GroupsResource groupsResource; private final GroupsResource groupsResource;
public List<GroupRepresentation> getAllGroups() { public List<GroupRepresentation> getAllGroups() {
return groupsResource.groups("", 0, Integer.MAX_VALUE, false); return groupsResource.groups("", 0, Integer.MAX_VALUE, false);
} }
public String addGroup(GroupRepresentation groupRepresentation) {
try (var response = groupsResource.add(groupRepresentation)) {
return getAddedResourceIdFromResponse(response);
}
}
String getAddedResourceIdFromResponse(Response response) {
if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
return extractResourceIdFromLocationHeader(response.getHeaderString("Location"));
}
throw new ResourceCreationException(
"Failed to add group - got response with status %d (%s) and headers %s".formatted(response.getStatus(), response.getStatusInfo()
.getReasonPhrase(), response.getHeaders()));
}
String extractResourceIdFromLocationHeader(String location) {
return location.substring(location.lastIndexOf('/') + 1);
}
} }
...@@ -10,10 +10,10 @@ import lombok.RequiredArgsConstructor; ...@@ -10,10 +10,10 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
public class KeycloakRemoteService { public class KeycloakRemoteService {
private final KeycloakApiFacade apiFacade; private final KeycloakApiService apiService;
private final GroupMapper groupMapper; private final GroupMapper groupMapper;
public Stream<Group> getGroupsWithOrganisationsEinheitId() { public Stream<Group> getGroupsWithOrganisationsEinheitId() {
return groupMapper.fromGroupRepresentations(apiFacade.getAllGroups()).stream(); return groupMapper.fromGroupRepresentations(apiService.getAllGroups()).stream();
} }
} }
package de.ozgcloud.admin.keycloak;
public class ResourceCreationException extends RuntimeException {
public ResourceCreationException(String message) {
super(message);
}
}
...@@ -23,12 +23,7 @@ class GroupRepresentationTestFactory { ...@@ -23,12 +23,7 @@ class GroupRepresentationTestFactory {
public static final Map<String, List<String>> SUB_GROUP_ATTRIBUTES = Map.of(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, List.of(SUB_GROUP_ORGANISATIONS_EINHEIT_ID)); public static final Map<String, List<String>> SUB_GROUP_ATTRIBUTES = Map.of(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, List.of(SUB_GROUP_ORGANISATIONS_EINHEIT_ID));
public static GroupRepresentation create() { public static GroupRepresentation create() {
var groupRepresentation = new GroupRepresentation(); return create(true);
groupRepresentation.setId(ID);
groupRepresentation.setName(NAME);
groupRepresentation.setAttributes(ATTRIBUTES);
groupRepresentation.setSubGroups(List.of(createSubGroup()));
return groupRepresentation;
} }
public static GroupRepresentation create(String name) { public static GroupRepresentation create(String name) {
...@@ -37,9 +32,28 @@ class GroupRepresentationTestFactory { ...@@ -37,9 +32,28 @@ class GroupRepresentationTestFactory {
return groupRepresentation; return groupRepresentation;
} }
private static GroupRepresentation createSubGroup() { public static GroupRepresentation createWithoutId(String name) {
var groupRepresentation = create(false);
groupRepresentation.setName(name);
return groupRepresentation;
}
public static GroupRepresentation create(boolean withId) {
var groupRepresentation = new GroupRepresentation(); var groupRepresentation = new GroupRepresentation();
if (withId) {
groupRepresentation.setId(ID);
}
groupRepresentation.setName(NAME);
groupRepresentation.setAttributes(ATTRIBUTES);
groupRepresentation.setSubGroups(List.of(createSubGroup(withId)));
return groupRepresentation;
}
private static GroupRepresentation createSubGroup(boolean withId) {
var groupRepresentation = new GroupRepresentation();
if (withId) {
groupRepresentation.setId(SUB_GROUP_ID); groupRepresentation.setId(SUB_GROUP_ID);
}
groupRepresentation.setName(SUB_GROUP_NAME); groupRepresentation.setName(SUB_GROUP_NAME);
groupRepresentation.setAttributes(SUB_GROUP_ATTRIBUTES); groupRepresentation.setAttributes(SUB_GROUP_ATTRIBUTES);
return groupRepresentation; return groupRepresentation;
......
...@@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.*; ...@@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.groups.Tuple.tuple; import static org.assertj.core.groups.Tuple.tuple;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
...@@ -11,15 +12,17 @@ import org.keycloak.representations.idm.GroupRepresentation; ...@@ -11,15 +12,17 @@ import org.keycloak.representations.idm.GroupRepresentation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import com.thedeanda.lorem.LoremIpsum;
import de.ozgcloud.admin.common.KeycloakInitializer; import de.ozgcloud.admin.common.KeycloakInitializer;
import de.ozgcloud.common.test.ITCase; import de.ozgcloud.common.test.ITCase;
@ITCase @ITCase
@ContextConfiguration(initializers = KeycloakInitializer.class) @ContextConfiguration(initializers = KeycloakInitializer.class)
class KeycloakApiFacadeITCase { class KeycloakApiServiceITCase {
@Autowired @Autowired
private KeycloakApiFacade apiFacade; private KeycloakApiService service;
@Autowired @Autowired
private KeycloakApiProperties properties; private KeycloakApiProperties properties;
...@@ -28,7 +31,7 @@ class KeycloakApiFacadeITCase { ...@@ -28,7 +31,7 @@ class KeycloakApiFacadeITCase {
@Test @Test
void shouldReturnAllTopLevelGroups() { void shouldReturnAllTopLevelGroups() {
var groups = apiFacade.getAllGroups(); var groups = service.getAllGroups();
assertThat(groups).hasSize(3).extracting(GroupRepresentation::getName) assertThat(groups).hasSize(3).extracting(GroupRepresentation::getName)
.containsExactlyInAnyOrder("GroupWithChild", "GroupWithoutOid", "GroupWithoutOidWithChild"); .containsExactlyInAnyOrder("GroupWithChild", "GroupWithoutOid", "GroupWithoutOidWithChild");
...@@ -36,7 +39,7 @@ class KeycloakApiFacadeITCase { ...@@ -36,7 +39,7 @@ class KeycloakApiFacadeITCase {
@Test @Test
void shouldTopLevelGroupsHaveAttributes() { void shouldTopLevelGroupsHaveAttributes() {
var groups = apiFacade.getAllGroups(); var groups = service.getAllGroups();
assertThat(groups).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId) assertThat(groups).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId)
.containsExactlyInAnyOrder(tuple("GroupWithChild", "GroupWithChild-oid"), tuple("GroupWithoutOid", null), .containsExactlyInAnyOrder(tuple("GroupWithChild", "GroupWithChild-oid"), tuple("GroupWithoutOid", null),
...@@ -45,7 +48,7 @@ class KeycloakApiFacadeITCase { ...@@ -45,7 +48,7 @@ class KeycloakApiFacadeITCase {
@Test @Test
void shouldGroupWithChildHaveLevel1Children() { void shouldGroupWithChildHaveLevel1Children() {
var groups = apiFacade.getAllGroups(); var groups = service.getAllGroups();
var groupWithChild = getTopLevelGroup(groups, "GroupWithChild"); var groupWithChild = getTopLevelGroup(groups, "GroupWithChild");
assertThat(groupWithChild.getSubGroups()).hasSize(2).extracting(GroupRepresentation::getName) assertThat(groupWithChild.getSubGroups()).hasSize(2).extracting(GroupRepresentation::getName)
...@@ -54,7 +57,7 @@ class KeycloakApiFacadeITCase { ...@@ -54,7 +57,7 @@ class KeycloakApiFacadeITCase {
@Test @Test
void shouldLevel1ChildrenOfGroupWithChildHaveAttributes() { void shouldLevel1ChildrenOfGroupWithChildHaveAttributes() {
var groups = apiFacade.getAllGroups(); var groups = service.getAllGroups();
var groupWithChild = getTopLevelGroup(groups, "GroupWithChild"); var groupWithChild = getTopLevelGroup(groups, "GroupWithChild");
assertThat(groupWithChild.getSubGroups()).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId) assertThat(groupWithChild.getSubGroups()).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId)
...@@ -63,7 +66,7 @@ class KeycloakApiFacadeITCase { ...@@ -63,7 +66,7 @@ class KeycloakApiFacadeITCase {
@Test @Test
void shouldLevel1ChildHaveLevel2Children() { void shouldLevel1ChildHaveLevel2Children() {
var groups = apiFacade.getAllGroups(); var groups = service.getAllGroups();
var level1Children = getTopLevelGroup(groups, "GroupWithChild").getSubGroups(); var level1Children = getTopLevelGroup(groups, "GroupWithChild").getSubGroups();
var level1Child = getTopLevelGroup(level1Children, "ChildLevel1WithChild"); var level1Child = getTopLevelGroup(level1Children, "ChildLevel1WithChild");
...@@ -72,7 +75,7 @@ class KeycloakApiFacadeITCase { ...@@ -72,7 +75,7 @@ class KeycloakApiFacadeITCase {
@Test @Test
void shouldGroupWithoutOidHaveLevel1Children() { void shouldGroupWithoutOidHaveLevel1Children() {
var groups = apiFacade.getAllGroups(); var groups = service.getAllGroups();
var groupWithoutOid = getTopLevelGroup(groups, "GroupWithoutOidWithChild"); var groupWithoutOid = getTopLevelGroup(groups, "GroupWithoutOidWithChild");
assertThat(groupWithoutOid.getSubGroups()).hasSize(1).extracting(GroupRepresentation::getName) assertThat(groupWithoutOid.getSubGroups()).hasSize(1).extracting(GroupRepresentation::getName)
...@@ -81,7 +84,7 @@ class KeycloakApiFacadeITCase { ...@@ -81,7 +84,7 @@ class KeycloakApiFacadeITCase {
@Test @Test
void shouldLevel1ChildrenOfGroupWithoutOidHaveAttributes() { void shouldLevel1ChildrenOfGroupWithoutOidHaveAttributes() {
var groups = apiFacade.getAllGroups(); var groups = service.getAllGroups();
var groupWithoutOid = getTopLevelGroup(groups, "GroupWithoutOidWithChild"); var groupWithoutOid = getTopLevelGroup(groups, "GroupWithoutOidWithChild");
assertThat(groupWithoutOid.getSubGroups()).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId) assertThat(groupWithoutOid.getSubGroups()).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId)
...@@ -101,4 +104,67 @@ class KeycloakApiFacadeITCase { ...@@ -101,4 +104,67 @@ class KeycloakApiFacadeITCase {
null; null;
} }
} }
@Nested
class TestAddGroup {
@Test
void shouldReturnId() {
var groupToAdd = createUniqueGroupRepresentation("shouldReturnId");
var groupId = service.addGroup(groupToAdd);
assertThat(groupId).isNotBlank();
}
@Test
void shouldAddGroup() {
var groupToAdd = createUniqueGroupRepresentation("shouldAddGroup");
var groupId = service.addGroup(groupToAdd);
assertThat(findGroupInKeycloak(groupId)).isPresent().get().extracting(GroupRepresentation::getName, GroupRepresentation::getAttributes)
.containsExactly(groupToAdd.getName(), groupToAdd.getAttributes());
}
@Test
void shouldThrowExceptionWhenAddingGroupWithId() {
var groupToAdd = GroupRepresentationTestFactory.create();
assertThatExceptionOfType(ResourceCreationException.class).isThrownBy(() -> service.addGroup(groupToAdd))
.withMessageContaining("404");
}
@Test
void shouldThrowExceptionWhenNameIsNotUnique() {
var groupToAdd = createUniqueGroupRepresentation("notUniqueName");
service.addGroup(groupToAdd);
assertThatExceptionOfType(ResourceCreationException.class).isThrownBy(() -> service.addGroup(groupToAdd))
.withMessageContaining("409");
}
@Test
void shouldNotAddSubgroups() {
var groupToAdd = createUniqueGroupRepresentation("shouldNotAddSubgroups");
var groupId = service.addGroup(groupToAdd);
assertThat(findGroupInKeycloak(groupId)).isPresent().get().extracting(GroupRepresentation::getSubGroups)
.asList().isEmpty();
}
private GroupRepresentation createUniqueGroupRepresentation(String suffix) {
// LoremIpsum does not guarantee unique results when called repeatedly
var groupName = "%s (%s)".formatted(LoremIpsum.getInstance().getName(), suffix);
return GroupRepresentationTestFactory.createWithoutId(groupName);
}
private Optional<GroupRepresentation> findGroupInKeycloak(String groupId) {
return service.getAllGroups().stream()
.filter(groupRepresentation -> groupId.equals(groupRepresentation.getId()))
.findFirst();
}
}
} }
package de.ozgcloud.admin.keycloak;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.resource.GroupsResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import com.thedeanda.lorem.LoremIpsum;
import jakarta.ws.rs.core.Response;
class KeycloakApiServiceTest {
@Spy
@InjectMocks
private KeycloakApiService service;
@Mock
private GroupsResource groupsResource;
@Mock
private Response response;
@Nested
class TestAddGroup {
private final GroupRepresentation groupRepresentation = GroupRepresentationTestFactory.create();
private final String RESOURCE_ID = GroupRepresentationTestFactory.create().getId();
@BeforeEach
void init() {
when(groupsResource.add(groupRepresentation)).thenReturn(response);
doReturn(RESOURCE_ID).when(service).getAddedResourceIdFromResponse(response);
}
@Test
void shouldCallGroupsResource() {
callService();
verify(groupsResource).add(groupRepresentation);
}
@Test
void shouldGetAddedResourceIdFromResponse() {
callService();
verify(service).getAddedResourceIdFromResponse(response);
}
@Test
void shouldReturnAddedResourceId() {
var id = callService();
assertThat(id).isEqualTo(RESOURCE_ID);
}
private String callService() {
return service.addGroup(groupRepresentation);
}
}
@Nested
class TestGetAddedResourceIdFromResponse {
private final String RESOURCE_ID = GroupRepresentationTestFactory.create().getId();
private final String LOCATION = LoremIpsum.getInstance().getUrl();
@Test
void shouldExtractResourceIdFromLocationHeader() {
givenResponseCreated();
callService();
verify(service).extractResourceIdFromLocationHeader(LOCATION);
}
@Test
void shouldReturnResourceId() {
givenResponseCreated();
var id = callService();
assertThat(id).isEqualTo(RESOURCE_ID);
}
@Test
void shouldThrowExceptionIsStatusOtherThenCreated() {
givenResponseUnauthorized();
assertThatExceptionOfType(ResourceCreationException.class).isThrownBy(this::callService)
.withMessageStartingWith("Failed to add group - got response with status 403 (Unauthorized)");
}
private void givenResponseCreated() {
when(response.getStatus()).thenReturn(201);
when(response.getHeaderString("Location")).thenReturn(LOCATION);
doReturn(RESOURCE_ID).when(service).extractResourceIdFromLocationHeader(LOCATION);
}
private void givenResponseUnauthorized() {
when(response.getStatus()).thenReturn(403);
when(response.getStatusInfo()).thenReturn(Response.Status.UNAUTHORIZED);
}
private String callService() {
return service.getAddedResourceIdFromResponse(response);
}
}
@Nested
class TestExtractResourceIdFromLocationHeader {
private final String RESOURCE_ID = GroupRepresentationTestFactory.create().getId();
private final String LOCATION = "https://keycloak-url.test/admin/realms/test/groups/" + RESOURCE_ID;
@Test
void shouldReturnId() {
var id = service.extractResourceIdFromLocationHeader(LOCATION);
assertThat(id).isEqualTo(RESOURCE_ID);
}
}
}
...@@ -16,7 +16,7 @@ import org.mockito.Spy; ...@@ -16,7 +16,7 @@ import org.mockito.Spy;
class KeycloakRemoteServiceTest { class KeycloakRemoteServiceTest {
@Mock @Mock
private KeycloakApiFacade apiFacade; private KeycloakApiService apiService;
@Mock @Mock
private GroupMapper mapper; private GroupMapper mapper;
@Spy @Spy
...@@ -35,7 +35,7 @@ class KeycloakRemoteServiceTest { ...@@ -35,7 +35,7 @@ class KeycloakRemoteServiceTest {
@BeforeEach @BeforeEach
void init() { void init() {
when(apiFacade.getAllGroups()).thenReturn(groupRepresentations); when(apiService.getAllGroups()).thenReturn(groupRepresentations);
when(mapper.fromGroupRepresentations(groupRepresentations)).thenReturn(mappedGroups); when(mapper.fromGroupRepresentations(groupRepresentations)).thenReturn(mappedGroups);
} }
...@@ -43,7 +43,7 @@ class KeycloakRemoteServiceTest { ...@@ -43,7 +43,7 @@ class KeycloakRemoteServiceTest {
void shouldGetAllGroups() { void shouldGetAllGroups() {
service.getGroupsWithOrganisationsEinheitId(); service.getGroupsWithOrganisationsEinheitId();
verify(apiFacade).getAllGroups(); verify(apiService).getAllGroups();
} }
@Test @Test
......
...@@ -2137,7 +2137,7 @@ ...@@ -2137,7 +2137,7 @@
} }
], ],
"clientRoles" : { "clientRoles" : {
"realm-management" : [ "view-users" ] "realm-management" : [ "view-users", "manage-users" ]
} }
} }
] ]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment