diff --git a/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java b/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java index 2bcf76243..b53671f03 100644 --- a/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java +++ b/src/main/java/de/adorsys/keycloak/config/service/GroupImportService.java @@ -26,6 +26,7 @@ import de.adorsys.keycloak.config.properties.ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues; import de.adorsys.keycloak.config.repository.GroupRepository; import de.adorsys.keycloak.config.util.CloneUtil; +import de.adorsys.keycloak.config.util.ThreadSleepUtil; import org.keycloak.representations.idm.GroupRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +34,6 @@ import org.springframework.util.CollectionUtils; import java.util.*; -import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -145,6 +145,10 @@ private void createGroup(String realmName, GroupRepresentation group) { addSubGroups(realmName, patchedGroup); } + /** + * This method retries the GET call to the created group as it fails in parallel mode pretty often. + * It uses a ramp from 0 milliseconds increasing to a square of the retryCount times a fixed number (500 milliseconds) as delay. + */ private GroupRepresentation loadCreatedGroupUsingRamp(String realmName, String groupName, int retryCount) { if (retryCount >= LOAD_CREATED_GROUP_MAX_RETRIES) { throw new ImportProcessingException("Cannot find created group '%s' in realm '%s'", groupName, realmName); @@ -157,7 +161,7 @@ private GroupRepresentation loadCreatedGroupUsingRamp(String realmName, String g } try { - TimeUnit.MILLISECONDS.sleep(250L * retryCount * retryCount); + ThreadSleepUtil.sleep(500L * retryCount * retryCount); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } diff --git a/src/main/java/de/adorsys/keycloak/config/util/ThreadSleepUtil.java b/src/main/java/de/adorsys/keycloak/config/util/ThreadSleepUtil.java new file mode 100644 index 000000000..ed1d1141c --- /dev/null +++ b/src/main/java/de/adorsys/keycloak/config/util/ThreadSleepUtil.java @@ -0,0 +1,32 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package de.adorsys.keycloak.config.util; + +public class ThreadSleepUtil { + + private ThreadSleepUtil() { + } + + public static void sleep(long millis) throws InterruptedException { + Thread.sleep(millis); + } + +} diff --git a/src/test/java/de/adorsys/keycloak/config/service/GroupImportServiceTest.java b/src/test/java/de/adorsys/keycloak/config/service/GroupImportServiceTest.java new file mode 100644 index 000000000..f3987a5b3 --- /dev/null +++ b/src/test/java/de/adorsys/keycloak/config/service/GroupImportServiceTest.java @@ -0,0 +1,169 @@ +/*- + * ---license-start + * keycloak-config-cli + * --- + * Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + */ + +package de.adorsys.keycloak.config.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import de.adorsys.keycloak.config.exception.ImportProcessingException; +import de.adorsys.keycloak.config.properties.ImportConfigProperties; +import de.adorsys.keycloak.config.repository.GroupRepository; +import de.adorsys.keycloak.config.util.ThreadSleepUtil; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.keycloak.representations.idm.GroupRepresentation; +import org.mockito.MockedStatic; +import org.mockito.stubbing.OngoingStubbing; + +class GroupImportServiceTest { + + private final GroupRepository groupRepository = mock(GroupRepository.class); + + private final ImportConfigProperties importConfigProperties = mock(ImportConfigProperties.class); + + private final GroupImportService groupImportService = + new GroupImportService(groupRepository, importConfigProperties); + + @Nested + class CreatingGroupIT { + + private final String realmName = "someRealm"; + + private final String groupId = "someGroupId"; + + private final String groupName = "someGroup"; + + private final List realmRoles = List.of("someRealmRole"); + + private final String clientId = "someClientId"; + + private final List clientRoleNames = List.of("someClientRoleName"); + + private final GroupRepresentation subGroup = new GroupRepresentation(); + + private final GroupRepresentation group = new GroupRepresentation(); + + @BeforeEach + void init() { + group.setId(groupId); + group.setName(groupName); + group.setRealmRoles(realmRoles); + group.setClientRoles(Map.of(clientId, clientRoleNames)); + group.setSubGroups(List.of(subGroup)); + + String subGroupName = "someSubGroupName"; + subGroup.setName(subGroupName); + + when(groupRepository.getGroupByName(realmName, groupName)).thenReturn(null).thenReturn(group); + when(groupRepository.getSubGroupByName(realmName, groupId, subGroupName)).thenReturn(subGroup); + } + + @Test + void createOrUpdateGroups_shouldCreateGroup() { + groupImportService.createOrUpdateGroups(List.of(group), realmName); + verify(groupRepository).createGroup(realmName, group); + } + + @Test + void createOrUpdateGroups_shouldAddRealmRoles() { + groupImportService.createOrUpdateGroups(List.of(group), realmName); + verify(groupRepository).addRealmRoles(realmName, groupId, realmRoles); + } + + @Test + void createOrUpdateGroups_shouldAddClientRoles() { + groupImportService.createOrUpdateGroups(List.of(group), realmName); + verify(groupRepository).addClientRoles(realmName, groupId, clientId, clientRoleNames); + } + + @Test + void createOrUpdateGroups_shouldAddSubGroups() { + groupImportService.createOrUpdateGroups(List.of(group), realmName); + verify(groupRepository).addSubGroup(realmName, groupId, subGroup); + } + + @Test + void createOrUpdateGroups_shouldPassInterruptWhileWaitingForRetries() { + try (MockedStatic util = mockStatic(ThreadSleepUtil.class)) { + util.when(() -> ThreadSleepUtil.sleep(0L)).thenThrow(new InterruptedException()); + + when(groupRepository.getGroupByName(realmName, groupName)) + .thenReturn(null) + .thenReturn(null) + .thenReturn(group); + + assertThat(Thread.interrupted()).isFalse(); + + groupImportService.createOrUpdateGroups(List.of(group), realmName); + + assertThat(Thread.interrupted()).isTrue(); + } + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + void createOrUpdateGroups_shouldRetryGettingCreatedGroup(int retries) { + OngoingStubbing getGroupNameStubbing = + when(groupRepository.getGroupByName(realmName, groupName)); + + for (int i = 0; i < retries - 1; i++) { + getGroupNameStubbing = getGroupNameStubbing.thenReturn(null); + } + + getGroupNameStubbing.thenReturn(group); + + groupImportService.createOrUpdateGroups(List.of(group), realmName); + + verify(groupRepository, times(retries)).getGroupByName(realmName, groupName); + } + + @Test + void createOrUpdateGroups_shouldStopImportWithNullAfterEveryRetry() { + when(groupRepository.getGroupByName(realmName, groupName)) + .thenReturn(null) + .thenReturn(null) + .thenReturn(null) + .thenReturn(null) + .thenReturn(null) + .thenReturn(null) + .thenReturn(group); + + ImportProcessingException exception = assertThrows( + ImportProcessingException.class, + () -> groupImportService.createOrUpdateGroups(List.of(group), realmName) + ); + + assertThat(exception) + .message() + .isEqualTo(String.format("Cannot find created group '%s' in realm '%s'", groupName, realmName)); + } + } +}