diff --git a/Jenkinsfile b/Jenkinsfile index 4ae2de8d852246f9bf8464631f225e5837cd246b..ce7f72860a1c5fb91d5b0672ef59500f6c294b31 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -358,5 +358,5 @@ String generateHelmChartVersion() { chartVersion += "-${env.BRANCH_NAME}" } - return chartVersion.replaceAll("_", "-") + return chartVersion.replaceAll("_", "-").take(63 - "administration-".length()) } diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000000000000000000000000000000000000..d07dd9b0e2b0281fbf514a968b9451cb6af62f93 --- /dev/null +++ b/lombok.config @@ -0,0 +1,30 @@ +# +# Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den +# Ministerpräsidenten des Landes Schleswig-Holstein +# Staatskanzlei +# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung +# +# Lizenziert unter der EUPL, Version 1.2 oder - sobald +# diese von der Europäischen Kommission genehmigt wurden - +# Folgeversionen der EUPL ("Lizenz"); +# Sie dürfen dieses Werk ausschließlich gemäß +# dieser Lizenz nutzen. +# Eine Kopie der Lizenz finden Sie hier: +# +# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 +# +# Sofern nicht durch anwendbare Rechtsvorschriften +# gefordert oder in schriftlicher Form vereinbart, wird +# die unter der Lizenz verbreitete Software "so wie sie +# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - +# ausdrücklich oder stillschweigend - verbreitet. +# Die sprachspezifischen Genehmigungen und Beschränkungen +# unter der Lizenz sind dem Lizenztext zu entnehmen. +# + +lombok.log.fieldName=LOG +lombok.log.slf4j.flagUsage = ERROR +lombok.log.log4j.flagUsage = ERROR +lombok.data.flagUsage = ERROR +lombok.nonNull.exceptionType = IllegalArgumentException +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1ba539b41d20ebebf73a96d7fa0109cc845021d3..915ccea3e9f6dfbca7d73078bb0a325dc625d97e 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ <groupId>de.ozgcloud.common</groupId> <artifactId>ozgcloud-common-parent</artifactId> <version>4.3.2</version> - <relativePath /> + <relativePath/> </parent> <groupId>de.ozgcloud</groupId> <artifactId>administration</artifactId> @@ -21,15 +21,32 @@ <publishImage>false</publishImage> <build.number>SET_BY_JENKINS</build.number> <spring-cloud-config-server.version>4.1.2</spring-cloud-config-server.version> - <testcontainers-keycloak.version>3.2.0</testcontainers-keycloak.version> - <keycloak-admin-client.version>23.0.6</keycloak-admin-client.version> + <testcontainers-keycloak.version>3.3.1</testcontainers-keycloak.version> + <keycloak-admin-client.version>24.0.5</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> + <zufi-manager.version>1.5.0-SNAPSHOT</zufi-manager.version> + <shedlock.version>5.16.0</shedlock.version> </properties> <dependencies> + <!-- OZG Cloud API --> + <dependency> + <groupId>de.ozgcloud.zufi</groupId> + <artifactId>zufi-manager-interface</artifactId> + <version>${zufi-manager.version}</version> + </dependency> + <!-- Spring --> + <dependency> + <groupId>net.devh</groupId> + <artifactId>grpc-client-spring-boot-starter</artifactId> + </dependency> + <dependency> + <groupId>io.grpc</groupId> + <artifactId>grpc-inprocess</artifactId> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> @@ -72,6 +89,26 @@ <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> + + <!-- ShedLock --> + <dependency> + <groupId>net.javacrumbs.shedlock</groupId> + <artifactId>shedlock-spring</artifactId> + <version>${shedlock.version}</version> + </dependency> + <dependency> + <groupId>net.javacrumbs.shedlock</groupId> + <artifactId>shedlock-provider-mongo</artifactId> + <version>${shedlock.version}</version> + </dependency> + + <!-- Keycloak --> + <dependency> + <groupId>org.keycloak</groupId> + <artifactId>keycloak-admin-client</artifactId> + <version>${keycloak-admin-client.version}</version> + </dependency> + <!-- tools --> <dependency> <groupId>org.mapstruct</groupId> @@ -83,6 +120,20 @@ <version>${mapstruct-processor.version}</version> </dependency> + <!-- commons --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + <dependency> + <groupId>commons-beanutils</groupId> + <artifactId>commons-beanutils</artifactId> + </dependency> + <!-- mongock --> <dependency> <groupId>io.mongock</groupId> @@ -145,12 +196,6 @@ <version>${testcontainers-keycloak.version}</version> <scope>test</scope> </dependency> - <dependency> - <groupId>org.keycloak</groupId> - <artifactId>keycloak-admin-client</artifactId> - <version>${keycloak-admin-client.version}</version> - <scope>test</scope> - </dependency> </dependencies> <profiles> <profile> diff --git a/src/main/helm/templates/_helpers.tpl b/src/main/helm/templates/_helpers.tpl index f7a32d59fcfec7e5137d0f7e80fd20cab36e6710..e1ad80c9e73a351f2cdaac3e730bfaa3b5995c60 100644 --- a/src/main/helm/templates/_helpers.tpl +++ b/src/main/helm/templates/_helpers.tpl @@ -23,10 +23,6 @@ app.kubernetes.io/name: {{ .Release.Name }} app.kubernetes.io/namespace: {{ include "app.namespace" . }} {{- end -}} -{{- define "app.nameToIdentifier" -}} -{{- trimAll "-" ( regexReplaceAll "[^a-zA-Z0-9-]" (lower .) "" ) | trunc 20 }} -{{- end -}} - {{- define "app.envSpringProfiles" }} {{- if (.Values.env).overrideSpringProfiles -}} {{ printf "%s" (.Values.env).overrideSpringProfiles }} @@ -65,3 +61,19 @@ app.kubernetes.io/namespace: {{ include "app.namespace" . }} {{- define "app.ssoServerUrl" -}} {{- required "sso.serverUrl muss angegeben sein" (.Values.sso).serverUrl -}} {{- end -}} + +{{- define "app.nameToIdentifier" -}} +{{- regexReplaceAll "[^a-zA-Z0-9]" (lower .) "" }} +{{- end -}} + +{{- define "app.generateKeycloakUserRessourceName" -}} +{{- printf "%s-keycloak-user" (include "app.nameToIdentifier" .) -}} +{{- end -}} + +{{- define "app.generateKeycloakUserSecretName" -}} +{{- printf "%s-credentials" (include "app.nameToIdentifier" .) -}} +{{- end -}} + +{{- define "app.serviceAccountName" -}} +{{ printf "%s" ( (.Values.serviceAccount).name | default "administration-service-account" ) }} +{{- end -}} diff --git a/src/main/helm/templates/api_password_secret.yaml b/src/main/helm/templates/api_password_secret.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b0e61458e8140f199cdf636c76343842c2b8913f --- /dev/null +++ b/src/main/helm/templates/api_password_secret.yaml @@ -0,0 +1,11 @@ +{{- if not (.Values.sso).api_user -}} +apiVersion: v1 +kind: Secret +metadata: + name: administration-api-password + labels: + {{- include "app.defaultLabels" . | indent 4 }} +type: Opaque +stringData: + password: {{ required "ozgcloud.keycloak.api.password must be set" .Values.ozgcloud.keycloak.api.password | quote }} +{{- end -}} \ No newline at end of file diff --git a/src/main/helm/templates/deployment.yaml b/src/main/helm/templates/deployment.yaml index 7d1d4762d2b94225e5bc46503dce5dd6c85449bd..7962926bad3084bd5537279b6c4fdc166e046db6 100644 --- a/src/main/helm/templates/deployment.yaml +++ b/src/main/helm/templates/deployment.yaml @@ -77,7 +77,33 @@ spec: - name: spring_data_mongodb_database value: {{ .Values.database.databaseName | default "administration-database" }} {{- end }} - + {{- if not (.Values.sso).api_user }} # used by dataport + - name: ozgcloud_keycloak_api_password + valueFrom: + secretKeyRef: + name: administration-api-password + key: password + optional: false + - name: ozgcloud_keycloak_api_user + value: {{ .Values.ozgcloud.keycloak.api.user }} + {{- else }} + - name: ozgcloud_keycloak_api_password + valueFrom: + secretKeyRef: + name: {{ include "app.generateKeycloakUserSecretName" .Values.sso.api_user.name }} + key: password + optional: false + - name: ozgcloud_keycloak_api_user + valueFrom: + secretKeyRef: + name: {{ include "app.generateKeycloakUserSecretName" .Values.sso.api_user.name }} + key: name + optional: false + {{- end }} + {{- if (((.Values.ozgcloud).sync).organisationseinheiten).cron }} + - name: ozgcloud_administration_sync_organisationseinheiten_cron + value: {{ .Values.ozgcloud.sync.organisationseinheiten.cron | quote }} + {{- end }} envFrom: {{- if (.Values.database).useExternal }} - secretRef: diff --git a/src/main/helm/templates/keycloak_user_crd.yaml b/src/main/helm/templates/keycloak_user_crd.yaml index 0144559de10b2edb52816d3a1807415cf6a257c9..3ef79ad12ac25be335589329c3a9ce76a7331871 100644 --- a/src/main/helm/templates/keycloak_user_crd.yaml +++ b/src/main/helm/templates/keycloak_user_crd.yaml @@ -1,10 +1,10 @@ {{- if not (.Values.sso).disableOzgOperator -}} -{{ range $user := concat ((.Values.sso).api_users | default list) ((.Values.sso).keycloak_users | default list) }} +{{ range $user := concat (empty (.Values.sso).api_user | ternary (list) (list .Values.sso.api_user)) ((.Values.sso).keycloak_users | default list) }} --- apiVersion: operator.ozgcloud.de/v1 kind: OzgCloudKeycloakUser metadata: - name: {{ include "app.nameToIdentifier" $user.name }}-keycloak-user + name: {{ include "app.generateKeycloakUserRessourceName" $user.name }} namespace: {{ include "app.namespace" $ }} spec: keep_after_delete: {{ $.Values.sso.keep_after_delete | default false }} diff --git a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role.yaml b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role.yaml index 9d8961c699d854c92b3290e4a5cc002cdfa87c34..ffbc9e30e8395421b3cbea343d9ee8bfde24ade1 100644 --- a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role.yaml +++ b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role.yaml @@ -1,5 +1,5 @@ {{- if not (.Values.sso).disableOzgOperator }} -{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_users) }} +{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_user) }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: @@ -9,9 +9,9 @@ rules: - apiGroups: - "*" resourceNames: - {{ range $user := concat (.Values.sso.keycloak_users | default list) (.Values.sso.api_users | default list) }} - - {{ include "app.nameToIdentifier" $user.name }}-credentials - {{ end }} + {{- range $user := concat (.Values.sso.keycloak_users | default list) (empty (.Values.sso).api_user | ternary (list) (list .Values.sso.api_user)) }} + - {{ include "app.generateKeycloakUserSecretName" $user.name }} + {{- end }} resources: - secrets verbs: diff --git a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role_binding.yaml b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role_binding.yaml index 1250afd39cd7e2308aca88ce232d3955344dc578..79c3de8162bcf5072b0eb35e038da438b3d66e15 100644 --- a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role_binding.yaml +++ b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_read_role_binding.yaml @@ -1,5 +1,5 @@ {{- if not (.Values.sso).disableOzgOperator }} -{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_users) }} +{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_user) }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: diff --git a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role.yaml b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role.yaml index 0072fe3e27c98741f13c1b12e45d09eed8a46416..25e03aeee9f8e21fe3958c6484e0a9f6883b704c 100644 --- a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role.yaml +++ b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role.yaml @@ -1,5 +1,5 @@ {{- if not (.Values.sso).disableOzgOperator }} -{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_users) }} +{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_user) }} apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: diff --git a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role_binding.yaml b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role_binding.yaml index ef069bf242a03deeeeece74f2fbcdb4fc3c33cfe..7baf4fc0113678eca8a12c2e7ab1a58944d707ca 100644 --- a/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role_binding.yaml +++ b/src/main/helm/templates/ozgcloud_keycloak_operator_secrets_write_role_binding.yaml @@ -1,5 +1,5 @@ {{- if not (.Values.sso).disableOzgOperator }} -{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_users) }} +{{- if or ((.Values.sso).keycloak_users) ((.Values.sso).api_user) }} apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: diff --git a/src/main/helm/templates/service_account.yaml b/src/main/helm/templates/service_account.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3bac8e223d1fd108b386d1f06ed4e9fb2284a67c --- /dev/null +++ b/src/main/helm/templates/service_account.yaml @@ -0,0 +1,31 @@ +# +# Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den +# Ministerpräsidenten des Landes Schleswig-Holstein +# Staatskanzlei +# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung +# +# Lizenziert unter der EUPL, Version 1.2 oder - sobald +# diese von der Europäischen Kommission genehmigt wurden - +# Folgeversionen der EUPL ("Lizenz"); +# Sie dürfen dieses Werk ausschließlich gemäß +# dieser Lizenz nutzen. +# Eine Kopie der Lizenz finden Sie hier: +# +# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 +# +# Sofern nicht durch anwendbare Rechtsvorschriften +# gefordert oder in schriftlicher Form vereinbart, wird +# die unter der Lizenz verbreitete Software "so wie sie +# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - +# ausdrücklich oder stillschweigend - verbreitet. +# Die sprachspezifischen Genehmigungen und Beschränkungen +# unter der Lizenz sind dem Lizenztext zu entnehmen. +# + +{{- if (.Values.serviceAccount).create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "app.serviceAccountName" . }} + namespace: {{ include "app.namespace" . }} +{{- end }} \ No newline at end of file diff --git a/src/main/helm/values.yaml b/src/main/helm/values.yaml index d7a06c379a6fdd1f106d35ecdd3f0d81c00376ae..65c0a551f00d862d2432233e89c37a7051d3ee1d 100644 --- a/src/main/helm/values.yaml +++ b/src/main/helm/values.yaml @@ -29,6 +29,9 @@ ozgcloud: bezeichner: helm adminDomainSuffix: admin environment: dev + keycloak: + api: + user: administrationApiUser image: repo: docker.ozg-sh.de @@ -39,4 +42,4 @@ database: databaseName: "administration-database" secretName: "ozg-mongodb-admin-administration-user" tls: - secretName: "ozg-mongodb-tls-cert" \ No newline at end of file + secretName: "ozg-mongodb-tls-cert" diff --git a/src/main/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurer.java b/src/main/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurer.java new file mode 100644 index 0000000000000000000000000000000000000000..97507d2e32b0f4c835e2d408c94e24967f0c0569 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurer.java @@ -0,0 +1,16 @@ +package de.ozgcloud.admin; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; +import org.springframework.hateoas.server.core.DefaultLinkRelationProvider; +import org.springframework.web.servlet.config.annotation.CorsRegistry; + +@Configuration +public class AdministrationRepositoryRestConfigurer implements RepositoryRestConfigurer { + + @Override + public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { + config.setLinkRelationProvider(new DefaultLinkRelationProvider()); + } +} diff --git a/src/main/java/de/ozgcloud/admin/GrpcConfiguration.java b/src/main/java/de/ozgcloud/admin/GrpcConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..81537a802b20132a52f2ee820948b91a91e4dbb7 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/GrpcConfiguration.java @@ -0,0 +1,22 @@ +package de.ozgcloud.admin; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * A workaround for @GrpcClient to work i.e. inject dependency until full Spring 3 support is available. + * https://github.com/yidongnan/grpc-spring-boot-starter/pull/775 + */ +@Configuration +@ImportAutoConfiguration({ + net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration.class, + net.devh.boot.grpc.client.autoconfigure.GrpcClientMetricAutoConfiguration.class, + net.devh.boot.grpc.client.autoconfigure.GrpcClientHealthAutoConfiguration.class, + net.devh.boot.grpc.client.autoconfigure.GrpcClientSecurityAutoConfiguration.class, + net.devh.boot.grpc.client.autoconfigure.GrpcClientTraceAutoConfiguration.class, + net.devh.boot.grpc.client.autoconfigure.GrpcDiscoveryClientAutoConfiguration.class, + net.devh.boot.grpc.common.autoconfigure.GrpcCommonCodecAutoConfiguration.class, + net.devh.boot.grpc.common.autoconfigure.GrpcCommonTraceAutoConfiguration.class +}) +public class GrpcConfiguration { +} diff --git a/src/main/java/de/ozgcloud/admin/SchedulingConfiguration.java b/src/main/java/de/ozgcloud/admin/SchedulingConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..012b8680f0fe768a7def4303e525d33b308c07f9 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/SchedulingConfiguration.java @@ -0,0 +1,27 @@ +package de.ozgcloud.admin; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +import com.mongodb.client.MongoClient; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.mongo.MongoLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; + +@Configuration +@EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "PT23H") +public class SchedulingConfiguration { + + @Value("${spring.data.mongodb.database}") + private String database; + + @Bean + LockProvider lockProvider(MongoClient mongoClient) { + return new MongoLockProvider(mongoClient.getDatabase(database)); + } + +} diff --git a/src/main/java/de/ozgcloud/admin/common/AbstractLinkedResourceDeserializer.java b/src/main/java/de/ozgcloud/admin/common/AbstractLinkedResourceDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..30747bdc41a6e09231c4c50e6a2429329b33ecb0 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/AbstractLinkedResourceDeserializer.java @@ -0,0 +1,112 @@ +package de.ozgcloud.admin.common; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import de.ozgcloud.common.datatype.StringBasedValue; +import lombok.Getter; + +abstract class AbstractLinkedResourceDeserializer extends StdDeserializer<Object> implements ContextualDeserializer { + + private static final long serialVersionUID = 1L; + + @Getter + private BeanProperty beanProperty; + + @Getter + private final JavaType targetType; + + protected AbstractLinkedResourceDeserializer() { + super(Object.class); + targetType = null; + } + + protected AbstractLinkedResourceDeserializer(BeanProperty beanProperty) { + super(Object.class); + this.beanProperty = beanProperty; + this.targetType = beanProperty.getType(); + } + + @Override + public Object deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { + if (jsonParser.isExpectedStartArrayToken()) { + Collection<Object> idList = targetType.getRawClass().isAssignableFrom(Set.class) ? new HashSet<>() : new ArrayList<>(); + + while (!jsonParser.nextToken().isStructEnd()) { + idList.add(extractId(jsonParser.getText())); + } + return idList; + } else { + return extractId(jsonParser.getText()); + } + } + + Object extractId(String url) { + Class<?> type; + if (targetType.isCollectionLikeType()) { + type = targetType.getContentType().getRawClass(); + } else { + type = targetType.getRawClass(); + } + + if (String.class.isAssignableFrom(type)) { + return extractStringId(url); + } + if (Long.class.isAssignableFrom(type) || Long.TYPE.isAssignableFrom(type)) { + return extractLongId(url); + } + if (StringBasedValue.class.isAssignableFrom(type)) { + return extractStringBasedValue(type, url); + } + return buildByBuilder(url); + } + + abstract Object buildByBuilder(String url); + + public static Long extractLongId(String uri) { + var trimedUri = StringUtils.trimToNull(uri); + if (Objects.isNull(trimedUri)) { + return null; + } + return Long.parseLong(URLDecoder.decode(trimedUri.substring(trimedUri.lastIndexOf('/') + 1), StandardCharsets.UTF_8)); + } + + private StringBasedValue extractStringBasedValue(Class<?> type, String url) { + String value = extractStringId(url); + Method fromMethod; + try { + fromMethod = type.getMethod("from", String.class); + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + String.format("Cannot generate Id from type '%s'. Missing 'from' Method.", targetType.getRawClass().getSimpleName())); + } + try { + return (StringBasedValue) fromMethod.invoke(null, value); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalStateException( + String.format("Cannot generate Id from type '%s'. Error calling 'from' Method.", targetType.getRawClass().getSimpleName()), + e); + } + } + + public static String extractStringId(String url) { + return URLDecoder.decode(url.substring(url.lastIndexOf('/') + 1), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/de/ozgcloud/admin/common/AbstractLinkedResourceSerializer.java b/src/main/java/de/ozgcloud/admin/common/AbstractLinkedResourceSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..c7938c6cf288761799bcb074e410ab8d81e0255c --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/AbstractLinkedResourceSerializer.java @@ -0,0 +1,37 @@ +package de.ozgcloud.admin.common; + +import java.io.IOException; +import java.util.Collection; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; + +import de.ozgcloud.common.errorhandling.TechnicalException; + +abstract class AbstractLinkedResourceSerializer extends JsonSerializer<Object> implements ContextualSerializer { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + + if (value instanceof Collection) { + gen.writeStartArray(); + ((Collection<?>) value).forEach(val -> writeObject(gen, buildLink(val))); + gen.writeEndArray(); + } else { + writeObject(gen, buildLink(value)); + } + } + + abstract String buildLink(Object id); + + abstract IdExtractor<Object> getExtractor(); + + void writeObject(JsonGenerator gen, Object value) { + try { + gen.writeObject(value); + } catch (IOException e) { + throw new TechnicalException("Error writing String to json", e); + } + } +} diff --git a/src/main/java/de/ozgcloud/admin/common/CollectionModelBuilder.java b/src/main/java/de/ozgcloud/admin/common/CollectionModelBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..8773c4fa77e2c10e5b7cfb3ae6bd3ca3bb539dd3 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/CollectionModelBuilder.java @@ -0,0 +1,63 @@ +package de.ozgcloud.admin.common; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.Link; + +import lombok.RequiredArgsConstructor; + +public class CollectionModelBuilder<T> { + + private final Iterable<T> entities; + + private final List<Link> links = new LinkedList<>(); + + private CollectionModelBuilder(Iterable<T> entities) { + this.entities = entities; + } + + public static <T> CollectionModelBuilder<T> fromEntities(Iterable<T> entities) { + return new CollectionModelBuilder<>(entities); + } + + public static <T> CollectionModelBuilder<T> fromEntities(Stream<T> entities) { + return new CollectionModelBuilder<>(entities.toList()); + } + + public CollectionModelBuilder<T> addLink(Link link) { + links.add(link); + return this; + } + + public ConditionalLinkAdder ifMatch(BooleanSupplier guard) { + return new ConditionalLinkAdder(guard.getAsBoolean()); + } + + public ConditionalLinkAdder ifMatch(Predicate<? super Iterable<T>> predicate) { + return new ConditionalLinkAdder(predicate.test(entities)); + } + + public CollectionModel<T> buildModel() { + var builtModel = CollectionModel.of(entities); + builtModel.add(links); + return builtModel; + } + + @RequiredArgsConstructor + public class ConditionalLinkAdder { + + public final boolean conditionFulfilled; + + public CollectionModelBuilder<T> addLink(Link link) { + if (conditionFulfilled) { + links.add(link); + } + return CollectionModelBuilder.this; + } + } +} \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/common/GrpcUtil.java b/src/main/java/de/ozgcloud/admin/common/GrpcUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..d78580fafc95ad3f547c8059cf8fb503cd6e2ce4 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/GrpcUtil.java @@ -0,0 +1,43 @@ +package de.ozgcloud.admin.common; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Optional; +import java.util.stream.StreamSupport; + +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GrpcUtil { + + public static final String ZUFI_MANAGER_GRPC_CLIENT = "zufi-manager"; + + public static final String SERVICE_KEY = "GRPC_SERVICE"; + + public static Key<String> keyOfString(String key) { + return Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + } + + public static Key<byte[]> createKeyOf(String key) { + return Key.of(key, Metadata.BINARY_BYTE_MARSHALLER); + } + + public static String getFromHeaders(String key, Metadata headers) { + return Optional.ofNullable(headers.get(createKeyOf(key))) + .map(GrpcUtil::byteToString) + .orElse(null); + } + + public static Collection<String> getCollection(String key, Metadata headers) { + return StreamSupport.stream(headers.getAll(createKeyOf(key)).spliterator(), false) + .map(GrpcUtil::byteToString) + .toList(); + } + + private static String byteToString(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/common/IdBuilder.java b/src/main/java/de/ozgcloud/admin/common/IdBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..d45ff3f1ff5fcaceb9e8aaed88fc24765c642055 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/IdBuilder.java @@ -0,0 +1,19 @@ +package de.ozgcloud.admin.common; + +import com.fasterxml.jackson.databind.BeanProperty; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +class IdBuilder implements ObjectBuilder<Object> { + + @Override + public Object build(Object id) { + return id; + } + + @Override + public ObjectBuilder<Object> constructContextAware(BeanProperty property) { + return new IdBuilder(); + } +} diff --git a/src/main/java/de/ozgcloud/admin/common/IdExtractor.java b/src/main/java/de/ozgcloud/admin/common/IdExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..7ecd4667434c5a7fca90c3c0cba62b825c85312f --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/IdExtractor.java @@ -0,0 +1,6 @@ +package de.ozgcloud.admin.common; + +@FunctionalInterface +public interface IdExtractor<T> { + String extractId(T object); +} \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/common/LinkedResource.java b/src/main/java/de/ozgcloud/admin/common/LinkedResource.java new file mode 100644 index 0000000000000000000000000000000000000000..789650d3e935c752cb34dbd1b531954ff3703c12 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/LinkedResource.java @@ -0,0 +1,30 @@ +package de.ozgcloud.admin.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Inherited + +@JacksonAnnotationsInside +@JsonSerialize(using = LinkedResourceSerializer.class) +@JsonDeserialize(using = LinkedResourceDeserializer.class) +public @interface LinkedResource { + + Class<?> controllerClass(); + + Class<? extends IdExtractor<Object>> extractor() default ToStringExtractor.class; + + @AliasFor(annotation = JsonDeserialize.class, attribute = "builder") + Class<? extends ObjectBuilder<Object>> builder() default IdBuilder.class; +} \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/common/LinkedResourceDeserializer.java b/src/main/java/de/ozgcloud/admin/common/LinkedResourceDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..226f2798ac2d8892f395808f29503751b0bfc5f6 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/LinkedResourceDeserializer.java @@ -0,0 +1,44 @@ +package de.ozgcloud.admin.common; + +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.lang3.reflect.ConstructorUtils; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import de.ozgcloud.common.errorhandling.TechnicalException; + +public class LinkedResourceDeserializer extends AbstractLinkedResourceDeserializer { + + private static final long serialVersionUID = 1L; + + private LinkedResource annotation; + + protected LinkedResourceDeserializer() { + super(); + } + + protected LinkedResourceDeserializer(BeanProperty beanProperty) { + super(beanProperty); + this.annotation = beanProperty.getAnnotation(LinkedResource.class); + } + + @Override + Object buildByBuilder(String url) { + ObjectBuilder<?> builder; + try { + builder = ConstructorUtils.invokeConstructor(annotation.builder()).constructContextAware(getBeanProperty()); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + throw new TechnicalException("Error instanciating Builder.", e); + } + return builder.build(extractStringId(url)); + } + + @Override + public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) { + return new LinkedResourceDeserializer(property); + } + +} diff --git a/src/main/java/de/ozgcloud/admin/common/LinkedResourceSerializer.java b/src/main/java/de/ozgcloud/admin/common/LinkedResourceSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..706dbabe599e5cdf3dc53a934863a4c3218e0c88 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/LinkedResourceSerializer.java @@ -0,0 +1,43 @@ +package de.ozgcloud.admin.common; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.lang3.reflect.ConstructorUtils; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class LinkedResourceSerializer extends AbstractLinkedResourceSerializer { + + private LinkedResource annotation; + + private LinkedResourceSerializer(LinkedResource annotation) { + this.annotation = annotation; + } + + @Override + public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) { + return new LinkedResourceSerializer(property.getAnnotation(LinkedResource.class)); + } + + @Override + String buildLink(Object id) { + return linkTo(annotation.controllerClass()).slash(getExtractor().extractId(id)).toString(); + } + + @Override + IdExtractor<Object> getExtractor() { + try { + return ConstructorUtils.invokeConstructor(annotation.extractor()); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + throw new TechnicalException("Error instanciating IdExtractor", e); + } + } +} diff --git a/src/main/java/de/ozgcloud/admin/common/ModelBuilder.java b/src/main/java/de/ozgcloud/admin/common/ModelBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..bca40c5e31a53d7f1de560472067ab33b1f4538f --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/ModelBuilder.java @@ -0,0 +1,241 @@ +package de.ozgcloud.admin.common; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class ModelBuilder<T> { + + private static final Map<Class<?>, Map<Object, List<Field>>> ANNOTATED_FIELDS_BY_ANNOTATION = new ConcurrentHashMap<>(); + + private final T entity; + private final EntityModel<T> model; + + private final List<Link> links = new LinkedList<>(); + private final List<Function<EntityModel<T>, EntityModel<T>>> mapper = new LinkedList<>(); + + private ModelBuilder(T entity) { + this.entity = entity; + this.model = null; + } + + private ModelBuilder(EntityModel<T> model) { + this.entity = null; + this.model = model; + } + + public static <T> ModelBuilder<T> fromEntity(T entity) { + return new ModelBuilder<>(entity); + } + + public static <T> ModelBuilder<T> fromModel(EntityModel<T> model) { + return new ModelBuilder<>(model); + } + + public ModelBuilder<T> addLink(Link link) { + links.add(link); + return this; + } + + public ModelBuilder<T> addLink(Optional<Link> link) { + link.ifPresent(links::add); + return this; + } + + public ModelBuilder<T> addLinks(Link... links) { + this.links.addAll(Arrays.asList(links)); + return this; + } + + public ModelBuilder<T> addLinks(Collection<Link> links) { + this.links.addAll(links); + return this; + } + + public ConditionalLinkAdder ifMatch(Predicate<T> predicate) { + return new ConditionalLinkAdder(predicate.test(Objects.isNull(entity) ? model.getContent() : entity)); + } + + public ConditionalLinkAdder ifMatch(BooleanSupplier guard) { + return new ConditionalLinkAdder(guard.getAsBoolean()); + } + + public ModelBuilder<T> map(UnaryOperator<EntityModel<T>> mapper) { + this.mapper.add(mapper); + return this; + } + + public EntityModel<T> buildModel() { + var filteredLinks = links.stream().filter(Objects::nonNull).collect(Collectors.toSet()); + + EntityModel<T> buildedModel = Objects.isNull(model) ? EntityModel.of(entity) : model; + buildedModel = buildedModel.add(filteredLinks); + + addLinkByLinkedResourceAnnotationIfMissing(buildedModel); + + return applyMapper(buildedModel); + } + + private EntityModel<T> applyMapper(EntityModel<T> resource) { + Iterator<Function<EntityModel<T>, EntityModel<T>>> i = mapper.iterator(); + EntityModel<T> result = resource; + while (i.hasNext()) { + result = i.next().apply(result); + } + return result; + } + + private void addLinkByLinkedResourceAnnotationIfMissing(EntityModel<T> resource) { + getFields(LinkedResource.class).stream() + .filter(field -> shouldAddLink(resource, field)) + .forEach(field -> handleLinkedResourceField(resource, field)); + } + + private void handleLinkedResourceField(EntityModel<T> resource, Field field) { + getEntityFieldValue(field).map(Object::toString).filter(StringUtils::isNotBlank).ifPresent(val -> resource + .add(WebMvcLinkBuilder.linkTo(field.getAnnotation(LinkedResource.class).controllerClass()).slash(val) + .withRel(sanitizeName(field.getName())))); + } + + private boolean shouldAddLink(EntityModel<T> resource, Field field) { + return !(field.getType().isArray() || Collection.class.isAssignableFrom(field.getType()) || resource.hasLink(sanitizeName(field.getName()))); + } + + private List<Field> getFields(Class<? extends Annotation> annotationClass) { + var fields = Optional.ofNullable(ANNOTATED_FIELDS_BY_ANNOTATION.get(getEntity().getClass())) + .map(fieldsByAnnotation -> fieldsByAnnotation.get(annotationClass)) + .orElseGet(Collections::emptyList); + + if (CollectionUtils.isEmpty(fields)) { + fields = FieldUtils.getFieldsListWithAnnotation(getEntity().getClass(), annotationClass); + + updateFields(annotationClass, fields); + } + return fields; + } + + private void updateFields(Class<? extends Annotation> annotationClass, List<Field> fields) { + var annotationMap = Optional.ofNullable(ANNOTATED_FIELDS_BY_ANNOTATION.get(getEntity().getClass())).orElseGet(HashMap::new); + annotationMap.put(annotationClass, fields); + + ANNOTATED_FIELDS_BY_ANNOTATION.put(annotationClass, annotationMap); + } + + private String sanitizeName(String fieldName) { + if (fieldName.endsWith("Id")) { + return fieldName.substring(0, fieldName.indexOf("Id")); + } + return fieldName; + } + + private Optional<Object> getEntityFieldValue(Field field) { + try { + field.setAccessible(true); + Optional<Object> value = Optional.ofNullable(field.get(getEntity())); + field.setAccessible(false); + return value; + } catch (IllegalArgumentException | IllegalAccessException e) { + LOG.warn("Cannot access field value of LinkedResource field.", e); + } + return Optional.empty(); + } + + private T getEntity() { + return Optional.ofNullable(entity == null ? model.getContent() : entity) + .orElseThrow(() -> new IllegalStateException("Entity must not null for ModelBuilding")); + } + + @RequiredArgsConstructor + public class ConditionalLinkAdder { + + public final boolean conditionFulfilled; + + public ModelBuilder<T> addLink(Supplier<Link> linkSupplier) { + if (conditionFulfilled) { + addLink(linkSupplier.get()); + } + return ModelBuilder.this; + } + + public ModelBuilder<T> addLinkIfPresent(Supplier<Optional<Link>> linkSupplier) { + if (conditionFulfilled) { + addLink(linkSupplier.get()); + } + return ModelBuilder.this; + } + + public ModelBuilder<T> addLink(Function<T, Link> linkBuilder) { + if (conditionFulfilled) { + addLink(linkBuilder.apply(getEntity())); + } + return ModelBuilder.this; + } + + public ModelBuilder<T> addLink(Link link) { + if (conditionFulfilled) { + links.add(link); + } + return ModelBuilder.this; + } + + public ModelBuilder<T> addLink(Optional<Link> link) { + if (conditionFulfilled) { + link.ifPresent(links::add); + } + return ModelBuilder.this; + } + + public ModelBuilder<T> addLinks(Link... links) { + if (conditionFulfilled) { + ModelBuilder.this.links.addAll(Arrays.asList(links)); + } + return ModelBuilder.this; + } + + @SafeVarargs + public final ModelBuilder<T> addLinks(Supplier<Link>... linkSuppliers) { + if (conditionFulfilled) { + for (int i = 0; i < linkSuppliers.length; i++) { + ModelBuilder.this.links.add(linkSuppliers[i].get()); + } + } + return ModelBuilder.this; + } + + public final ModelBuilder<T> addLinks(Supplier<Collection<Link>> linksSupplier) { + if (conditionFulfilled) { + linksSupplier.get().forEach(ModelBuilder.this.links::add); + } + + return ModelBuilder.this; + } + + } +} \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/common/ObjectBuilder.java b/src/main/java/de/ozgcloud/admin/common/ObjectBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..649aa24e1ef13dd8c50104c6cf39b0fa8386e813 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/ObjectBuilder.java @@ -0,0 +1,10 @@ +package de.ozgcloud.admin.common; + +import com.fasterxml.jackson.databind.BeanProperty; + +public interface ObjectBuilder<T> { + + T build(Object id); + + ObjectBuilder<T> constructContextAware(BeanProperty property); +} diff --git a/src/main/java/de/ozgcloud/admin/common/ToStringExtractor.java b/src/main/java/de/ozgcloud/admin/common/ToStringExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..3d474a013f6c28bad510fd9059c76dee7c842e75 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/ToStringExtractor.java @@ -0,0 +1,12 @@ +package de.ozgcloud.admin.common; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor +class ToStringExtractor implements IdExtractor<Object> { + + @Override + public String extractId(Object object) { + return object.toString(); + } +} diff --git a/src/main/java/de/ozgcloud/admin/keycloak/AddGroupData.java b/src/main/java/de/ozgcloud/admin/keycloak/AddGroupData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfef1afa5a70b06618372fcb6e7c1eb035f3e872 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/AddGroupData.java @@ -0,0 +1,14 @@ +package de.ozgcloud.admin.keycloak; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@Builder +@ToString +public class AddGroupData { + + private String name; + private String organisationsEinheitId; +} diff --git a/src/main/java/de/ozgcloud/admin/keycloak/Group.java b/src/main/java/de/ozgcloud/admin/keycloak/Group.java new file mode 100644 index 0000000000000000000000000000000000000000..005a7ce23d0709ade874e9fad35e7be17906ceee --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/Group.java @@ -0,0 +1,18 @@ +package de.ozgcloud.admin.keycloak; + +import java.util.List; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +@Getter +@Builder(toBuilder = true) +public class Group { + + private String id; + 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 new file mode 100644 index 0000000000000000000000000000000000000000..a18a572526e3e4da1ca1bbc66433fe17266acedc --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/GroupMapper.java @@ -0,0 +1,61 @@ +package de.ozgcloud.admin.keycloak; + +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.keycloak.representations.idm.GroupRepresentation; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValueCheckStrategy; +import org.springframework.beans.factory.annotation.Autowired; + +import lombok.extern.log4j.Log4j2; + +@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS) +@Log4j2 +abstract class GroupMapper { + + @Autowired + private KeycloakApiProperties keycloakApiProperties; + + public abstract List<Group> fromGroupRepresentations(List<GroupRepresentation> groupRepresentations); + + @Mapping(target = "organisationsEinheitId", source = "attributes") + public abstract Group fromGroupRepresentation(GroupRepresentation groupRepresentation); + + String getOrganisationsEinheitId(Map<String, List<String>> attributes) { + if (attributes == null) { + return null; + } + var values = attributes.get(keycloakApiProperties.getOrganisationsEinheitIdKey()); + if (values == null) { + return null; + } + if (values.size() > 1 && values.stream().distinct().count() > 1) { + throw new GroupRepresentationMappingException("Group contains multiple values for organisationsEinheitId: %s".formatted(values)); + } + return values.getFirst(); + } + + @AfterMapping + protected void deleteGroupsWithoutOrganisationsEinheitId(@MappingTarget List<Group> groups) { + groups.removeIf(this::isMissingOrganisationsEinheitId); + } + + protected boolean isMissingOrganisationsEinheitId(Group group) { + return StringUtils.isBlank(group.getOrganisationsEinheitId()); + } + + @Mapping(target = "attributes", expression = "java(buildGroupAttributes(groupToAdd))") + public abstract GroupRepresentation toGroupRepresentation(AddGroupData groupToAdd); + + Map<String, List<String>> buildGroupAttributes(AddGroupData groupToAdd) { + var organisationsEinheitId = groupToAdd.getOrganisationsEinheitId(); + return StringUtils.isNotBlank(organisationsEinheitId) ? + Map.of(keycloakApiProperties.getOrganisationsEinheitIdKey(), List.of(organisationsEinheitId)) : + Map.of(); + } +} diff --git a/src/main/java/de/ozgcloud/admin/keycloak/GroupRepresentationMappingException.java b/src/main/java/de/ozgcloud/admin/keycloak/GroupRepresentationMappingException.java new file mode 100644 index 0000000000000000000000000000000000000000..c07743ddf481a64e3d296309fbaa556fbab31f14 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/GroupRepresentationMappingException.java @@ -0,0 +1,8 @@ +package de.ozgcloud.admin.keycloak; + +public class GroupRepresentationMappingException extends RuntimeException { + + public GroupRepresentationMappingException(String message) { + super(message); + } +} diff --git a/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiProperties.java b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..57d23fe52be991d79a41c2ea50affdeb6825283d --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiProperties.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.keycloak; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Configuration +@ConfigurationProperties("ozgcloud.keycloak.api") +public class KeycloakApiProperties { + + @NotBlank + private String url; + @NotBlank + private String user; + @NotBlank + private String password; + @NotBlank + private String realm; + @NotBlank + private String client; + @NotBlank + private String organisationsEinheitIdKey; +} \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiService.java b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiService.java new file mode 100644 index 0000000000000000000000000000000000000000..888f98c8b9acbecc3eaecf16cfbacd9dca176587 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakApiService.java @@ -0,0 +1,40 @@ +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.Service; + +import jakarta.ws.rs.core.Response; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +class KeycloakApiService { + + private final GroupsResource groupsResource; + + public List<GroupRepresentation> getAllGroups() { + return groupsResource.groups("", 0, Integer.MAX_VALUE, false); + } + + public String addGroup(GroupRepresentation groupRepresentation) { + try (var response = groupsResource.add(groupRepresentation)) { + return getCreatedResourceIdFromResponse(response); + } + } + + String getCreatedResourceIdFromResponse(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); + } +} diff --git a/src/main/java/de/ozgcloud/admin/keycloak/KeycloakConfiguration.java b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..b72c4c2037cab6bcba65e8308b9e7ef8141aa83d --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.keycloak; + +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.admin.client.resource.GroupsResource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class KeycloakConfiguration { + + private final KeycloakApiProperties keycloakApiProperties; + + @Bean + Keycloak keycloak() { + return buildKeycloakInstance(); + } + + @Bean + GroupsResource groupsResource(Keycloak keycloak) { + return keycloak.realm(keycloakApiProperties.getRealm()).groups(); + } + + private Keycloak buildKeycloakInstance() { + return KeycloakBuilder.builder() + .serverUrl(keycloakApiProperties.getUrl()) + .realm(keycloakApiProperties.getRealm()) + .clientId(keycloakApiProperties.getClient()) + .username(keycloakApiProperties.getUser()) + .password(keycloakApiProperties.getPassword()) + .build(); + } +} diff --git a/src/main/java/de/ozgcloud/admin/keycloak/KeycloakRemoteService.java b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakRemoteService.java new file mode 100644 index 0000000000000000000000000000000000000000..1d0a854481d0db242dd5c21583ad4d4616fa593b --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/KeycloakRemoteService.java @@ -0,0 +1,23 @@ +package de.ozgcloud.admin.keycloak; + +import java.util.stream.Stream; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class KeycloakRemoteService { + + private final KeycloakApiService apiService; + private final GroupMapper groupMapper; + + public Stream<Group> getGroupsWithOrganisationsEinheitId() { + return groupMapper.fromGroupRepresentations(apiService.getAllGroups()).stream(); + } + + public String addGroup(AddGroupData addGroupData) { + return apiService.addGroup(groupMapper.toGroupRepresentation(addGroupData)); + } +} diff --git a/src/main/java/de/ozgcloud/admin/keycloak/ResourceCreationException.java b/src/main/java/de/ozgcloud/admin/keycloak/ResourceCreationException.java new file mode 100644 index 0000000000000000000000000000000000000000..dc6f8ebfebac48b57c2fb8b60d8056f5d4e23a1d --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/keycloak/ResourceCreationException.java @@ -0,0 +1,8 @@ +package de.ozgcloud.admin.keycloak; + +public class ResourceCreationException extends RuntimeException { + + public ResourceCreationException(String message) { + super(message); + } +} diff --git a/src/main/java/de/ozgcloud/admin/migration/MongockFailedEventListener.java b/src/main/java/de/ozgcloud/admin/migration/MongockFailedEventListener.java index 84e860cb2cbdee0312df576f903dd285e89e1bd9..c46364338fef285376784272d2f2275743a92f79 100644 --- a/src/main/java/de/ozgcloud/admin/migration/MongockFailedEventListener.java +++ b/src/main/java/de/ozgcloud/admin/migration/MongockFailedEventListener.java @@ -34,6 +34,6 @@ public class MongockFailedEventListener implements ApplicationListener<SpringMig @Override public void onApplicationEvent(SpringMigrationFailureEvent event) { - log.error("Mongock migration failed", event.getMigrationResult()); + LOG.error("Mongock migration failed", event.getMigrationResult()); } } \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/migration/MongockStartEventListener.java b/src/main/java/de/ozgcloud/admin/migration/MongockStartEventListener.java index ad1558351ae353658dbe653bfe2630e627373489..e66855af555eb96ad054bf3ae9601d0c432a9fbb 100644 --- a/src/main/java/de/ozgcloud/admin/migration/MongockStartEventListener.java +++ b/src/main/java/de/ozgcloud/admin/migration/MongockStartEventListener.java @@ -34,6 +34,6 @@ public class MongockStartEventListener implements ApplicationListener<SpringMigr @Override public void onApplicationEvent(SpringMigrationStartedEvent event) { - log.info("Mongock start migration..."); + LOG.info("Mongock start migration..."); } } \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/migration/MongockSuccessEventListener.java b/src/main/java/de/ozgcloud/admin/migration/MongockSuccessEventListener.java index 95a7f76ee988dc8f1e72b8db6341da77514e5b54..176bc14fdfbbac69e00b16a2e649cf8b5db1050d 100644 --- a/src/main/java/de/ozgcloud/admin/migration/MongockSuccessEventListener.java +++ b/src/main/java/de/ozgcloud/admin/migration/MongockSuccessEventListener.java @@ -34,6 +34,6 @@ public class MongockSuccessEventListener implements ApplicationListener<SpringMi @Override public void onApplicationEvent(SpringMigrationSuccessEvent event) { - log.info("Mongock migration successfull", event.getMigrationResult()); + LOG.info("Mongock migration successfull", event.getMigrationResult()); } } \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheit.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheit.java new file mode 100644 index 0000000000000000000000000000000000000000..470a29e850ac9929ac8229c8211b036484f1c0f4 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheit.java @@ -0,0 +1,45 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.util.List; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.mongodb.core.mapping.Document; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Builder(toBuilder = true) +@Jacksonized +@Document(language = "german", collection = OrganisationsEinheit.COLLECTION_NAME) +@TypeAlias("OrganisationsEinheit") +public class OrganisationsEinheit { + + static final String COLLECTION_NAME = "organisationsEinheit"; + + @JsonIgnore + @Id + private String id; + @JsonIgnore + private String keycloakId; + @JsonIgnore + private String zufiId; + @JsonIgnore + private Long lastSyncTimestamp; + @JsonIgnore + private String parentId; + @JsonIgnore + @Singular + private List<OrganisationsEinheit> children; + + private String name; + private String organisationsEinheitId; + private SyncResult syncResult; + @Builder.Default + private OrganisationsEinheitSettings settings = new OrganisationsEinheitSettings(null); +} \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitController.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitController.java new file mode 100644 index 0000000000000000000000000000000000000000..3e40411d38eca6d07c75ad84db0c5164ba120d1a --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitController.java @@ -0,0 +1,39 @@ +package de.ozgcloud.admin.organisationseinheit; + +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping(OrganisationsEinheitController.PATH) +@RequiredArgsConstructor +public class OrganisationsEinheitController { + + static final String PATH = "/api/organisationseinheits"; // NOSONAR + + private final OrganisationsEinheitService organisationsEinheitService; + + private final OrganisationsEinheitModelAssembler assembler; + + @GetMapping + public CollectionModel<EntityModel<OrganisationsEinheit>> getAll() { + var organisationsEinheiten = organisationsEinheitService.getOrganisationsEinheiten(); + return assembler.toCollectionModel(organisationsEinheiten); + } + + @GetMapping("/{id}") + public EntityModel<OrganisationsEinheit> getById(@PathVariable String id) { + return assembler.toModel(organisationsEinheitService.getOrganisationsEinheitById(id)); + } + + @GetMapping("/{id}/children") + public CollectionModel<EntityModel<OrganisationsEinheit>> getChildren(@PathVariable String id) { + var children = organisationsEinheitService.getChildren(id); + return assembler.toChildrenCollectionModel(id, children); + } +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitMapper.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..da1634ec80bf9a5169022184c50bba014dd735a4 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitMapper.java @@ -0,0 +1,17 @@ +package de.ozgcloud.admin.organisationseinheit; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import de.ozgcloud.admin.keycloak.AddGroupData; +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcOrganisationsEinheit; + +@Mapper +interface OrganisationsEinheitMapper { + + @Mapping(target = "organisationsEinheitId", source = "xzufiId.id") + @Mapping(target = "zufiId", source = "id") + OrganisationsEinheit fromGrpc(GrpcOrganisationsEinheit grpcOrganisationsEinheit); + + AddGroupData toAddGroupData(OrganisationsEinheit organisationsEinheit); +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitModelAssembler.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitModelAssembler.java new file mode 100644 index 0000000000000000000000000000000000000000..6ac15d9d36936471e22321829f262a55f5ad36fd --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitModelAssembler.java @@ -0,0 +1,59 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import java.util.stream.StreamSupport; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.LinkRelation; +import org.springframework.hateoas.mediatype.hal.HalModelBuilder; +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.stereotype.Component; + +import de.ozgcloud.admin.common.CollectionModelBuilder; + +@Component +class OrganisationsEinheitModelAssembler + implements RepresentationModelAssembler<OrganisationsEinheit, EntityModel<OrganisationsEinheit>> { + + static final String REL_CHILD_ORGANISATIONS_EINHEITEN = "childList"; + + @Override + public EntityModel<OrganisationsEinheit> toModel(OrganisationsEinheit organisationsEinheit) { + var halModelBuilder = HalModelBuilder.halModelOf(organisationsEinheit); + + embedChildOrganisationsEinheiten(organisationsEinheit, halModelBuilder); + + return halModelBuilder + .<EntityModel<OrganisationsEinheit>>build() + .add(linkTo(methodOn(OrganisationsEinheitController.class).getById(organisationsEinheit.getId())).withSelfRel()); + } + + void embedChildOrganisationsEinheiten(OrganisationsEinheit organisationsEinheit, HalModelBuilder halModelBuilder) { + if (CollectionUtils.isNotEmpty(organisationsEinheit.getChildren())) { + var childrenModel = toChildrenCollectionModel(organisationsEinheit.getId(), organisationsEinheit.getChildren()).getContent(); + halModelBuilder.embed(childrenModel, LinkRelation.of(REL_CHILD_ORGANISATIONS_EINHEITEN)) + .link(linkTo(methodOn(OrganisationsEinheitController.class).getChildren(organisationsEinheit.getId())).withRel( + REL_CHILD_ORGANISATIONS_EINHEITEN)); + } + } + + @Override + public CollectionModel<EntityModel<OrganisationsEinheit>> toCollectionModel( + Iterable<? extends OrganisationsEinheit> organisationsEinheiten) { + var models = StreamSupport.stream(organisationsEinheiten.spliterator(), false).map(this::toModel); + return CollectionModelBuilder.fromEntities(models) + .addLink(linkTo(methodOn(OrganisationsEinheitController.class).getAll()).withSelfRel()) + .buildModel(); + } + + public CollectionModel<EntityModel<OrganisationsEinheit>> toChildrenCollectionModel(String parentId, + Iterable<? extends OrganisationsEinheit> organisationsEinheiten) { + var models = StreamSupport.stream(organisationsEinheiten.spliterator(), false).map(this::toModel); + return CollectionModelBuilder.fromEntities(models) + .addLink(linkTo(methodOn(OrganisationsEinheitController.class).getChildren(parentId)).withSelfRel()) + .buildModel(); + } +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRemoteService.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRemoteService.java new file mode 100644 index 0000000000000000000000000000000000000000..7847a92134a6a8bb82ec4c3f68ea33bc72201c2a --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRemoteService.java @@ -0,0 +1,32 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.ozgcloud.admin.common.GrpcUtil; +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcGetByOrganisationsEinheitIdRequest; +import de.ozgcloud.zufi.grpc.organisationseinheit.OrganisationsEinheitServiceGrpc.OrganisationsEinheitServiceBlockingStub; +import net.devh.boot.grpc.client.inject.GrpcClient; + +@Service +class OrganisationsEinheitRemoteService { + + @GrpcClient(GrpcUtil.ZUFI_MANAGER_GRPC_CLIENT) + private OrganisationsEinheitServiceBlockingStub serviceStub; + + @Autowired + private OrganisationsEinheitMapper organisationsEinheitMapper; + + public List<OrganisationsEinheit> getByOrganisationsEinheitId(String organisationsEinheitId) { + var request = buildGetByOrganisationsEinheitIdRequest(organisationsEinheitId); + var response = serviceStub.getByOrganisationsEinheitId(request); + return response.getOrganisationsEinheitenList().stream().map(organisationsEinheitMapper::fromGrpc).toList(); + } + + GrpcGetByOrganisationsEinheitIdRequest buildGetByOrganisationsEinheitIdRequest(String organisationsEinheitId) { + return GrpcGetByOrganisationsEinheitIdRequest.newBuilder().setOrganisationsEinheitId(organisationsEinheitId).build(); + } + +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRepository.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..f32cbadfe751ab671151de99142112f22ff02671 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRepository.java @@ -0,0 +1,30 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.Update; +import org.springframework.stereotype.Repository; + +@Repository +interface OrganisationsEinheitRepository extends MongoRepository<OrganisationsEinheit, String> { + + @Query("{'syncResult': {$ne: 'DELETED'}}") + List<OrganisationsEinheit> findAllNotDeleted(); + + @Query("{'syncResult': {$ne: 'DELETED'}, 'parentId': ?0}") + List<OrganisationsEinheit> findChildren(String parentId); + + @Query("{'syncResult': { $ne: null }, 'keycloakId': ?0}") + Optional<OrganisationsEinheit> findSyncedByKeycloakId(String keycloakId); + + @Query("{'syncResult': { $ne: null }, 'lastSyncTimestamp': { $lt: ?0 }}") + @Update("{'$set': {'syncResult': 'DELETED'}}") + void setUnsyncedAsDeleted(long lastSyncTimestamp); + + @Query("{'syncResult': null }") + Stream<OrganisationsEinheit> findAllWithoutSyncResult(); +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessor.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..4c4bb9d279afa74858b92d21f38f57819d558beb --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessor.java @@ -0,0 +1,20 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelProcessor; +import org.springframework.stereotype.Component; + +import de.ozgcloud.admin.Root; + +@Component +class OrganisationsEinheitRootProcessor implements RepresentationModelProcessor<EntityModel<Root>> { + + static final String REL_ORGANISATIONS_EINHEITEN = "organisationsEinheiten"; + + @Override + public EntityModel<Root> process(EntityModel<Root> model) { + return model.add(linkTo(methodOn(OrganisationsEinheitController.class).getAll()).withRel(REL_ORGANISATIONS_EINHEITEN)); + } +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitService.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitService.java new file mode 100644 index 0000000000000000000000000000000000000000..dc0a1873b90d57cbcbdeda7c67fd61fbb68abeca --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitService.java @@ -0,0 +1,43 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.util.List; +import java.util.Objects; + +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +class OrganisationsEinheitService { + + private final OrganisationsEinheitRepository repository; + + public OrganisationsEinheit saveOrganisationsEinheit(OrganisationsEinheit organisationsEinheit) { + return repository.save(organisationsEinheit); + } + + public List<OrganisationsEinheit> getOrganisationsEinheiten() { + var organisationsEinheiten = repository.findAllNotDeleted(); + return organisationsEinheiten.stream() + .filter(organisationsEinheit -> Objects.isNull(organisationsEinheit.getParentId())) + .map(rootOrganisationsEinheit -> addChildren(rootOrganisationsEinheit, organisationsEinheiten)) + .toList(); + } + + OrganisationsEinheit addChildren(OrganisationsEinheit rootOrganisationsEinheit, List<OrganisationsEinheit> allOrganisationsEinheiten) { + return rootOrganisationsEinheit.toBuilder() + .children(allOrganisationsEinheiten.stream() + .filter(organisationsEinheit -> rootOrganisationsEinheit.getId().equals(organisationsEinheit.getParentId())).toList()) + .build(); + } + + public OrganisationsEinheit getOrganisationsEinheitById(String id) { + return repository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Organisationseinheit with id " + id + " not found")); + } + + public List<OrganisationsEinheit> getChildren(String parentId) { + return repository.findChildren(parentId); + } +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSettings.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSettings.java new file mode 100644 index 0000000000000000000000000000000000000000..92e6cff4dc846668d7d546eeba4665e13f5bf6e3 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSettings.java @@ -0,0 +1,14 @@ +package de.ozgcloud.admin.organisationseinheit; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Builder(toBuilder = true) +@Jacksonized +public class OrganisationsEinheitSettings { + + private String signatur; + +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSynchronizationException.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSynchronizationException.java new file mode 100644 index 0000000000000000000000000000000000000000..42944f0a41b1d5eb7cd1a724ab8e1792bc7da7de --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSynchronizationException.java @@ -0,0 +1,12 @@ +package de.ozgcloud.admin.organisationseinheit; + +public class OrganisationsEinheitSynchronizationException extends RuntimeException { + + public OrganisationsEinheitSynchronizationException(String message) { + super(message); + } + + public OrganisationsEinheitSynchronizationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncResult.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncResult.java new file mode 100644 index 0000000000000000000000000000000000000000..8a1d06bcfc138a88ec49ba10c660b0e232535fe3 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncResult.java @@ -0,0 +1,5 @@ +package de.ozgcloud.admin.organisationseinheit; + +public enum SyncResult { + OK, NOT_FOUND_IN_PVOG, NAME_MISMATCH, ORGANISATIONSEINHEIT_ID_NOT_UNIQUE, DELETED +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncScheduler.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..116a7e0c61dd25b35b87dff650d47f5fc8566697 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncScheduler.java @@ -0,0 +1,24 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.time.Instant; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; + +@Component +@RequiredArgsConstructor +class SyncScheduler { + + private final SyncService syncService; + + @SchedulerLock(name = "SyncScheduler_syncOrganisationsEinheitenWithKeycloak", lockAtLeastFor = "PT5M", lockAtMostFor = "PT23H") + @Scheduled(cron = "${ozgcloud.administration.sync.organisationseinheiten.cron}") + public void syncOrganisationsEinheitenWithKeycloak() { + var syncTimestamp = Instant.now().toEpochMilli(); + syncService.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + syncService.syncAddedOrganisationsEinheiten(syncTimestamp); + } +} diff --git a/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncService.java b/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncService.java new file mode 100644 index 0000000000000000000000000000000000000000..a1c911a20d16d2cb4fdbbd707417180521c0bd9f --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/organisationseinheit/SyncService.java @@ -0,0 +1,126 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import de.ozgcloud.admin.keycloak.AddGroupData; +import de.ozgcloud.admin.keycloak.Group; +import de.ozgcloud.admin.keycloak.KeycloakRemoteService; +import de.ozgcloud.admin.keycloak.ResourceCreationException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Service +@RequiredArgsConstructor +@Log4j2 +class SyncService { + + private final OrganisationsEinheitRepository repository; + private final KeycloakRemoteService keycloakRemoteService; + private final OrganisationsEinheitRemoteService organisationsRemoteService; + private final OrganisationsEinheitMapper organisationsEinheitMapper; + + public void syncOrganisationsEinheitenFromKeycloak(long syncTimestamp) { + keycloakRemoteService.getGroupsWithOrganisationsEinheitId().forEach(group -> syncGroupsWithSubGroups(group, null, syncTimestamp)); + repository.setUnsyncedAsDeleted(syncTimestamp); + } + + void syncGroupsWithSubGroups(Group group, OrganisationsEinheit parent, long syncTimestamp) { + var synced = syncGroup(group, parent, syncTimestamp); + var saved = saveSyncedOrganisationsEinheit(synced); + group.getSubGroups().forEach(subGroup -> syncGroupsWithSubGroups(subGroup, saved, syncTimestamp)); + } + + OrganisationsEinheit syncGroup(Group group, OrganisationsEinheit parent, long syncTimestamp) { + var pvogOrganisationsEinheiten = organisationsRemoteService.getByOrganisationsEinheitId(group.getOrganisationsEinheitId()); + var syncedName = syncName(pvogOrganisationsEinheiten, group); + var syncResult = evaluateSyncResult(pvogOrganisationsEinheiten, group); + var zufiId = syncZufiId(pvogOrganisationsEinheiten); + var organisationsEinheitBuilder = OrganisationsEinheit.builder() + .keycloakId(group.getId()) + .name(syncedName) + .organisationsEinheitId(group.getOrganisationsEinheitId()) + .syncResult(syncResult) + .zufiId(zufiId) + .lastSyncTimestamp(syncTimestamp); + if (parent != null) { + organisationsEinheitBuilder.parentId(parent.getId()); + } + return organisationsEinheitBuilder.build(); + } + + String syncName(List<OrganisationsEinheit> pvogOrganisationsEinheiten, Group group) { + if (pvogOrganisationsEinheiten.size() != 1) { + return group.getName(); + } + return pvogOrganisationsEinheiten.getFirst().getName(); + } + + SyncResult evaluateSyncResult(List<OrganisationsEinheit> pvogOrganisationsEinheiten, Group group) { + if (pvogOrganisationsEinheiten.isEmpty()) { + return SyncResult.NOT_FOUND_IN_PVOG; + } + if (pvogOrganisationsEinheiten.size() > 1) { + return SyncResult.ORGANISATIONSEINHEIT_ID_NOT_UNIQUE; + } + if (!pvogOrganisationsEinheiten.getFirst().getName().equals(group.getName())) { + return SyncResult.NAME_MISMATCH; + } + return SyncResult.OK; + } + + public String syncZufiId(List<OrganisationsEinheit> pvogOrganisationsEinheiten) { + if (pvogOrganisationsEinheiten.size() != 1) { + return null; + } + return pvogOrganisationsEinheiten.getFirst().getId(); + } + + OrganisationsEinheit saveSyncedOrganisationsEinheit(OrganisationsEinheit syncedOrganisationsEinheit) { + var existingOrganisationsEinheit = repository.findSyncedByKeycloakId(syncedOrganisationsEinheit.getKeycloakId()); + + return repository.save(existingOrganisationsEinheit + .map(organisationsEinheit -> syncedOrganisationsEinheit.toBuilder() + .id(organisationsEinheit.getId()) + .settings(organisationsEinheit.getSettings()).build()) + .orElse(syncedOrganisationsEinheit)); + } + + public void syncAddedOrganisationsEinheiten(long syncTimestamp) { + repository.findAllWithoutSyncResult().forEach( + organisationsEinheit -> syncAddedOrganisationsEinheit(organisationsEinheit, syncTimestamp)); + } + + void syncAddedOrganisationsEinheit(OrganisationsEinheit organisationsEinheit, long syncTimestamp) { + addAsGroupInKeycloak(organisationsEinheit).ifPresent( + addedGroupId -> updateAfterSuccessfulGroupCreation(organisationsEinheit, addedGroupId, syncTimestamp)); + } + + Optional<String> addAsGroupInKeycloak(OrganisationsEinheit organisationsEinheit) { + if (organisationsEinheit.getParentId() != null) { + throw new OrganisationsEinheitSynchronizationException( + "Organisationseinheit %s has parent".formatted(organisationsEinheit.getOrganisationsEinheitId())); + } + var addGroupData = organisationsEinheitMapper.toAddGroupData(organisationsEinheit); + return addGroupInKeycloak(addGroupData); + } + + void updateAfterSuccessfulGroupCreation(OrganisationsEinheit organisationsEinheit, String keycloakId, long syncTimestamp) { + var updatedOrganisationsEinheit = organisationsEinheit.toBuilder() + .keycloakId(keycloakId) + .syncResult(SyncResult.OK) + .lastSyncTimestamp(syncTimestamp) + .build(); + repository.save(updatedOrganisationsEinheit); + } + + Optional<String> addGroupInKeycloak(AddGroupData addGroupData) { + try { + return Optional.of(keycloakRemoteService.addGroup(addGroupData)); + } catch (ResourceCreationException e) { + throw new OrganisationsEinheitSynchronizationException("Error creating group %s in Keycloak".formatted(addGroupData), e); + } + } +} diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index c3514d2f7c0a361129dde64e8f53fb137c55a67f..691b5933ad9270b73fe54d3f0779c5b692e49389 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -1,2 +1,6 @@ ozgcloud: - production: false \ No newline at end of file + production: false + administration: + sync: + organisationseinheiten: + cron: "* */5 * * * *" \ No newline at end of file diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index c4a009f732ce241044969d32e6252ff0ee5e7091..b39daf33653aa0db748c56b8a2353f4614a84dde 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -2,9 +2,23 @@ spring: data: mongodb: uri: mongodb://localhost:27017/config-db + database: config-db security: user: name: user password: password ozgcloud: - production: false \ No newline at end of file + production: false + oauth2: + auth-server-url: http://localhost:8088 + realm: by-kiel-dev + resource: admin + keycloak: + api: + user: administrationApiUser + password: administrationApiUser + +grpc: + client: + zufi-manager: + negotiationType: PLAINTEXT \ No newline at end of file diff --git a/src/main/resources/application-remotekc.yaml b/src/main/resources/application-remotekc.yaml index dab1f7331534b8c20a8af49afd14db0cdab86345..e64923222403088b888536380a901d46bb3fc442 100644 --- a/src/main/resources/application-remotekc.yaml +++ b/src/main/resources/application-remotekc.yaml @@ -2,4 +2,7 @@ ozgcloud: oauth2: auth-server-url: https://sso.dev.by.ozg-cloud.de realm: by-kiel-dev - resource: admin \ No newline at end of file + resource: admin + keycloak: + api: + user: administrationApiUser diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 315e37b9756df692953d1c5761747bfc1e97f4ac..af6a93ebdb869b45fc3c0be85fa38dbe821a0683 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -71,4 +71,22 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: ${ozgcloud.oauth2.auth-server-url}/realms/${ozgcloud.oauth2.realm} \ No newline at end of file + issuer-uri: ${ozgcloud.oauth2.auth-server-url}/realms/${ozgcloud.oauth2.realm} + +ozgcloud: + keycloak: + api: + url: ${ozgcloud.oauth2.auth-server-url} + realm: ${ozgcloud.oauth2.realm} + client: ${ozgcloud.oauth2.resource} + organisations-einheit-id-key: organisationseinheitId + administration: + sync: + organisationseinheiten: + cron: "0 15 0 * * *" + +grpc: + client: + zufi-manager: + address: static://127.0.0.1:9190 + negotiationType: TLS \ No newline at end of file diff --git a/src/test/helm-linter-values.yaml b/src/test/helm-linter-values.yaml index 35b73c5d7532c489c04350074a6b6476b4f5fb20..49a84a11402b24a67c0b7a3525cc06fe535c7189 100644 --- a/src/test/helm-linter-values.yaml +++ b/src/test/helm-linter-values.yaml @@ -34,10 +34,10 @@ networkPolicy: sso: serverUrl: https://sso.company.local operatorNamespace: ozgcloud-keycloak-operator - api_users: - - name: dummy-user - first_name: dummy - last_name: user + api_user: + name: dummy-user + first_name: dummy + last_name: user client: additional_redirect_uris: - https://dummy:8080 diff --git a/src/test/helm/api_password_secret_test.yaml b/src/test/helm/api_password_secret_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d3848ff1ccfefa89cbc3e0ceae91306823ccff9f --- /dev/null +++ b/src/test/helm/api_password_secret_test.yaml @@ -0,0 +1,81 @@ +# +# Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den +# Ministerpräsidenten des Landes Schleswig-Holstein +# Staatskanzlei +# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung +# +# Lizenziert unter der EUPL, Version 1.2 oder - sobald +# diese von der Europäischen Kommission genehmigt wurden - +# Folgeversionen der EUPL ("Lizenz"); +# Sie dürfen dieses Werk ausschließlich gemäß +# dieser Lizenz nutzen. +# Eine Kopie der Lizenz finden Sie hier: +# +# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 +# +# Sofern nicht durch anwendbare Rechtsvorschriften +# gefordert oder in schriftlicher Form vereinbart, wird +# die unter der Lizenz verbreitete Software "so wie sie +# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - +# ausdrücklich oder stillschweigend - verbreitet. +# Die sprachspezifischen Genehmigungen und Beschränkungen +# unter der Lizenz sind dem Lizenztext zu entnehmen. +# + +suite: test api password secret +release: + name: administration + namespace: sh-helm-test +templates: + - templates/api_password_secret.yaml +tests: + - it: should not create api_password_secret if api_user set + set: + sso: + api_user: + name: administrationApiUser + asserts: + - hasDocuments: + count: 0 + - it: should fail if api_user not set and password not set + asserts: + - failedTemplate: + errorMessage: "ozgcloud.keycloak.api.password must be set" + - it: should create api_password_secret if api_user not set and password set + set: + ozgcloud: + keycloak: + api: + password: testPassword + asserts: + - hasDocuments: + count: 1 + - it: test api secret kind + set: + ozgcloud: + keycloak: + api: + password: testPassword + asserts: + - isKind: + of: Secret + - isAPIVersion: + of: v1 + - it: test api password + set: + ozgcloud: + keycloak: + api: + password: testPassword + asserts: + - equal: + path: stringData.password + value: testPassword + - it: not create api_password_secret if api_user set + set: + sso: + api_user: + name: administrationApiUser + asserts: + - hasDocuments: + count: 0 \ No newline at end of file diff --git a/src/test/helm/deployment_env_test.yaml b/src/test/helm/deployment_env_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1bf9144e8ce2a3a31e1dbc2330bb63c9fc0efb7f --- /dev/null +++ b/src/test/helm/deployment_env_test.yaml @@ -0,0 +1,58 @@ +# +# Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den +# Ministerpräsidenten des Landes Schleswig-Holstein +# Staatskanzlei +# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung +# +# Lizenziert unter der EUPL, Version 1.2 oder - sobald +# diese von der Europäischen Kommission genehmigt wurden - +# Folgeversionen der EUPL ("Lizenz"); +# Sie dürfen dieses Werk ausschließlich gemäß +# dieser Lizenz nutzen. +# Eine Kopie der Lizenz finden Sie hier: +# +# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 +# +# Sofern nicht durch anwendbare Rechtsvorschriften +# gefordert oder in schriftlicher Form vereinbart, wird +# die unter der Lizenz verbreitete Software "so wie sie +# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - +# ausdrücklich oder stillschweigend - verbreitet. +# Die sprachspezifischen Genehmigungen und Beschränkungen +# unter der Lizenz sind dem Lizenztext zu entnehmen. +# + +suite: test environment variables +release: + name: administration + namespace: by-helm-test +templates: + - templates/deployment.yaml +set: + ozgcloud: + bundesland: sh + bezeichner: helm + sso: + serverUrl: https://sso.company.local + imagePullSecret: image-pull-secret +tests: + - it: should not contain organisationseinheiten sync cron by default + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: ozgcloud_administration_sync_organisationseinheiten_cron + - it: should contain organisationseinheiten sync cron + set: + ozgcloud: + sync: + organisationseinheiten: + cron: "*/15 * * * *" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: ozgcloud_administration_sync_organisationseinheiten_cron + value: "*/15 * * * *" + + diff --git a/src/test/helm/deployment_keycloak_api_env_test.yaml b/src/test/helm/deployment_keycloak_api_env_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2e0ad9b249cbf409bcad99ade931524e3f3f7766 --- /dev/null +++ b/src/test/helm/deployment_keycloak_api_env_test.yaml @@ -0,0 +1,91 @@ +# +# Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den +# Ministerpräsidenten des Landes Schleswig-Holstein +# Staatskanzlei +# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung +# +# Lizenziert unter der EUPL, Version 1.2 oder - sobald +# diese von der Europäischen Kommission genehmigt wurden - +# Folgeversionen der EUPL ("Lizenz"); +# Sie dürfen dieses Werk ausschließlich gemäß +# dieser Lizenz nutzen. +# Eine Kopie der Lizenz finden Sie hier: +# +# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 +# +# Sofern nicht durch anwendbare Rechtsvorschriften +# gefordert oder in schriftlicher Form vereinbart, wird +# die unter der Lizenz verbreitete Software "so wie sie +# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - +# ausdrücklich oder stillschweigend - verbreitet. +# Die sprachspezifischen Genehmigungen und Beschränkungen +# unter der Lizenz sind dem Lizenztext zu entnehmen. +# + +suite: test environment variables for username and password of keycloak api user +release: + name: administration + namespace: sh-helm-test +templates: + - templates/deployment.yaml +set: + baseUrl: test.company.local + ozgcloud: + environment: test + bundesland: sh + bezeichner: helm + sso: + serverUrl: https://sso.company.local + imagePullSecret: image-pull-secret +tests: + - it: should set user from values when api_user not set + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: ozgcloud_keycloak_api_user + value: administrationApiUser + + - it: should set user from operator-generated secret when api_user is set + set: + sso: + api_user: + name: -administration-ApiUser + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: ozgcloud_keycloak_api_user + valueFrom: + secretKeyRef: + name: administrationapiuser-credentials + key: name + optional: false + + - it: should set password from template-generated secret when api_user is not set + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: ozgcloud_keycloak_api_password + valueFrom: + secretKeyRef: + name: administration-api-password + key: password + optional: false + + - it: should set password from operator-generated secret when api_user is set + set: + sso: + api_user: + name: -administration-ApiUser + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: ozgcloud_keycloak_api_password + valueFrom: + secretKeyRef: + name: administrationapiuser-credentials + key: password + optional: false diff --git a/src/test/helm/keycloak_user_crd_test.yaml b/src/test/helm/keycloak_user_crd_test.yaml index 29c901d0387015bdb782a9ad0b71a115ea16c2cc..850cb2c8f32d43704d8ed8a055ae40aa6d0b99d6 100644 --- a/src/test/helm/keycloak_user_crd_test.yaml +++ b/src/test/helm/keycloak_user_crd_test.yaml @@ -32,8 +32,8 @@ tests: - it: should contain header data set: sso: - api_users: - - name: testapiuser + api_user: + name: testapiuser asserts: - isAPIVersion: of: operator.ozgcloud.de/v1 @@ -42,8 +42,8 @@ tests: - it: should have metadata set: sso: - api_users: - - name: testapiuser + api_user: + name: testapiuser asserts: - equal: path: metadata.name @@ -58,8 +58,8 @@ tests: bezeichner: helm environment: test sso: - api_users: - - name: testapiuser + api_user: + name: testapiuser asserts: - equal: path: spec.keep_after_delete @@ -248,11 +248,11 @@ tests: environment: test baseUrl: "test.by.ozg-cloud.de" sso: - api_users: - - name: testapiuser - first_name: Api - last_name: User - email: testapiuser@ozg-sh.de + api_user: + name: testapiuser + first_name: Api + last_name: User + email: testapiuser@ozg-sh.de asserts: - equal: path: spec.keep_after_delete @@ -284,14 +284,14 @@ tests: environment: test baseUrl: "test.by.ozg-cloud.de" sso: - api_users: - - name: testapiuser - first_name: Api - last_name: User - email: testapiuser@ozg-sh.de - client_roles: - - name: realm-management - role: view-users + api_user: + name: testapiuser + first_name: Api + last_name: User + email: testapiuser@ozg-sh.de + client_roles: + - name: realm-management + role: view-users asserts: - equal: path: spec.keep_after_delete @@ -326,13 +326,13 @@ tests: environment: test baseUrl: "test.by.ozg-cloud.de" sso: - api_users: - - name: testapiuser - first_name: Api - last_name: User - email: testapiuser@ozg-sh.de - realm_roles: - - "offline_access" + api_user: + name: testapiuser + first_name: Api + last_name: User + email: testapiuser@ozg-sh.de + realm_roles: + - "offline_access" asserts: - equal: path: spec.keep_after_delete @@ -366,13 +366,13 @@ tests: environment: test baseUrl: "test.by.ozg-cloud.de" sso: - api_users: - - name: testapiuser - first_name: Api - last_name: User - email: testapiuser@ozg-sh.de - groups: - - Bauamt + api_user: + name: testapiuser + first_name: Api + last_name: User + email: testapiuser@ozg-sh.de + groups: + - Bauamt asserts: - equal: path: spec.keep_after_delete @@ -406,16 +406,16 @@ tests: environment: test baseUrl: "test.by.ozg-cloud.de" sso: - api_users: - - name: testapiuser - first_name: Api - last_name: User - email: testapiuser@ozg-sh.de - realm_roles: - - "offline_access" - client_roles: - - name: realm-management - role: view-users + api_user: + name: testapiuser + first_name: Api + last_name: User + email: testapiuser@ozg-sh.de + realm_roles: + - "offline_access" + client_roles: + - name: realm-management + role: view-users keycloak_users: - name: dorothea first_name: Dorothea @@ -506,8 +506,8 @@ tests: bezeichner: helm environment: test sso: - api_users: - - name: testApiUser + api_user: + name: testApiUser asserts: - equal: path: spec.keycloak_user.username @@ -546,9 +546,9 @@ tests: - it: should set updateUser set: sso: - api_users: - - name: testapiuser - update_user: true + api_user: + name: testapiuser + update_user: true asserts: - equal: path: spec.update_user @@ -557,8 +557,8 @@ tests: - it: should set default updateUser to false set: sso: - api_users: - - name: testapiuser + api_user: + name: testapiuser asserts: - equal: path: spec.update_user diff --git a/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_binding_test.yaml b/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_binding_test.yaml index 6365584bef0d88e2cd172076609c1d918ae877ee..a1c34ab3ecaa3f81f5fb0ebefbb1910b28279e8d 100644 --- a/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_binding_test.yaml +++ b/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_binding_test.yaml @@ -62,11 +62,11 @@ tests: asserts: - hasDocuments: count: 0 - - it: should have subjects values on api_users + - it: should have subjects values on api_user set: sso: - api_users: - - name: apiUser + api_user: + name: apiUser operatorNamespace: test-operator-namespace asserts: - contains: @@ -80,8 +80,8 @@ tests: set: sso: disableOzgOperator: true - api_users: - - name: apiUser + api_user: + name: apiUser asserts: - hasDocuments: count: 0 diff --git a/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_test.yaml b/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_test.yaml index f5e9ad5adcec2bd2d26ac27d602acbd2e1f54c7d..a35779d64276cad49fab52054cd68301cd60a0fb 100644 --- a/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_test.yaml +++ b/src/test/helm/ozgcloud_keycloak_operator_secrets_read_role_test.yaml @@ -55,11 +55,11 @@ tests: asserts: - hasDocuments: count: 0 - - it: should have subjects values on api_users + - it: should have subjects values on api_user set: sso: - api_users: - - name: apiUser- + api_user: + name: apiUser asserts: - contains: path: rules @@ -78,8 +78,8 @@ tests: set: sso: disableOzgOperator: true - api_users: - - name: apiUser + api_user: + name: apiUser asserts: - hasDocuments: count: 0 \ No newline at end of file diff --git a/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_binding_test.yaml b/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_binding_test.yaml index cbc773bf876afcb77f766dd68ead204453be20e4..3980873ea6ac6a58c8d00b5be397f27190dcaca3 100644 --- a/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_binding_test.yaml +++ b/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_binding_test.yaml @@ -62,11 +62,11 @@ tests: asserts: - hasDocuments: count: 0 - - it: should have subjects values on api_users + - it: should have subjects values on api_user set: sso: - api_users: - - name: apiUsers + api_user: + name: apiUsers operatorNamespace: test-operator-namespace asserts: - contains: @@ -80,8 +80,8 @@ tests: set: sso: disableOzgOperator: true - api_users: - - name: apiUser + api_user: + name: apiUser asserts: - hasDocuments: count: 0 diff --git a/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_test.yaml b/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_test.yaml index a06970b1d97c3800f49474ce22173467547a0703..b9d68a70a47a42336d1ede35d294115b67f51a7d 100644 --- a/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_test.yaml +++ b/src/test/helm/ozgcloud_keycloak_operator_secrets_write_role_test.yaml @@ -47,11 +47,11 @@ tests: asserts: - hasDocuments: count: 0 - - it: should have subjects values on api_users + - it: should have subjects values on api_user set: sso: - api_users: - - name: apiUser + api_user: + name: apiUser asserts: - contains: path: rules @@ -67,8 +67,8 @@ tests: set: sso: disableOzgOperator: true - api_users: - - name: apiUser + api_user: + name: apiUser asserts: - hasDocuments: count: 0 \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurerTest.java b/src/test/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c07cece24c0f6f57e39c49a0e5b3273f123b4ebb --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurerTest.java @@ -0,0 +1,37 @@ +package de.ozgcloud.admin; + +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.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.hateoas.server.LinkRelationProvider; +import org.springframework.hateoas.server.core.DefaultLinkRelationProvider; + +class AdministrationRepositoryRestConfigurerTest { + + @InjectMocks + private AdministrationRepositoryRestConfigurer configurer; + + @Nested + class TestConfigureRepositoryRestConfiguration { + + @Captor + private ArgumentCaptor<LinkRelationProvider> linkRelationProviderArgumentCaptor; + + @Test + void shouldUseDefaultLinkRelationProvider() { + var configuration = mock(RepositoryRestConfiguration.class); + + configurer.configureRepositoryRestConfiguration(configuration, null); + + verify(configuration).setLinkRelationProvider(linkRelationProviderArgumentCaptor.capture()); + assertThat(linkRelationProviderArgumentCaptor.getValue()).isInstanceOf(DefaultLinkRelationProvider.class); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/common/CollectionModelBuilderTest.java b/src/test/java/de/ozgcloud/admin/common/CollectionModelBuilderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bc4dd74e3f2b4121bf7fc681b8ab5de5cdc42fde --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/CollectionModelBuilderTest.java @@ -0,0 +1,92 @@ +package de.ozgcloud.admin.common; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.hateoas.Link; + +import de.ozgcloud.admin.organisationseinheit.OrganisationsEinheit; +import de.ozgcloud.admin.organisationseinheit.OrganisationsEinheitTestFactory; + +class CollectionModelBuilderTest { + + private final String HREF = "http://test"; + private final String REL = "rel"; + + @Nested + class TestBuildModel { + + @Test + void shouldBuildModel() { + var vorgang = OrganisationsEinheitTestFactory.create(); + + var model = CollectionModelBuilder.fromEntities(List.of(vorgang)).buildModel(); + + assertThat(model.getContent()).hasSize(1).first().usingRecursiveComparison().isEqualTo(vorgang); + } + } + + @Nested + class TestAddLink { + + @Test + void shouldAddLink() { + var model = CollectionModelBuilder.fromEntities(List.of()).addLink(Link.of(HREF, REL)).buildModel(); + + assertThat(model.getLinks()).hasSize(1).first().extracting(Link::getHref, link -> link.getRel().value()).containsExactly(HREF, REL); + } + } + + @Nested + class TestIfMatch { + + @Nested + class TestWithBooleanSupplier { + + @Test + void shouldAddLink() { + var model = CollectionModelBuilder.fromEntities(List.of()).ifMatch(() -> true).addLink(Link.of(HREF, REL)).buildModel(); + + assertThat(model.getLinks()).hasSize(1).first().extracting(Link::getHref, link -> link.getRel().value()).containsExactly(HREF, REL); + } + + @Test + void shouldNotAddLink() { + var model = CollectionModelBuilder.fromEntities(List.of()).ifMatch(() -> false).addLink(Link.of(HREF, REL)).buildModel(); + + assertThat(model.getLinks()).isEmpty(); + } + } + + @Nested + class TestWithPredicate { + + private final Stream<OrganisationsEinheit> wiedervorlageStream = Stream.of(OrganisationsEinheitTestFactory.create()); + + @Test + void shouldAddLink() { + var model = CollectionModelBuilder.fromEntities(wiedervorlageStream) + .ifMatch(wiedervorlagen -> true) + .addLink(Link.of(HREF, REL)) + .buildModel(); + + assertThat(model.getLinks()).hasSize(1).first().extracting(Link::getHref, link -> link.getRel().value()).containsExactly(HREF, REL); + } + + @Test + void shouldNotAddLink() { + var model = CollectionModelBuilder.fromEntities(wiedervorlageStream) + .ifMatch(wiedervorlagen -> false) + .addLink(Link.of(HREF, REL)) + .buildModel(); + + assertThat(model.getLinks()).isEmpty(); + } + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/common/EntityModelTestFactory.java b/src/test/java/de/ozgcloud/admin/common/EntityModelTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..99617d582151e26fa46c12e4ef40e585b18fa0eb --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/EntityModelTestFactory.java @@ -0,0 +1,20 @@ +package de.ozgcloud.admin.common; + +import org.springframework.hateoas.EntityModel; + +import de.ozgcloud.admin.organisationseinheit.OrganisationsEinheit; +import lombok.NoArgsConstructor; + +public class EntityModelTestFactory { + + public static final NullableEntityModel NULLABLE = createNullable(); + + private static NullableEntityModel createNullable() { + return new NullableEntityModel(); + } + + @NoArgsConstructor + private static class NullableEntityModel extends EntityModel<OrganisationsEinheit> { + + } +} diff --git a/src/test/java/de/ozgcloud/admin/common/IdBuilderTest.java b/src/test/java/de/ozgcloud/admin/common/IdBuilderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6a3d3a08d5b55434bc96d58632846d2da3936eab --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/IdBuilderTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.common; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.BeanProperty; + +class IdBuilderTest { + + @DisplayName("Test building ID when deserializing linked resources") + @Nested + class TestBuilingId { + private static final String ID = "id"; + + @Test + void shouldBuildId() { + IdBuilder idBuilder = new IdBuilder(); + + var idObject = idBuilder.build(ID); + + assertThat(idObject).isInstanceOf(Object.class).asString().isEqualTo(ID); + } + + @Test + void shouldCreateObjectBuilder() { + BeanProperty property = mock(BeanProperty.class); + ObjectBuilder<Object> idBuilder = new IdBuilder().constructContextAware(property); + + assertThat(idBuilder).isNotNull(); + } + } +} diff --git a/src/test/java/de/ozgcloud/admin/common/KeycloakInitializer.java b/src/test/java/de/ozgcloud/admin/common/KeycloakInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..7fec947e3c250ecaa53b158bfa266c05e57acf9e --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/KeycloakInitializer.java @@ -0,0 +1,47 @@ +package de.ozgcloud.admin.common; + +import java.time.Duration; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.testcontainers.containers.wait.strategy.Wait; + +import dasniko.testcontainers.keycloak.KeycloakContainer; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class KeycloakInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { + + private static final String AUTH_SERVER_URL = "ozgcloud.oauth2.auth-server-url"; + + private static KeycloakContainer keycloakContainer; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + initContainer(); + setProperties(applicationContext); + } + + @SuppressWarnings("resource") + private void initContainer() { + if (keycloakContainer == null) { + LOG.info("Creating Keycloak-container..."); + keycloakContainer = new KeycloakContainer().withRealmImportFile("keycloak/realm-export.json").withVerboseOutput(); + } + if (!keycloakContainer.isRunning()) { + LOG.info("Starting Keycloak-container..."); + keycloakContainer.setWaitStrategy( + Wait.forLogMessage(".*message\":\"started.*", 1).withStartupTimeout(Duration.ofMinutes(3))); + keycloakContainer.start(); + LOG.info("Keycloak-container started"); + } + } + + private void setProperties(ConfigurableApplicationContext applicationContext) { + LOG.info("Keycloak URL: {}", keycloakContainer.getAuthServerUrl()); + + TestPropertyValues.of(AUTH_SERVER_URL + "=" + keycloakContainer.getAuthServerUrl()).applyTo(applicationContext); + + } +} diff --git a/src/test/java/de/ozgcloud/admin/common/LinkedResourceDeserializerTest.java b/src/test/java/de/ozgcloud/admin/common/LinkedResourceDeserializerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..85c2cffe20da1dfc5195087a66dc8ef069080f55 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/LinkedResourceDeserializerTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.common; + +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.ozgcloud.admin.common.user.TestId; +import de.ozgcloud.admin.organisationseinheit.OrganisationsEinheitTestFactory; + +class LinkedResourceDeserializerTest { + private static final String TEST_JSON = "{\"id\":\"/api/vorgangs/" + OrganisationsEinheitTestFactory.ID + "\"}"; + + @DisplayName("Test the deserilization of linked resource json") + @Nested + class TestDeserialization { + @Test + void shouldDeserialize() throws IOException { + LinkedResourceTestObject res = new ObjectMapper().readValue(TEST_JSON.getBytes(), LinkedResourceTestObject.class); + + assertThat(res).hasFieldOrPropertyWithValue("id", TestId.from(OrganisationsEinheitTestFactory.ID)); + } + + } + +} diff --git a/src/test/java/de/ozgcloud/admin/common/LinkedResourceSerializerTest.java b/src/test/java/de/ozgcloud/admin/common/LinkedResourceSerializerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..dacf8bcedacb365629ea84c8e5366ed8f540b994 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/LinkedResourceSerializerTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.common; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.ozgcloud.admin.common.user.TestId; +import de.ozgcloud.admin.organisationseinheit.OrganisationsEinheitTestFactory; + +class LinkedResourceSerializerTest { + + @DisplayName("Test the json serialization of linked resource annotations") + @Nested + class TestSerialization { + @Test + void shouldSerialize() throws JsonProcessingException { + var testObj = new LinkedResourceTestObject(TestId.from(OrganisationsEinheitTestFactory.ID)); + + String serialized = new ObjectMapper().writeValueAsString(testObj); + + assertThat(serialized).isEqualTo("{\"id\":\"/api/organisationseinheits/" + OrganisationsEinheitTestFactory.ID + "\"}"); + } + + } +} diff --git a/src/test/java/de/ozgcloud/admin/common/LinkedResourceTestObject.java b/src/test/java/de/ozgcloud/admin/common/LinkedResourceTestObject.java new file mode 100644 index 0000000000000000000000000000000000000000..f387e57475d5355f758eb1d2941d77fc51f4a74c --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/LinkedResourceTestObject.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.common; + +import de.ozgcloud.admin.common.user.TestId; +import de.ozgcloud.admin.organisationseinheit.OrganisationsEinheitController; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +class LinkedResourceTestObject { + @LinkedResource(controllerClass = OrganisationsEinheitController.class) + private TestId id; +} diff --git a/src/test/java/de/ozgcloud/admin/common/ModelBuilderTest.java b/src/test/java/de/ozgcloud/admin/common/ModelBuilderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..600e54392d19e7bf6b107ae6741f5b80488c926b --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/ModelBuilderTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.common; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +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.NullAndEmptySource; +import org.mockito.Mock; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.RequestMapping; + +import lombok.Builder; + +class ModelBuilderTest { + + @DisplayName("Add link by annotation if missing") + @Nested + class TestAddLinkByAnnotationIfMissing { + + private static final String USER_MANAGER_URL = "http://localhost"; + private static final String USER_MANAGER_PROFILE_TEMPLATE = "/api/profile/%s"; + + @Mock + private ApplicationContext context; + + private TestEntity entity = TestEntityTestFactory.create(); + + @Test + void shouldHaveAddLinkByLinkedResource() { + var model = ModelBuilder.fromEntity(entity).buildModel(); + + assertThat(model.getLink(TestController.FILE_REL).get().getHref()).isEqualTo(TestController.PATH + "/" + TestEntityTestFactory.FILE); + } + + @ParameterizedTest + @NullAndEmptySource + void shouldNotAddLinkByLinkedRessourceIfFieldValueIsNotSet(String fileId) { + var model = ModelBuilder.fromEntity(TestEntityTestFactory.createBuilder().file(fileId).build()).buildModel(); + + assertThat(model.getLink(TestController.FILE_REL)).isEmpty(); + } + } +} + +@Builder +class TestEntity { + + @LinkedResource(controllerClass = TestController.class) + private String file; +} + +@RequestMapping(TestController.PATH) +class TestController { + + static final String PATH = "/api/test"; + + static final String USER_REL = "user"; + static final String FILE_REL = "file"; + +} + +class TestEntityTestFactory { + + static final String USER = UUID.randomUUID().toString(); + static final String FILE = UUID.randomUUID().toString(); + + public static TestEntity create() { + return createBuilder().build(); + } + + public static TestEntity.TestEntityBuilder createBuilder() { + return TestEntity.builder() + .file(FILE); + } +} diff --git a/src/test/java/de/ozgcloud/admin/common/user/TestId.java b/src/test/java/de/ozgcloud/admin/common/user/TestId.java new file mode 100644 index 0000000000000000000000000000000000000000..062ac78d9fed39e852260dec5f4ed938f3209346 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/user/TestId.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.admin.common.user; + +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; + +import de.ozgcloud.common.datatype.StringBasedValue; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PACKAGE) +@EqualsAndHashCode(callSuper = true) +public class TestId extends StringBasedValue { + + private static final long serialVersionUID = 1L; + + TestId(String userId) { + super(userId); + } + + public static TestId from(UUID userId) { + return TestId.from(userId.toString()); + } + + public static TestId from(String userId) { + return new TestId(userId); + } + + public static TestId empty() { + return new TestId(StringUtils.EMPTY); + } + +} diff --git a/src/test/java/de/ozgcloud/admin/keycloak/AddGroupDataTestFactory.java b/src/test/java/de/ozgcloud/admin/keycloak/AddGroupDataTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..cea95944ffe15491d5a60ead097db8a8a29c4989 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/keycloak/AddGroupDataTestFactory.java @@ -0,0 +1,17 @@ +package de.ozgcloud.admin.keycloak; + +public class AddGroupDataTestFactory { + + public static final String NAME = GroupRepresentationTestFactory.NAME; + public static final String ORGANISATIONS_EINHEIT_ID = GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID; + + public static AddGroupData create() { + return createBuilder().build(); + } + + public static AddGroupData.AddGroupDataBuilder createBuilder() { + return new AddGroupData.AddGroupDataBuilder() + .name(NAME) + .organisationsEinheitId(ORGANISATIONS_EINHEIT_ID); + } +} diff --git a/src/test/java/de/ozgcloud/admin/keycloak/GroupMapperTest.java b/src/test/java/de/ozgcloud/admin/keycloak/GroupMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..82b47bca310517e47c6ac5b812c4abc9aef66db9 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/keycloak/GroupMapperTest.java @@ -0,0 +1,318 @@ +package de.ozgcloud.admin.keycloak; + +import static de.ozgcloud.admin.keycloak.GroupRepresentationTestFactory.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +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.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.keycloak.representations.idm.GroupRepresentation; +import org.mapstruct.factory.Mappers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +class GroupMapperTest { + + @Mock + private KeycloakApiProperties properties; + @Spy + @InjectMocks + private GroupMapper mapper = Mappers.getMapper(GroupMapper.class); + + @Nested + class TestFromGroupRepresentations { + + private final GroupRepresentation groupRepresentation = GroupRepresentationTestFactory.create(); + + @Captor + private ArgumentCaptor<List<Group>> groupsCaptor; + + @Test + void shouldMapFromGroupRepresentation() { + givenMappedGroupWithOrganisationsEinheitId(); + + callMapper(); + + verify(mapper).fromGroupRepresentation(groupRepresentation); + } + + @Test + void shouldReturnListWithMappedGroupRepresentation() { + var group = givenMappedGroupWithOrganisationsEinheitId(); + + var groups = callMapper(); + + assertThat(groups).containsExactly(group); + } + + @ParameterizedTest + @NullAndEmptySource + void shouldReturnEmptyList(String organisationsEinheitId) { + givenMappedGroupWithOrganisationsEinheitId(organisationsEinheitId); + + var groups = callMapper(); + + assertThat(groups).isEmpty(); + } + + @Test + void shouldDeleteGroupsWithoutOrganisationsEinheitId() { + var group = givenMappedGroupWithOrganisationsEinheitId(); + + callMapper(); + + verify(mapper).deleteGroupsWithoutOrganisationsEinheitId(groupsCaptor.capture()); + assertThat(groupsCaptor.getValue()).containsExactly(group); + } + + private Group givenMappedGroupWithOrganisationsEinheitId() { + var group = GroupTestFactory.create(); + doReturn(group).when(mapper).fromGroupRepresentation(groupRepresentation); + return group; + } + + private void givenMappedGroupWithOrganisationsEinheitId(String id) { + var group = GroupTestFactory.createBuilder().organisationsEinheitId(id).build(); + doReturn(group).when(mapper).fromGroupRepresentation(groupRepresentation); + } + + private List<Group> callMapper() { + return mapper.fromGroupRepresentations(List.of(groupRepresentation)); + } + } + + @Nested + class TestFromGroupRepresentation { + + private final GroupRepresentation groupRepresentation = GroupRepresentationTestFactory.create(); + + @BeforeEach + void init() { + doReturn(GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID).when(mapper) + .getOrganisationsEinheitId(ATTRIBUTES); + doReturn(GroupRepresentationTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID).when(mapper) + .getOrganisationsEinheitId(SUB_GROUP_ATTRIBUTES); + } + + @Test + void shouldGetOrganisationsEinheitId() { + callMapper(); + + verify(mapper).getOrganisationsEinheitId(groupRepresentation.getAttributes()); + } + + @Test + void shouldSetId() { + var group = callMapper(); + + assertThat(group.getId()).isEqualTo(groupRepresentation.getId()); + } + + @Test + void shouldSetOrganisationsEinheitId() { + var group = callMapper(); + + assertThat(group.getOrganisationsEinheitId()).isEqualTo(GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID); + } + + @Test + void shouldSetName() { + var group = callMapper(); + + assertThat(group.getName()).isEqualTo(GroupRepresentationTestFactory.NAME); + } + + @Test + void shouldSetSubGroups() { + var group = callMapper(); + + assertThat(group.getSubGroups()).usingRecursiveFieldByFieldElementComparator() + .containsExactlyElementsOf(GroupTestFactory.create().getSubGroups()); + } + + private Group callMapper() { + return mapper.fromGroupRepresentation(groupRepresentation); + } + } + + @Nested + class TestGetOrganisationsEinheitId { + + @Test + void shouldReturnNullIfAttributesAreNull() { + var result = mapper.getOrganisationsEinheitId(null); + + assertThat(result).isNull(); + } + + @Test + void shouldReturnNullIfAttributeIsAbsent() { + givenOrganisationsEinheitIdProperty(); + + var result = mapper.getOrganisationsEinheitId(Map.of("dummy-attribute", List.of("123"))); + + assertThat(result).isNull(); + } + + @Test + void shouldReturnOrganisationsEinheitId() { + givenOrganisationsEinheitIdProperty(); + var value = GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID; + var result = mapper.getOrganisationsEinheitId(Map.of(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, List.of(value))); + + assertThat(result).isEqualTo(value); + } + + @Test + void shouldThrowExceptionIfMultipleValuesAreAvailable() { + givenOrganisationsEinheitIdProperty(); + var value = GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID; + var value2 = UUID.randomUUID().toString(); + + assertThatExceptionOfType(GroupRepresentationMappingException.class) + .isThrownBy(() -> mapper.getOrganisationsEinheitId(Map.of(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, List.of(value, value2)))) + .withMessage("Group contains multiple values for organisationsEinheitId: %s", List.of(value, value2)); + } + } + + @Nested + class TestDeleteGroupsWithoutOrganisationsEinheitId { + + private final Group group = GroupTestFactory.create(); + private final List<Group> groups = new ArrayList<>(); + + @BeforeEach + void init() { + groups.add(group); + } + + @Test + void shouldCheckIfGroupHasOrganisationsEinheitId() { + mapper.deleteGroupsWithoutOrganisationsEinheitId(groups); + + verify(mapper).isMissingOrganisationsEinheitId(group); + } + + @Test + void shouldKeepGroupsWithOrganisationsEinheitId() { + doReturn(false).when(mapper).isMissingOrganisationsEinheitId(group); + + mapper.deleteGroupsWithoutOrganisationsEinheitId(groups); + + assertThat(groups).containsExactly(group); + } + + @Test + void shouldRemoveGroupsWithoutOrganisationsEinheitId() { + doReturn(true).when(mapper).isMissingOrganisationsEinheitId(group); + + mapper.deleteGroupsWithoutOrganisationsEinheitId(groups); + + assertThat(groups).isEmpty(); + } + } + + @Nested + class TestIsMissingOrganisationsEinheitId { + + @ParameterizedTest + @ValueSource(strings = " ") + @NullAndEmptySource + void shouldReturnTrueIfIdIsBlank(String organisationsEinheitId) { + var group = GroupTestFactory.createBuilder().organisationsEinheitId(organisationsEinheitId).build(); + + var isMissing = mapper.isMissingOrganisationsEinheitId(group); + + assertThat(isMissing).isTrue(); + } + + @Test + void shouldReturnFalseIfIdIsSet() { + var isMissing = mapper.isMissingOrganisationsEinheitId(GroupTestFactory.create()); + + assertThat(isMissing).isFalse(); + } + } + + @Nested + class TestToGroupRepresentation { + + private final AddGroupData addGroupData = AddGroupDataTestFactory.create(); + + @BeforeEach + void init() { + doReturn(ATTRIBUTES).when(mapper).buildGroupAttributes(addGroupData); + } + + @Test + void shouldSetName() { + var groupRepresentation = callMapper(); + + assertThat(groupRepresentation.getName()).isEqualTo(addGroupData.getName()); + } + + @Test + void shouldNotSetSubGroups() { + var groupRepresentation = callMapper(); + + assertThat(groupRepresentation.getSubGroups()).isEmpty(); + } + + @Test + void shouldBuildGroupAttributes() { + callMapper(); + + verify(mapper).buildGroupAttributes(addGroupData); + } + + @Test + void shouldSetAttributeOrganisationsEinheitId() { + var groupRepresentation = callMapper(); + + assertThat(groupRepresentation.getAttributes()).isNotNull().containsKey(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE); + } + + private GroupRepresentation callMapper() { + return mapper.toGroupRepresentation(addGroupData); + } + } + + @Nested + class TestBuildGroupAttributes { + + @ParameterizedTest + @NullAndEmptySource + void shouldReturnEmptyMap(String organisationsEinheitId) { + var addGroupData = AddGroupDataTestFactory.createBuilder().organisationsEinheitId(organisationsEinheitId).build(); + + var attributes = mapper.buildGroupAttributes(addGroupData); + + assertThat(attributes).isEmpty(); + } + + @Test + void shouldAddOrganisationsEinheitIdToAttributes() { + givenOrganisationsEinheitIdProperty(); + + var attributes = mapper.buildGroupAttributes(AddGroupDataTestFactory.create()); + + assertThat(attributes).hasSize(1).containsEntry(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, List.of(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)); + } + } + + private void givenOrganisationsEinheitIdProperty() { + when(properties.getOrganisationsEinheitIdKey()).thenReturn(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE); + } +} diff --git a/src/test/java/de/ozgcloud/admin/keycloak/GroupRepresentationTestFactory.java b/src/test/java/de/ozgcloud/admin/keycloak/GroupRepresentationTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..034f962513dc0ffefe78f26ab9d7755281d763a0 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/keycloak/GroupRepresentationTestFactory.java @@ -0,0 +1,64 @@ +package de.ozgcloud.admin.keycloak; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.keycloak.representations.idm.GroupRepresentation; + +import com.thedeanda.lorem.LoremIpsum; + +class GroupRepresentationTestFactory { + + public static final String ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE = "organisationseinheitId"; + + public static final String ID = UUID.randomUUID().toString(); + public static final String PARENT_ID = UUID.randomUUID().toString(); + public static final String NAME = LoremIpsum.getInstance().getName(); + public static final String ORGANISATIONS_EINHEIT_ID = UUID.randomUUID().toString(); + public static final Map<String, List<String>> ATTRIBUTES = Map.of(ORGANIZATIONS_EINHEIT_ID_ATTRIBUTE, List.of(ORGANISATIONS_EINHEIT_ID)); + + public static final String SUB_GROUP_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 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() { + return create(true); + } + + public static GroupRepresentation create(String name) { + var groupRepresentation = create(); + groupRepresentation.setName(name); + return groupRepresentation; + } + + 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(); + if (withId) { + groupRepresentation.setId(ID); + } + groupRepresentation.setParentId(PARENT_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.setParentId(ID); + groupRepresentation.setName(SUB_GROUP_NAME); + groupRepresentation.setAttributes(SUB_GROUP_ATTRIBUTES); + return groupRepresentation; + } +} diff --git a/src/test/java/de/ozgcloud/admin/keycloak/GroupTestFactory.java b/src/test/java/de/ozgcloud/admin/keycloak/GroupTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..ed248fdde42e8e6c245a1879aa1373df3e8e17aa --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/keycloak/GroupTestFactory.java @@ -0,0 +1,27 @@ +package de.ozgcloud.admin.keycloak; + +import java.util.List; + +public class GroupTestFactory { + + public static final String ID = GroupRepresentationTestFactory.ID; + public static final String NAME = GroupRepresentationTestFactory.NAME; + public static final String ORGANISATIONS_EINHEIT_ID = GroupRepresentationTestFactory.ORGANISATIONS_EINHEIT_ID; + + public static final String SUB_GROUP_ID = GroupRepresentationTestFactory.SUB_GROUP_ID; + 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(); + } + + public static Group.GroupBuilder createBuilder() { + return new Group.GroupBuilder() + .id(ID) + .name(NAME) + .organisationsEinheitId(ORGANISATIONS_EINHEIT_ID) + .subGroups(List.of(Group.builder().id(SUB_GROUP_ID).name(SUB_GROUP_NAME).organisationsEinheitId(SUB_GROUP_ORGANISATIONS_EINHEIT_ID) + .build())); + } +} diff --git a/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiServiceITCase.java b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiServiceITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..f2c7dd6c83ee9651cca09c17ad7eda8927ac94eb --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiServiceITCase.java @@ -0,0 +1,172 @@ +package de.ozgcloud.admin.keycloak; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.groups.Tuple.tuple; + +import java.util.List; +import java.util.Optional; + +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 com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.admin.common.KeycloakInitializer; +import de.ozgcloud.common.test.ITCase; + +@ITCase +@ContextConfiguration(initializers = KeycloakInitializer.class) +class KeycloakApiServiceITCase { + + @Autowired + private KeycloakApiService service; + @Autowired + private KeycloakApiProperties properties; + + @Nested + class TestGetAllGroups { + + @Test + void shouldReturnAllTopLevelGroups() { + var groups = service.getAllGroups(); + + assertThat(groups).extracting(GroupRepresentation::getName) + .contains("GroupWithChild", "GroupWithoutOid", "GroupWithoutOidWithChild"); + } + + @Test + void shouldTopLevelGroupsHaveAttributes() { + var groups = service.getAllGroups(); + + assertThat(groups).extracting(GroupRepresentation::getName, this::getOrganisationsEinheitId) + .contains(tuple("GroupWithChild", "GroupWithChild-oid"), tuple("GroupWithoutOid", null), + tuple("GroupWithoutOidWithChild", null)); + } + + @Test + void shouldGroupWithChildHaveLevel1Children() { + var groups = service.getAllGroups(); + var groupWithChild = getTopLevelGroup(groups, "GroupWithChild"); + + assertThat(groupWithChild.getSubGroups()).hasSize(2).extracting(GroupRepresentation::getName) + .containsExactlyInAnyOrder("ChildLevel1WithChild", "ChildLevel1WithoutOid"); + } + + @Test + void shouldLevel1ChildrenOfGroupWithChildHaveAttributes() { + var groups = service.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 = service.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 = service.getAllGroups(); + var groupWithoutOid = getTopLevelGroup(groups, "GroupWithoutOidWithChild"); + + assertThat(groupWithoutOid.getSubGroups()).hasSize(1).extracting(GroupRepresentation::getName) + .containsExactlyInAnyOrder("ChildLevel1"); + } + + @Test + void shouldLevel1ChildrenOfGroupWithoutOidHaveAttributes() { + var groups = service.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(properties.getOrganisationsEinheitIdKey()) ? + attributes.get(properties.getOrganisationsEinheitIdKey()).getFirst() : + 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 shouldNotAddSubgroupsOfAddedGroup() { + var groupToAdd = createUniqueGroupRepresentation("shouldNotAddSubgroups"); + + var groupId = service.addGroup(groupToAdd); + + assertThat(findGroupInKeycloak(groupId)).isPresent().get().extracting(GroupRepresentation::getSubGroups) + .asList().isEmpty(); + } + + private GroupRepresentation createUniqueGroupRepresentation(String nameSuffix) { + // LoremIpsum does not guarantee unique results when called repeatedly + var groupName = "%s (%s)".formatted(LoremIpsum.getInstance().getName(), nameSuffix); + var group = GroupRepresentationTestFactory.createWithoutId(groupName); + group.setParentId(null); + return group; + } + + private Optional<GroupRepresentation> findGroupInKeycloak(String groupId) { + return service.getAllGroups().stream() + .filter(groupRepresentation -> groupId.equals(groupRepresentation.getId())) + .findFirst(); + } + } +} diff --git a/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiServiceTest.java b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ce9ed1256d94d81e5e04e820fe2436bfdc34b168 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakApiServiceTest.java @@ -0,0 +1,151 @@ +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.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 TestGetAllGroups { + + @Test + void shouldCallGroupsResource() { + service.getAllGroups(); + + verify(groupsResource).groups("", 0, Integer.MAX_VALUE, false); + } + + @Test + void shouldReturnGroupRepresentations() { + var groupRepresentation = GroupRepresentationTestFactory.create(); + when(groupsResource.groups("", 0, Integer.MAX_VALUE, false)).thenReturn(List.of(groupRepresentation)); + + var gotGroupRepresentations = service.getAllGroups(); + + assertThat(gotGroupRepresentations).containsExactly(groupRepresentation); + } + } + + @Nested + class TestAddGroup { + + private final GroupRepresentation groupRepresentation = GroupRepresentationTestFactory.create(); + private final String resourceId = GroupRepresentationTestFactory.create().getId(); + + @BeforeEach + void init() { + when(groupsResource.add(groupRepresentation)).thenReturn(response); + doReturn(resourceId).when(service).getCreatedResourceIdFromResponse(response); + } + + @Test + void shouldCallGroupsResource() { + callService(); + + verify(groupsResource).add(groupRepresentation); + } + + @Test + void shouldGetAddedResourceIdFromResponse() { + callService(); + + verify(service).getCreatedResourceIdFromResponse(response); + } + + @Test + void shouldReturnAddedResourceId() { + var id = callService(); + + assertThat(id).isEqualTo(resourceId); + } + + private String callService() { + return service.addGroup(groupRepresentation); + } + } + + @Nested + class TestGetCreatedResourceIdFromResponse { + + private final String resourceId = 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(resourceId); + } + + @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(resourceId).when(service).extractResourceIdFromLocationHeader(location); + } + + private void givenResponseUnauthorized() { + when(response.getStatus()).thenReturn(403); + when(response.getStatusInfo()).thenReturn(Response.Status.UNAUTHORIZED); + } + + private String callService() { + return service.getCreatedResourceIdFromResponse(response); + } + } + + @Nested + class TestExtractResourceIdFromLocationHeader { + + private final String resourceId = GroupRepresentationTestFactory.create().getId(); + private final String location = "https://keycloak-url.test/admin/realms/test/groups/" + resourceId; + + @Test + void shouldReturnId() { + var id = service.extractResourceIdFromLocationHeader(location); + + assertThat(id).isEqualTo(resourceId); + } + } +} 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 0000000000000000000000000000000000000000..928122d6599d86a99b19b270e173827d27c3c269 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/keycloak/KeycloakRemoteServiceTest.java @@ -0,0 +1,104 @@ +package de.ozgcloud.admin.keycloak; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.UUID; + +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 KeycloakApiService apiService; + @Mock + private GroupMapper mapper; + @Spy + @InjectMocks + private KeycloakRemoteService service; + + @Nested + class TestGetGroupsWithOrganisationsEinheitId { + + 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(apiService.getAllGroups()).thenReturn(groupRepresentations); + when(mapper.fromGroupRepresentations(groupRepresentations)).thenReturn(mappedGroups); + } + + @Test + void shouldGetAllGroups() { + service.getGroupsWithOrganisationsEinheitId(); + + verify(apiService).getAllGroups(); + } + + @Test + void shouldMapGroups() { + service.getGroupsWithOrganisationsEinheitId(); + + verify(mapper).fromGroupRepresentations(groupRepresentations); + } + + @Test + void shouldReturnMappedGroups() { + var groups = service.getGroupsWithOrganisationsEinheitId(); + + assertThat(groups).containsExactlyElementsOf(mappedGroups); + } + } + + @Nested + class TestAddGroup { + + private static final String ADDED_GROUP_ID = UUID.randomUUID().toString(); + + private final AddGroupData addGroupData = AddGroupDataTestFactory.create(); + private final GroupRepresentation groupRepresentation = GroupRepresentationTestFactory.create(); + + @BeforeEach + void init() { + when(mapper.toGroupRepresentation(addGroupData)).thenReturn(groupRepresentation); + when(apiService.addGroup(groupRepresentation)).thenReturn(ADDED_GROUP_ID); + } + + @Test + void shouldMapToGroupRepresentation() { + callService(); + + verify(mapper).toGroupRepresentation(addGroupData); + } + + @Test + void shouldAddGroupInKeycloak() { + callService(); + + verify(apiService).addGroup(groupRepresentation); + } + + @Test + void shouldReturnIdOfAddedGroup() { + var id = callService(); + + assertThat(id).isEqualTo(ADDED_GROUP_ID); + } + + private String callService() { + return service.addGroup(addGroupData); + } + } +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcGetByOrganisationsEinheitIdRequestTestFactory.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcGetByOrganisationsEinheitIdRequestTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..8e3c82212d12ec8115fb413e2ec4c12dadb9202b --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcGetByOrganisationsEinheitIdRequestTestFactory.java @@ -0,0 +1,18 @@ +package de.ozgcloud.admin.organisationseinheit; + +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcGetByOrganisationsEinheitIdRequest; + +public class GrpcGetByOrganisationsEinheitIdRequestTestFactory { + + public static final String ORGANISATIONS_EINHEIT_ID = OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID; + + public static GrpcGetByOrganisationsEinheitIdRequest create() { + return createBuilder().build(); + } + + public static GrpcGetByOrganisationsEinheitIdRequest.Builder createBuilder() { + return GrpcGetByOrganisationsEinheitIdRequest.newBuilder() + .setOrganisationsEinheitId(ORGANISATIONS_EINHEIT_ID); + } + +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcGetByOrganisationsEinheitIdResponseTestFactory.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcGetByOrganisationsEinheitIdResponseTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e71492d4a93a233c1c7fa6afd31d9b61302a3699 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcGetByOrganisationsEinheitIdResponseTestFactory.java @@ -0,0 +1,18 @@ +package de.ozgcloud.admin.organisationseinheit; + +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcGetByOrganisationsEinheitIdResponse; +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcOrganisationsEinheit; + +public class GrpcGetByOrganisationsEinheitIdResponseTestFactory { + + public static final GrpcOrganisationsEinheit GRPC_ORGANISATIONS_EINHEIT = GrpcOrganisationsEinheitTestFactory.create(); + + public static GrpcGetByOrganisationsEinheitIdResponse create() { + return createBuilder().build(); + } + + public static GrpcGetByOrganisationsEinheitIdResponse.Builder createBuilder() { + return GrpcGetByOrganisationsEinheitIdResponse.newBuilder() + .addOrganisationsEinheiten(GRPC_ORGANISATIONS_EINHEIT); + } +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcOrganisationsEinheitTestFactory.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcOrganisationsEinheitTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..ed0a83acf32871168ee11c498e991be74354b7a9 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcOrganisationsEinheitTestFactory.java @@ -0,0 +1,23 @@ +package de.ozgcloud.admin.organisationseinheit; + +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcOrganisationsEinheit; +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcXzufiId; + +public class GrpcOrganisationsEinheitTestFactory { + + public static final String ID = OrganisationsEinheitTestFactory.ZUFI_ID; + public static final String NAME = OrganisationsEinheitTestFactory.NAME; + public static final GrpcXzufiId XZUFI_ID = GrpcXzufiIdTestFactory.create(); + + public static GrpcOrganisationsEinheit create() { + return createBuilder().build(); + } + + public static GrpcOrganisationsEinheit.Builder createBuilder() { + return GrpcOrganisationsEinheit.newBuilder() + .setId(ID) + .setName(NAME) + .setXzufiId(XZUFI_ID); + } + +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcXzufiIdTestFactory.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcXzufiIdTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..7c7b71e25f7312cb86560ae7134d9d1bda28cba2 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/GrpcXzufiIdTestFactory.java @@ -0,0 +1,21 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.util.UUID; + +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcXzufiId; + +public class GrpcXzufiIdTestFactory { + + public static final String ORGANISATIONS_EINHEIT_ID = OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID; + public static final String SCHEME_AGENCY_ID = UUID.randomUUID().toString(); + + public static GrpcXzufiId create() { + return createBuilder().build(); + } + + public static GrpcXzufiId.Builder createBuilder() { + return GrpcXzufiId.newBuilder() + .setId(ORGANISATIONS_EINHEIT_ID) + .setSchemeAgencyId(SCHEME_AGENCY_ID); + } +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitControllerTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a913eb55dcfaeeb4c89da5a8085b04a3bef75e22 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitControllerTest.java @@ -0,0 +1,179 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import lombok.SneakyThrows; + +class OrganisationsEinheitControllerTest { + + @InjectMocks + private OrganisationsEinheitController controller; + + @Mock + private OrganisationsEinheitService organisationsEinheitService; + + @Mock + private OrganisationsEinheitModelAssembler assembler; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Nested + class TestGetAll { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + private final List<OrganisationsEinheit> organisationsEinheiten = List.of(organisationsEinheit); + private final CollectionModel<EntityModel<OrganisationsEinheit>> collectionModel = CollectionModel.of( + List.of(EntityModel.of(organisationsEinheit))); + + @BeforeEach + void setUp() { + when(organisationsEinheitService.getOrganisationsEinheiten()).thenReturn(organisationsEinheiten); + when(assembler.toCollectionModel(organisationsEinheiten)).thenReturn(collectionModel); + } + + @Test + void shouldCallService() { + doRequest(); + + verify(organisationsEinheitService).getOrganisationsEinheiten(); + } + + @Test + void shouldCallAssembler() { + doRequest(); + + verify(assembler).toCollectionModel(organisationsEinheiten); + } + + @Test + void shouldReturnCollectionModel() { + var response = controller.getAll(); + + assertThat(response).isEqualTo(collectionModel); + } + + @SneakyThrows + @Test + void shouldReturnHttpOk() { + doRequest().andExpect(status().isOk()); + } + + @SneakyThrows + private ResultActions doRequest() { + return mockMvc.perform(get(OrganisationsEinheitController.PATH)); + } + } + + @Nested + class TestGetById { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + private final EntityModel<OrganisationsEinheit> entityModel = EntityModel.of(organisationsEinheit); + + @BeforeEach + void setUp() { + when(organisationsEinheitService.getOrganisationsEinheitById(OrganisationsEinheitTestFactory.ID)).thenReturn(organisationsEinheit); + when(assembler.toModel(organisationsEinheit)).thenReturn(entityModel); + } + + @Test + void shouldCallService() { + doRequest(); + + verify(organisationsEinheitService).getOrganisationsEinheitById(OrganisationsEinheitTestFactory.ID); + } + + @Test + void shouldCallAssembler() { + doRequest(); + + verify(assembler).toModel(organisationsEinheit); + } + + @SneakyThrows + @Test + void shouldReturnHttpOk() { + doRequest().andExpect(status().isOk()); + } + + @Test + void shouldReturnEntityModel() { + var response = controller.getById(OrganisationsEinheitTestFactory.ID); + + assertThat(response).isEqualTo(entityModel); + } + + @SneakyThrows + private ResultActions doRequest() { + return mockMvc.perform(get(OrganisationsEinheitController.PATH + "/" + OrganisationsEinheitTestFactory.ID)); + } + } + + @Nested + class TestGetChildren { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + private final List<OrganisationsEinheit> children = List.of(organisationsEinheit); + private final CollectionModel<EntityModel<OrganisationsEinheit>> collectionModel = CollectionModel.of( + List.of(EntityModel.of(organisationsEinheit))); + + @BeforeEach + void setUp() { + when(organisationsEinheitService.getChildren(OrganisationsEinheitTestFactory.ID)).thenReturn(children); + when(assembler.toChildrenCollectionModel(OrganisationsEinheitTestFactory.ID, children)).thenReturn(collectionModel); + } + + @Test + void shouldCallService() { + doRequest(); + + verify(organisationsEinheitService).getChildren(OrganisationsEinheitTestFactory.ID); + } + + @SneakyThrows + @Test + void shouldReturnHttpOk() { + doRequest().andExpect(status().isOk()); + } + + @Test + void shouldCallAssember() { + doRequest(); + + verify(assembler).toChildrenCollectionModel(OrganisationsEinheitTestFactory.ID, children); + } + + @Test + void shouldReturnCollectionModel() { + var response = controller.getChildren(OrganisationsEinheitTestFactory.ID); + + assertThat(response).isEqualTo(collectionModel); + } + + @SneakyThrows + private ResultActions doRequest() { + return mockMvc.perform(get(OrganisationsEinheitController.PATH + "/" + OrganisationsEinheitTestFactory.ID + "/children")); + } + } +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitITCase.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..43103841feb481d6b69ead078fe02e1b003dde32 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitITCase.java @@ -0,0 +1,108 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import de.ozgcloud.common.test.ITCase; +import lombok.SneakyThrows; + +@ITCase +@WithMockUser +@AutoConfigureMockMvc +@SpringBootTest(properties = { + "ozgcloud.keycloak.api.url:test", + "ozgcloud.keycloak.api.user:test", + "ozgcloud.keycloak.api.password:test", + "ozgcloud.keycloak.api.realm:test", + "ozgcloud.keycloak.api.client:test", + "ozgcloud.keycloak.api.organisationsEinheitIdKey:test" }) +class OrganisationsEinheitITCase { + + private final String PATH = OrganisationsEinheitController.PATH; + + @MockBean + private OrganisationsEinheitService service; + @MockBean + private JwtDecoder decoder; + + @Autowired + private MockMvc mockMvc; + + @DisplayName("Get all") + @WithMockUser + @Nested + class TestGetAll { + + @BeforeEach + void init() { + when(service.getOrganisationsEinheiten()).thenReturn(List.of(OrganisationsEinheitTestFactory.create())); + } + + @SneakyThrows + @Test + void shouldContainList() { + var response = mockMvc.perform(get(PATH)).andExpect(status().isOk()); + + response.andDo(print()).andExpect(jsonPath("$._embedded.organisationsEinheitList").isNotEmpty()); + } + + @SneakyThrows + @Test + void shouldContainOrganisationsEinheit() { + mockMvc.perform(get(PATH)) + .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].name").value(OrganisationsEinheitTestFactory.NAME)) + .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].organisationsEinheitId").value( + OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID)) + .andExpect( + jsonPath("$._embedded.organisationsEinheitList[0].syncResult").value(OrganisationsEinheitTestFactory.SYNC_RESULT.name())) + .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].zufiId").doesNotExist()) + .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].parentId").doesNotExist()) + .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].keycloakId").doesNotExist()) + .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].id").doesNotExist()); + } + } + + @DisplayName("Get by id") + @WithMockUser + @Nested + class TestGetById { + + @BeforeEach + void init() { + when(service.getOrganisationsEinheitById(OrganisationsEinheitTestFactory.ID)).thenReturn(OrganisationsEinheitTestFactory.create()); + } + + @SneakyThrows + @Test + void shouldReturnOrganisationsEinheit() { + var response = mockMvc.perform(get(PATH + "/" + OrganisationsEinheitTestFactory.ID)).andExpect(status().isOk()); + + response + .andExpect(jsonPath("$.name").value(OrganisationsEinheitTestFactory.NAME)) + .andExpect(jsonPath("$.organisationsEinheitId").value( + OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID)) + .andExpect(jsonPath("$.syncResult").value(OrganisationsEinheitTestFactory.SYNC_RESULT.name())) + .andExpect(jsonPath("$.zufiId").doesNotExist()) + .andExpect(jsonPath("$.parentId").doesNotExist()) + .andExpect(jsonPath("$.keycloakId").doesNotExist()) + .andExpect(jsonPath("$.id").doesNotExist()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitMapperTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..53ec27998c68c86ce3dcdb7b91a1a8ba77fb8c20 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitMapperTest.java @@ -0,0 +1,61 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collections; +import java.util.Objects; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +import de.ozgcloud.admin.keycloak.AddGroupData; + +class OrganisationsEinheitMapperTest { + + private final OrganisationsEinheitMapper mapper = Mappers.getMapper(OrganisationsEinheitMapper.class); + + @Nested + class TestFromGrpc { + + @Test + void shouldMap() { + var mapped = mapper.fromGrpc(GrpcOrganisationsEinheitTestFactory.create()); + + assertThat(mapped) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getChildren + ).containsExactly( + OrganisationsEinheitTestFactory.NAME, + OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID, + OrganisationsEinheitTestFactory.ZUFI_ID, + null, + null, + Collections.emptyList() + ); + } + } + + @Nested + class TestToAddGroupData { + + @Test + void shouldMap() { + var mapped = mapper.toAddGroupData(OrganisationsEinheitTestFactory.create()); + + assertThat(mapped).extracting( + AddGroupData::getName, + AddGroupData::getOrganisationsEinheitId + ).containsExactly( + OrganisationsEinheitTestFactory.NAME, + OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID + ); + } + } +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitModelAssemblerTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitModelAssemblerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c726c6b5219bdadedb46e336c2baa9fed5edee5f --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitModelAssemblerTest.java @@ -0,0 +1,194 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static de.ozgcloud.admin.organisationseinheit.OrganisationsEinheitModelAssembler.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkRelation; +import org.springframework.hateoas.mediatype.hal.HalModelBuilder; +import org.springframework.web.util.UriTemplate; + +class OrganisationsEinheitModelAssemblerTest { + + @Spy + @InjectMocks + private OrganisationsEinheitModelAssembler assembler; + + @Nested + class TestToModel { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().parentId(null).build(); + + @BeforeEach + void setUp() { + doNothing().when(assembler).embedChildOrganisationsEinheiten(eq(organisationsEinheit), any()); + } + + @Test + void shouldAddSelfLink() { + var model = assembler.toModel(organisationsEinheit); + + assertThat(model.getLink(IanaLinkRelations.SELF)) + .isNotEmpty() + .get() + .extracting(Link::getHref) + .isEqualTo(new UriTemplate(OrganisationsEinheitController.PATH + "/{id}").expand(OrganisationsEinheitTestFactory.ID).toString()); + } + + @Test + void shouldReturnModel() { + var model = assembler.toModel(organisationsEinheit); + + assertThat(model.getContent()).isEqualTo(organisationsEinheit); + } + + @Test + void shouldEmbedChildOrganisationsEinheiten() { + assembler.toModel(organisationsEinheit); + + verify(assembler).embedChildOrganisationsEinheiten(eq(organisationsEinheit), any()); + } + } + + @Nested + class TestEmbedChildOrganisationsEinheiten { + + private OrganisationsEinheit parentOrganisationsEinheit; + private OrganisationsEinheit childOrganisationsEinheit; + private CollectionModel<EntityModel<OrganisationsEinheit>> childrenCollectionModel; + @Spy + private final HalModelBuilder halModelBuilder = HalModelBuilder.emptyHalModel(); + @Captor + private ArgumentCaptor<LinkRelation> linkRelationArgumentCaptor; + @Captor + private ArgumentCaptor<Collection<EntityModel<OrganisationsEinheit>>> collectionModelContentArgumentCaptor; + + @BeforeEach + void setUp() { + parentOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().parentId(null).build(); + childOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().id(UUID.randomUUID().toString()) + .parentId(parentOrganisationsEinheit.getId()).build(); + parentOrganisationsEinheit = parentOrganisationsEinheit.toBuilder().child(childOrganisationsEinheit).build(); + childrenCollectionModel = CollectionModel.of(List.of(EntityModel.of(childOrganisationsEinheit))); + doReturn(childrenCollectionModel).when(assembler) + .toChildrenCollectionModel(parentOrganisationsEinheit.getId(), parentOrganisationsEinheit.getChildren()); + } + + @Test + void shouldBuildChildrenCollectionModel() { + assembler.embedChildOrganisationsEinheiten(parentOrganisationsEinheit, halModelBuilder); + + verify(assembler).toChildrenCollectionModel(parentOrganisationsEinheit.getId(), parentOrganisationsEinheit.getChildren()); + } + + @Test + void shouldEmbedChildrenModels() { + assembler.embedChildOrganisationsEinheiten(parentOrganisationsEinheit, halModelBuilder); + + verify(halModelBuilder).embed(collectionModelContentArgumentCaptor.capture(), linkRelationArgumentCaptor.capture()); + assertThat(collectionModelContentArgumentCaptor.getValue()).usingRecursiveComparison().isEqualTo(childrenCollectionModel.getContent()); + assertThat(linkRelationArgumentCaptor.getValue().toString()).isEqualTo(REL_CHILD_ORGANISATIONS_EINHEITEN); + } + + @Test + void shouldHaveChildListLinkRelation() { + assembler.embedChildOrganisationsEinheiten(parentOrganisationsEinheit, halModelBuilder); + + assertThat(halModelBuilder.build().getLink(REL_CHILD_ORGANISATIONS_EINHEITEN)) + .isNotEmpty() + .get() + .extracting(Link::getHref) + .isEqualTo(OrganisationsEinheitController.PATH + "/" + parentOrganisationsEinheit.getId() + "/children"); + } + } + + @Nested + class TestToCollectionModel { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + + @BeforeEach + void setUp() { + doReturn(EntityModel.of(organisationsEinheit)).when(assembler).toModel(organisationsEinheit); + } + + @Test + void shouldAddSelfLink() { + var collectionModel = assembler.toCollectionModel(List.of(organisationsEinheit)); + + assertThat(collectionModel.getLink(IanaLinkRelations.SELF)) + .isNotEmpty() + .get() + .extracting(Link::getHref) + .isEqualTo(OrganisationsEinheitController.PATH); + } + + @Test + void shouldBuildModels() { + assembler.toCollectionModel(List.of(organisationsEinheit)); + + verify(assembler).toModel(organisationsEinheit); + } + + @Test + void shouldHaveModels() { + var collectionModel = assembler.toCollectionModel(List.of(organisationsEinheit)); + + assertThat(collectionModel.getContent()).extracting(EntityModel::getContent).containsExactly(organisationsEinheit); + } + } + + @Nested + class ToChildrenCollectionModel { + + private final OrganisationsEinheit parentOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().parentId(null).build(); + private final OrganisationsEinheit childOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()).build(); + + @BeforeEach + void setUp() { + doReturn(EntityModel.of(childOrganisationsEinheit)).when(assembler).toModel(childOrganisationsEinheit); + } + + @Test + void shouldAddSelfLink() { + var collectionModel = assembler.toChildrenCollectionModel(parentOrganisationsEinheit.getId(), List.of(childOrganisationsEinheit)); + + assertThat(collectionModel.getLink(IanaLinkRelations.SELF)) + .isNotEmpty() + .get() + .extracting(Link::getHref) + .isEqualTo(OrganisationsEinheitController.PATH + "/" + parentOrganisationsEinheit.getId() + "/children"); + } + + @Test + void shouldBuildModels() { + assembler.toChildrenCollectionModel(parentOrganisationsEinheit.getId(), List.of(childOrganisationsEinheit)); + + verify(assembler).toModel(childOrganisationsEinheit); + } + + @Test + void shouldHaveModels() { + var collectionModel = assembler.toChildrenCollectionModel(parentOrganisationsEinheit.getId(), List.of(childOrganisationsEinheit)); + + assertThat(collectionModel.getContent()).extracting(EntityModel::getContent).containsExactly(childOrganisationsEinheit); + } + + } +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRemoteServiceTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRemoteServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fde08bcea821a504734046bb2d16024dbd1bb056 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRemoteServiceTest.java @@ -0,0 +1,84 @@ +package de.ozgcloud.admin.organisationseinheit; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcGetByOrganisationsEinheitIdRequest; +import de.ozgcloud.zufi.grpc.organisationseinheit.GrpcGetByOrganisationsEinheitIdResponse; +import de.ozgcloud.zufi.grpc.organisationseinheit.OrganisationsEinheitServiceGrpc.OrganisationsEinheitServiceBlockingStub; + +class OrganisationsEinheitRemoteServiceTest { + + @Spy + @InjectMocks + private OrganisationsEinheitRemoteService service; + + @Mock + private OrganisationsEinheitServiceBlockingStub serviceStub; + + @Mock + private OrganisationsEinheitMapper mapper; + + @Nested + class TestGetByOrganisationsEinheitId { + + private final GrpcGetByOrganisationsEinheitIdRequest request = GrpcGetByOrganisationsEinheitIdRequestTestFactory.create(); + private final GrpcGetByOrganisationsEinheitIdResponse response = GrpcGetByOrganisationsEinheitIdResponseTestFactory.create(); + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + + @BeforeEach + void setUp() { + doReturn(request).when(service).buildGetByOrganisationsEinheitIdRequest(OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID); + when(serviceStub.getByOrganisationsEinheitId(request)).thenReturn(response); + when(mapper.fromGrpc(GrpcGetByOrganisationsEinheitIdResponseTestFactory.GRPC_ORGANISATIONS_EINHEIT)).thenReturn(organisationsEinheit); + } + + @Test + void shouldBuildRequest() { + service.getByOrganisationsEinheitId(OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID); + + verify(service).buildGetByOrganisationsEinheitIdRequest(OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID); + } + + @Test + void shouldCallServiceStub() { + service.getByOrganisationsEinheitId(OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID); + + verify(serviceStub).getByOrganisationsEinheitId(request); + } + + @Test + void shouldMapResponse() { + service.getByOrganisationsEinheitId(OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID); + + verify(mapper).fromGrpc(GrpcGetByOrganisationsEinheitIdResponseTestFactory.GRPC_ORGANISATIONS_EINHEIT); + } + + @Test + void shouldReturnOrganisationsEinheiten() { + var organisationsEinheiten = service.getByOrganisationsEinheitId(OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID); + + assertThat(organisationsEinheiten).containsExactly(organisationsEinheit); + + } + } + + @Nested + class TestBuildGetByOrganisationsEinheitIdRequest { + + @Test + void shouldBuild() { + var request = service.buildGetByOrganisationsEinheitIdRequest(OrganisationsEinheitTestFactory.ID); + + assertThat(request.getOrganisationsEinheitId()).isEqualTo(OrganisationsEinheitTestFactory.ID); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRepositoryITCase.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRepositoryITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..dcfeebde1d6aa5bfb83944ec11c110f72dd833a8 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRepositoryITCase.java @@ -0,0 +1,290 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Instant; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import de.ozgcloud.common.test.DbInitializer; + +@DataMongoTest +@ContextConfiguration(initializers = { DbInitializer.class }, classes = { OrganisationsEinheitRepository.class, MongoOperations.class }) +@ActiveProfiles({ "itcase", "with_db" }) +@EnableAutoConfiguration +class OrganisationsEinheitRepositoryITCase { + + @Autowired + private OrganisationsEinheitRepository repository; + + @Autowired + private MongoOperations operations; + + @BeforeEach + void clearDatabase() { + operations.dropCollection(OrganisationsEinheit.class); + } + + @Nested + class TestFindAllNotDeleted { + + private static final String DELETED_ORGANISATIONS_EINHEIT_ID_1 = "1"; + private static final String DELETED_ORGANISATIONS_EINHEIT_ID_2 = "2"; + private static final String SYNCED_ORGANISATIONS_EINHEIT_ID_1 = "3"; + private static final String SYNCED_ORGANISATIONS_EINHEIT_ID_2 = "4"; + private static final String ADDED_FROM_ZUFI_ORGANISATIONS_EINHEIT_ID_1 = "5"; + + private final OrganisationsEinheit deleted1 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(DELETED_ORGANISATIONS_EINHEIT_ID_1) + .syncResult(SyncResult.DELETED) + .build(); + private final OrganisationsEinheit deleted2 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(DELETED_ORGANISATIONS_EINHEIT_ID_2) + .syncResult(SyncResult.DELETED) + .build(); + private final OrganisationsEinheit synced1 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(SYNCED_ORGANISATIONS_EINHEIT_ID_1) + .build(); + private final OrganisationsEinheit synced2 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(SYNCED_ORGANISATIONS_EINHEIT_ID_2) + .build(); + private final OrganisationsEinheit addedFromZufi = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .syncResult(null) + .organisationsEinheitId(ADDED_FROM_ZUFI_ORGANISATIONS_EINHEIT_ID_1) + .build(); + + @BeforeEach + void setUp() { + operations.save(deleted1); + operations.save(deleted2); + operations.save(synced1); + operations.save(synced2); + operations.save(addedFromZufi); + } + + @Test + void shouldReturnNotDeleted() { + var allNotDeleted = repository.findAllNotDeleted(); + + assertThat(allNotDeleted).extracting( + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult + ).containsExactly( + tuple(SYNCED_ORGANISATIONS_EINHEIT_ID_1, OrganisationsEinheitTestFactory.SYNC_RESULT), + tuple(SYNCED_ORGANISATIONS_EINHEIT_ID_2, OrganisationsEinheitTestFactory.SYNC_RESULT), + tuple(ADDED_FROM_ZUFI_ORGANISATIONS_EINHEIT_ID_1, null) + ); + } + } + + @Nested + class TestFindSyncedByKeycloakId { + + @Test + void shouldReturnEmptyOnEmptyDatabase() { + var organisationsEinheit = repository.findSyncedByKeycloakId(OrganisationsEinheitTestFactory.KEYCLOAK_ID); + + assertThat(organisationsEinheit).isEmpty(); + } + + @Test + void shouldReturnEmptyOnNotFoundKeycloakId() { + operations.save(OrganisationsEinheitTestFactory.createBuilder().id(null).build()); + + var organisationsEinheit = repository.findSyncedByKeycloakId("not_found"); + + assertThat(organisationsEinheit).isEmpty(); + } + + @Test + void shouldReturnEmptyOnSyncResultNull() { + operations.save(OrganisationsEinheitTestFactory.createBuilder().id(null).syncResult(null).build()); + + var organisationsEinheit = repository.findSyncedByKeycloakId(OrganisationsEinheitTestFactory.KEYCLOAK_ID); + + assertThat(organisationsEinheit).isEmpty(); + } + + @Test + void shouldFind() { + operations.save(OrganisationsEinheitTestFactory.createBuilder().id(null).build()); + operations.save(OrganisationsEinheitTestFactory.createBuilder().id(null).keycloakId(UUID.randomUUID().toString()).build()); + + var organisationsEinheit = repository.findSyncedByKeycloakId(OrganisationsEinheitTestFactory.KEYCLOAK_ID); + + assertThat(organisationsEinheit) + .isNotEmpty() + .get() + .extracting(OrganisationsEinheit::getKeycloakId) + .isEqualTo(OrganisationsEinheitTestFactory.KEYCLOAK_ID); + } + } + + @Nested + class TestSetUnsyncedAsDeleted { + + private static final String DELETED_ORGANISATIONS_EINHEIT_ID_1 = "1"; + private static final String DELETED_ORGANISATIONS_EINHEIT_ID_2 = "2"; + private static final String SYNCED_ORGANISATIONS_EINHEIT_ID_1 = "3"; + private static final String SYNCED_ORGANISATIONS_EINHEIT_ID_2 = "4"; + + private final OrganisationsEinheit deleted1 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(DELETED_ORGANISATIONS_EINHEIT_ID_1) + .lastSyncTimestamp(OrganisationsEinheitTestFactory.LAST_SYNC_UPDATE - 1) + .build(); + private final OrganisationsEinheit deleted2 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(DELETED_ORGANISATIONS_EINHEIT_ID_2) + .lastSyncTimestamp(OrganisationsEinheitTestFactory.LAST_SYNC_UPDATE - 2) + .build(); + private final OrganisationsEinheit synced1 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(SYNCED_ORGANISATIONS_EINHEIT_ID_1) + .build(); + private final OrganisationsEinheit synced2 = OrganisationsEinheitTestFactory.createBuilder() + .id(null) + .organisationsEinheitId(SYNCED_ORGANISATIONS_EINHEIT_ID_2) + .build(); + + @BeforeEach + void setUp() { + operations.save(deleted1); + operations.save(deleted2); + operations.save(synced1); + operations.save(synced2); + } + + @Test + void shouldSetUnsyncedAsDeleted() { + repository.setUnsyncedAsDeleted(OrganisationsEinheitTestFactory.LAST_SYNC_UPDATE); + + var all = repository.findAll(); + + assertThat(all).extracting( + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult + ).containsExactly( + tuple(DELETED_ORGANISATIONS_EINHEIT_ID_1, SyncResult.DELETED), + tuple(DELETED_ORGANISATIONS_EINHEIT_ID_2, SyncResult.DELETED), + tuple(SYNCED_ORGANISATIONS_EINHEIT_ID_1, OrganisationsEinheitTestFactory.SYNC_RESULT), + tuple(SYNCED_ORGANISATIONS_EINHEIT_ID_2, OrganisationsEinheitTestFactory.SYNC_RESULT) + ); + } + + @Test + void shouldSetAllAsDeleted() { + repository.setUnsyncedAsDeleted(Instant.now().toEpochMilli()); + + var all = repository.findAll(); + + assertThat(all).extracting( + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult + ).containsExactly( + tuple(DELETED_ORGANISATIONS_EINHEIT_ID_1, SyncResult.DELETED), + tuple(DELETED_ORGANISATIONS_EINHEIT_ID_2, SyncResult.DELETED), + tuple(SYNCED_ORGANISATIONS_EINHEIT_ID_1, SyncResult.DELETED), + tuple(SYNCED_ORGANISATIONS_EINHEIT_ID_2, SyncResult.DELETED) + ); + } + } + + @Nested + class TestFindAllWithoutSyncResult { + + private static final String SYNCED_ID_1 = "1"; + private static final String WITHOUT_SYNC_RESULT_ID_1 = "2"; + private static final String WITHOUT_SYNC_RESULT_ID_2 = "3"; + + private final OrganisationsEinheit synced1 = OrganisationsEinheitTestFactory.createBuilder() + .id(SYNCED_ID_1) + .build(); + private final OrganisationsEinheit withoutSyncResult1 = OrganisationsEinheitTestFactory.createBuilder() + .id(WITHOUT_SYNC_RESULT_ID_1) + .syncResult(null) + .build(); + private final OrganisationsEinheit withoutSyncResult2 = OrganisationsEinheitTestFactory.createBuilder() + .id(WITHOUT_SYNC_RESULT_ID_2) + .parentId(SYNCED_ID_1) + .syncResult(null) + .build(); + + @BeforeEach + void setUp() { + operations.save(synced1); + operations.save(withoutSyncResult1); + operations.save(withoutSyncResult2); + } + + @Test + void shouldFindAllWithoutSyncResult() { + var allWithoutSyncResult = repository.findAllWithoutSyncResult(); + + assertThat(allWithoutSyncResult).extracting(OrganisationsEinheit::getId) + .containsExactlyInAnyOrder(WITHOUT_SYNC_RESULT_ID_1, WITHOUT_SYNC_RESULT_ID_2); + } + } + + @Nested + class TestFindChildren { + + private final OrganisationsEinheit root1 = OrganisationsEinheitTestFactory.createBuilder().parentId(null).build(); + private final OrganisationsEinheit root2 = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()) + .parentId(null) + .build(); + private final OrganisationsEinheit child1 = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()) + .parentId(root1.getId()) + .build(); + private final OrganisationsEinheit child2 = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()) + .parentId(root1.getId()) + .build(); + + private final OrganisationsEinheit child3 = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()) + .parentId(root2.getId()) + .build(); + + @BeforeEach + void setUp() { + operations.save(root1); + operations.save(root2); + operations.save(child1); + operations.save(child2); + operations.save(child3); + } + + @Test + void shouldFind() { + var found = repository.findChildren(root1.getId()); + + assertThat(found).extracting(OrganisationsEinheit::getId).containsOnly(child1.getId(), child2.getId()); + } + + @Test + void shouldNotFind() { + var found = repository.findChildren(UUID.randomUUID().toString()); + + assertThat(found).isEmpty(); + + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessorTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..85e0308c11706d45e11369ae26a7ea634b8fc87a --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitRootProcessorTest.java @@ -0,0 +1,44 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; + +import de.ozgcloud.admin.RootTestFactory; + +class OrganisationsEinheitRootProcessorTest { + + @InjectMocks + private OrganisationsEinheitRootProcessor organisationsEinheitRootProcessor; + + @Nested + class TestProcess { + + @Nested + class OrganisationsEinheitenLinkRelation { + + @Test + void shouldExists() { + var model = organisationsEinheitRootProcessor.process(EntityModel.of(RootTestFactory.create())); + + assertThat(model.getLink(OrganisationsEinheitRootProcessor.REL_ORGANISATIONS_EINHEITEN)).isNotEmpty(); + } + + @Test + void shouldHaveHref() { + var model = organisationsEinheitRootProcessor.process(EntityModel.of(RootTestFactory.create())); + + assertThat(model.getLink(OrganisationsEinheitRootProcessor.REL_ORGANISATIONS_EINHEITEN)) + .get() + .extracting(Link::getHref) + .isEqualTo(OrganisationsEinheitController.PATH); + } + } + + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitServiceTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e151636ee95ef52d2dab645c0e19ad5b134f1114 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitServiceTest.java @@ -0,0 +1,161 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; + +class OrganisationsEinheitServiceTest { + + @Spy + @InjectMocks + private OrganisationsEinheitService service; + + @Mock + private OrganisationsEinheitRepository repository; + + @Nested + class TestSaveOrganisationsEinheit { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + + @BeforeEach + void setUp() { + when(repository.save(organisationsEinheit)).thenReturn(organisationsEinheit); + } + + @Test + void shouldCallRepository() { + service.saveOrganisationsEinheit(organisationsEinheit); + + verify(repository).save(organisationsEinheit); + } + + @Test + void shouldReturnSavedOrganisationsEinheit() { + var saved = service.saveOrganisationsEinheit(organisationsEinheit); + + assertThat(saved).isEqualTo(organisationsEinheit); + } + } + + @Nested + class TestGetOrganisationsEinheiten { + + private final OrganisationsEinheit rootOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().parentId(null).build(); + private final OrganisationsEinheit childOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()).parentId(rootOrganisationsEinheit.getId()).build(); + private final List<OrganisationsEinheit> organisationsEinheiten = List.of(rootOrganisationsEinheit, childOrganisationsEinheit); + + @BeforeEach + void setUp() { + when(repository.findAllNotDeleted()).thenReturn(organisationsEinheiten); + doReturn(rootOrganisationsEinheit).when(service).addChildren(rootOrganisationsEinheit, organisationsEinheiten); + } + + @Test + void shouldCallRepository() { + service.getOrganisationsEinheiten(); + + verify(repository).findAllNotDeleted(); + } + + @Test + void shouldAddChildren() { + service.getOrganisationsEinheiten(); + + verify(service).addChildren(rootOrganisationsEinheit, organisationsEinheiten); + } + + @Test + void shouldReturnAll() { + var gotOrganisationsEinheiten = service.getOrganisationsEinheiten(); + + assertThat(gotOrganisationsEinheiten).containsExactly(rootOrganisationsEinheit); + } + } + + @Nested + class TestGetOrganisationsEinheitById { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + + @Test + void shouldCallRepository() { + when(repository.findById(OrganisationsEinheitTestFactory.ID)).thenReturn(Optional.of(organisationsEinheit)); + + service.getOrganisationsEinheitById(OrganisationsEinheitTestFactory.ID); + + verify(repository).findById(OrganisationsEinheitTestFactory.ID); + } + + @Test + void shouldReturnOrganisationsEinheit() { + when(repository.findById(OrganisationsEinheitTestFactory.ID)).thenReturn(Optional.of(organisationsEinheit)); + + var gotOrganisationsEinheit = service.getOrganisationsEinheitById(OrganisationsEinheitTestFactory.ID); + + assertThat(gotOrganisationsEinheit).isEqualTo(organisationsEinheit); + } + + @Test + void shouldThrowResourceNotFoundException() { + when(repository.findById("not_exists")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> service.getOrganisationsEinheitById("not_exists")) + .isInstanceOf(ResourceNotFoundException.class) + .hasMessage("Organisationseinheit with id not_exists not found"); + } + } + + @Nested + class TestAddChildren { + + private final OrganisationsEinheit rootOrganisationsEinheit = OrganisationsEinheitTestFactory.create(); + private final OrganisationsEinheit childOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()).parentId(rootOrganisationsEinheit.getId()).build(); + private final List<OrganisationsEinheit> organisationsEinheiten = List.of(rootOrganisationsEinheit, childOrganisationsEinheit); + + @Test + void shouldAddChildren() { + var organisationsEinheit = service.addChildren(rootOrganisationsEinheit, organisationsEinheiten); + + assertThat(organisationsEinheit.getChildren()).containsExactly(childOrganisationsEinheit); + } + } + + @Nested + class TestGetChildren { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + + @BeforeEach + void setUp() { + when(repository.findChildren(OrganisationsEinheitTestFactory.ID)).thenReturn(List.of(organisationsEinheit)); + } + + @Test + void shouldCallRepository() { + service.getChildren(OrganisationsEinheitTestFactory.ID); + + verify(repository).findChildren(OrganisationsEinheitTestFactory.ID); + } + + @Test + void shouldReturnChildren() { + var children = service.getChildren(OrganisationsEinheitTestFactory.ID); + + assertThat(children).containsExactly(organisationsEinheit); + } + } +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSettingsTestFactory.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSettingsTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..c5028fb0e25bb8b6127e623a4e09b75794545d61 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitSettingsTestFactory.java @@ -0,0 +1,23 @@ +package de.ozgcloud.admin.organisationseinheit; + +import com.thedeanda.lorem.LoremIpsum; + +public class OrganisationsEinheitSettingsTestFactory { + + public static final String SIGNATUR = LoremIpsum.getInstance().getWords(4); + + public static OrganisationsEinheitSettings create() { + return createBuilder().build(); + } + + public static OrganisationsEinheitSettings.OrganisationsEinheitSettingsBuilder createBuilder() { + return OrganisationsEinheitSettings.builder() + .signatur(SIGNATUR); + } + + public static OrganisationsEinheitSettings.OrganisationsEinheitSettingsBuilder createNewBuilder() { + return OrganisationsEinheitSettings.builder() + .signatur(LoremIpsum.getInstance().getWords(3)); + } + +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitTestFactory.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..756ec38fc6a01660b352924cebbd24176c4a9939 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitTestFactory.java @@ -0,0 +1,49 @@ +package de.ozgcloud.admin.organisationseinheit; + +import java.time.Instant; +import java.util.UUID; + +import com.thedeanda.lorem.LoremIpsum; + +public class OrganisationsEinheitTestFactory { + + public static final String ID = UUID.randomUUID().toString(); + public static final String KEYCLOAK_ID = UUID.randomUUID().toString(); + public static final String PARENT_ID = UUID.randomUUID().toString(); + public static final String NAME = LoremIpsum.getInstance().getName(); + public static final String ORGANISATIONS_EINHEIT_ID = UUID.randomUUID().toString(); + public static final String ZUFI_ID = UUID.randomUUID().toString(); + public static final SyncResult SYNC_RESULT = SyncResult.OK; + public static final long LAST_SYNC_UPDATE = Instant.now().toEpochMilli(); + public static final OrganisationsEinheitSettings SETTINGS = OrganisationsEinheitSettingsTestFactory.create(); + + public static OrganisationsEinheit create() { + return createBuilder().build(); + } + + public static OrganisationsEinheit.OrganisationsEinheitBuilder createBuilder() { + return OrganisationsEinheit.builder() + .id(ID) + .keycloakId(KEYCLOAK_ID) + .name(NAME) + .organisationsEinheitId(ORGANISATIONS_EINHEIT_ID) + .parentId(PARENT_ID) + .zufiId(ZUFI_ID) + .syncResult(SYNC_RESULT) + .settings(SETTINGS) + .lastSyncTimestamp(LAST_SYNC_UPDATE); + } + + public static OrganisationsEinheit.OrganisationsEinheitBuilder createNewBuilder() { + return OrganisationsEinheit.builder() + .id(UUID.randomUUID().toString()) + .keycloakId(UUID.randomUUID().toString()) + .name(LoremIpsum.getInstance().getName()) + .organisationsEinheitId(UUID.randomUUID().toString()) + .parentId(UUID.randomUUID().toString()) + .zufiId(UUID.randomUUID().toString()) + .syncResult(SyncResult.DELETED) + .settings(OrganisationsEinheitSettingsTestFactory.createNewBuilder().build()) + .lastSyncTimestamp(LAST_SYNC_UPDATE - 10000); + } +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncAddedOrganisationsEinheitenITCase.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncAddedOrganisationsEinheitenITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..1af0cf1617c485ebb64936aba8e653cdafa83ed1 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncAddedOrganisationsEinheitenITCase.java @@ -0,0 +1,62 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.groups.Tuple.tuple; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.test.context.ContextConfiguration; + +import de.ozgcloud.admin.common.KeycloakInitializer; +import de.ozgcloud.admin.keycloak.Group; +import de.ozgcloud.admin.keycloak.KeycloakRemoteService; +import de.ozgcloud.common.test.DbInitializer; +import de.ozgcloud.common.test.ITCase; + +@ITCase +@ContextConfiguration(initializers = { DbInitializer.class, KeycloakInitializer.class }) +class SyncAddedOrganisationsEinheitenITCase { + + @Autowired + private SyncService service; + @SpyBean + private KeycloakRemoteService keycloakRemoteService; + @Autowired + private MongoOperations operations; + + private final long syncTimestamp = Instant.now().toEpochMilli(); + + @BeforeEach + void clearDatabase() { + operations.dropCollection(OrganisationsEinheit.class); + } + + @Test + void shouldSynchronizeAddedTopLevelGroup() { + var topLevel = topLevel("shouldSynchronizeAddedTopLevelGroup"); + operations.save(topLevel); + + service.syncAddedOrganisationsEinheiten(syncTimestamp); + + assertThat(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()) + .extracting(Group::getName, Group::getOrganisationsEinheitId, Group::getSubGroups) + .contains(tuple(topLevel.getName(), topLevel.getOrganisationsEinheitId(), List.of())); + } + + private static OrganisationsEinheit topLevel(String nameSuffix) { + return OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()) + .parentId(null) + .keycloakId(null) + .syncResult(null) + .name("topLevel (%s)".formatted(nameSuffix)) + .build(); + } +} diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncSchedulerTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncSchedulerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c78510c4c8c77dabfe5bf774382775c6959af0fa --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncSchedulerTest.java @@ -0,0 +1,55 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.Instant; + +import org.assertj.core.data.*; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +class SyncSchedulerTest { + + @InjectMocks + private SyncScheduler scheduler; + + @Mock + private SyncService syncService; + + @Nested + class TestSyncOrganisationsEinheitenWithKeycloak { + + @Captor + private ArgumentCaptor<Long> syncTimestampArgumentCaptor; + + @Test + void shouldSyncOrganisationsEinheitenFromKeycloak() { + callService(); + + verify(syncService).syncOrganisationsEinheitenFromKeycloak(syncTimestampArgumentCaptor.capture()); + assertThatSyncTimestampIsCloseToNow(); + } + + @Test + void shouldSyncAddedOrganisationsEinheiten() { + callService(); + + verify(syncService).syncAddedOrganisationsEinheiten(syncTimestampArgumentCaptor.capture()); + assertThatSyncTimestampIsCloseToNow(); + } + + private void assertThatSyncTimestampIsCloseToNow() { + assertThat(syncTimestampArgumentCaptor.getValue()).isCloseTo(Instant.now().toEpochMilli(), Offset.offset(5000L)); + } + + private void callService() { + scheduler.syncOrganisationsEinheitenWithKeycloak(); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncServiceITCase.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncServiceITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..c681785b71ea74380ac446ca736527d131579284 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncServiceITCase.java @@ -0,0 +1,630 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.mongodb.core.MongoOperations; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.admin.keycloak.GroupTestFactory; +import de.ozgcloud.admin.keycloak.KeycloakRemoteService; +import de.ozgcloud.common.test.DataITCase; + +@DataITCase +@EnableAutoConfiguration +class SyncServiceITCase { + + @Autowired + private MongoOperations operations; + + @Autowired + private SyncService service; + + @Autowired + private OrganisationsEinheitRepository repository; + + @MockBean + private KeycloakRemoteService keycloakRemoteService; + + @MockBean + private OrganisationsEinheitRemoteService organisationsEinheitRemoteService; + + @BeforeEach + void clearDatabase() { + operations.dropCollection(OrganisationsEinheit.class); + } + + @DisplayName("Organisationseinheit not found in PVOG") + @Test + void shouldSyncNotFoundInPvog() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().clearSubGroups().build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn(List.of()); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var synced = repository.findAll(); + + assertThat(synced). + hasSize(1) + .first() + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.NOT_FOUND_IN_PVOG, + null, + null, + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Organisationseinheit found in PVOG but group name is different") + @Test + void shouldSyncNameMismatch() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().clearSubGroups().build(); + var pvogOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var synced = repository.findAll(); + + assertThat(synced). + hasSize(1) + .first() + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + pvogOranigastionsEinheit.getName(), + group.getOrganisationsEinheitId(), + SyncResult.NAME_MISMATCH, + pvogOranigastionsEinheit.getId(), + null, + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Multiple Organisationseinheiten for same group found in PVOG") + @Test + void shouldSyncOrganisationsEinheitNotUnique() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().clearSubGroups().build(); + var pvogOranigastionsEinheit1 = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .build(); + var pvogOranigastionsEinheit2 = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit1, pvogOranigastionsEinheit2)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var synced = repository.findAll(); + + assertThat(synced). + hasSize(1) + .first() + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.ORGANISATIONSEINHEIT_ID_NOT_UNIQUE, + null, + null, + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Organisationseinheit found in PVOG and have same data") + @Test + void shouldSyncOk() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().clearSubGroups().build(); + var pvogOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .name(group.getName()).build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var synced = repository.findAll(); + + assertThat(synced). + hasSize(1) + .first() + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.OK, + pvogOranigastionsEinheit.getId(), + null, + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("OrganisationseinheitId attribute of group changed") + @Test + void shouldSyncUpdateAttribute() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().clearSubGroups().build(); + var pvogOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .name(group.getName()).build(); + var newOrganisationsEinheitId = UUID.randomUUID().toString(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn( + Stream.of(group.toBuilder().organisationsEinheitId(newOrganisationsEinheitId).build())); + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp + 1000); + + var synced = repository.findAll(); + + assertThat(synced). + hasSize(1) + .first() + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + newOrganisationsEinheitId, + SyncResult.NOT_FOUND_IN_PVOG, + null, + null, + Collections.emptyList(), + syncTimestamp + 1000); + } + + @DisplayName("Group deleted in Keycloak") + @Test + void shouldSyncDeleted() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().clearSubGroups().build(); + var pvogOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .name(group.getName()).build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of()); + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp + 1000); + + var synced = repository.findAll(); + + assertThat(synced). + hasSize(1) + .first() + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.DELETED, + pvogOranigastionsEinheit.getId(), + null, + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Organisationseinheit for parent group and sub group not found in PVOG") + @Test + void shouldSyncParentNotFoundInPvogSubGroupNotFoundInPvog() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().build(); + var subGroup = group.getSubGroups().getFirst(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn(List.of()); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var parentSynced = repository.findSyncedByKeycloakId(group.getId()).get(); + var childSynced = repository.findSyncedByKeycloakId(subGroup.getId()).get(); + + assertThat(parentSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.NOT_FOUND_IN_PVOG, + null, + null, + Collections.emptyList(), + syncTimestamp); + assertThat(childSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + subGroup.getId(), + subGroup.getName(), + subGroup.getOrganisationsEinheitId(), + SyncResult.NOT_FOUND_IN_PVOG, + null, + parentSynced.getId(), + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Organisationseinheit for parent group and sub group found in PVOG but have different names") + @Test + void shouldSyncParentNameMismatchSubGroupNameMismatch() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().build(); + var subGroup = group.getSubGroups().getFirst(); + var pvogOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .build(); + var pvogChildOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder() + .organisationsEinheitId(subGroup.getOrganisationsEinheitId()).name( + LoremIpsum.getInstance().getWords(2)) + .build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogChildOranigastionsEinheit)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var parentSynced = repository.findSyncedByKeycloakId(group.getId()).get(); + var childSynced = repository.findSyncedByKeycloakId(subGroup.getId()).get(); + + assertThat(parentSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + pvogOranigastionsEinheit.getName(), + group.getOrganisationsEinheitId(), + SyncResult.NAME_MISMATCH, + pvogOranigastionsEinheit.getId(), + null, + Collections.emptyList(), + syncTimestamp); + assertThat(childSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + subGroup.getId(), + pvogChildOranigastionsEinheit.getName(), + subGroup.getOrganisationsEinheitId(), + SyncResult.NAME_MISMATCH, + pvogChildOranigastionsEinheit.getId(), + parentSynced.getId(), + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Multiple Organisationseinheiten for parent group and sub group found in PVOG") + @Test + void shouldSyncParentAndChildOrganisationsEinheitNotUnique() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().build(); + var subGroup = group.getSubGroups().getFirst(); + var pvogOranigastionsEinheit1 = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .build(); + var pvogOranigastionsEinheit2 = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .build(); + var pvogChildOranigastionsEinheit1 = OrganisationsEinheitTestFactory.createBuilder() + .organisationsEinheitId(subGroup.getOrganisationsEinheitId()).name( + LoremIpsum.getInstance().getWords(2)) + .build(); + var pvogChildOranigastionsEinheit2 = OrganisationsEinheitTestFactory.createBuilder() + .organisationsEinheitId(subGroup.getOrganisationsEinheitId()).name( + LoremIpsum.getInstance().getWords(2)) + .build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit1, pvogOranigastionsEinheit2)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogChildOranigastionsEinheit1, pvogChildOranigastionsEinheit2)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var parentSynced = repository.findSyncedByKeycloakId(group.getId()).get(); + var childSynced = repository.findSyncedByKeycloakId(subGroup.getId()).get(); + + assertThat(parentSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.ORGANISATIONSEINHEIT_ID_NOT_UNIQUE, + null, + null, + Collections.emptyList(), + syncTimestamp); + assertThat(childSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + subGroup.getId(), + subGroup.getName(), + subGroup.getOrganisationsEinheitId(), + SyncResult.ORGANISATIONSEINHEIT_ID_NOT_UNIQUE, + null, + parentSynced.getId(), + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Organisationseinheit and child Organisationseinheit found in PVOG and have same data") + @Test + void shouldSyncOkGroupWithSubGroup() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().build(); + var subGroup = group.getSubGroups().getFirst(); + var pvogOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .name(group.getName()).build(); + var pvogChildOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder() + .organisationsEinheitId(subGroup.getOrganisationsEinheitId()).name( + subGroup.getName()) + .build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogChildOranigastionsEinheit)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + + var parentSynced = repository.findSyncedByKeycloakId(group.getId()).get(); + var childSynced = repository.findSyncedByKeycloakId(subGroup.getId()).get(); + + assertThat(parentSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.OK, + pvogOranigastionsEinheit.getId(), + null, + Collections.emptyList(), + syncTimestamp); + assertThat(childSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + subGroup.getId(), + subGroup.getName(), + subGroup.getOrganisationsEinheitId(), + SyncResult.OK, + pvogChildOranigastionsEinheit.getId(), + parentSynced.getId(), + Collections.emptyList(), + syncTimestamp); + } + + @DisplayName("Group with sub group deleted in Keycloak") + @Test + void shouldSyncGroupAndSubGroupDeleted() { + var syncTimestamp = Instant.now().toEpochMilli(); + var group = GroupTestFactory.createBuilder().build(); + var subGroup = group.getSubGroups().getFirst(); + var pvogOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder().organisationsEinheitId(group.getOrganisationsEinheitId()) + .name(group.getName()).build(); + var pvogChildOranigastionsEinheit = OrganisationsEinheitTestFactory.createBuilder() + .organisationsEinheitId(subGroup.getOrganisationsEinheitId()).name( + subGroup.getName()) + .build(); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOranigastionsEinheit)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogChildOranigastionsEinheit)); + + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of()); + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp + 1000); + + var parentSynced = repository.findSyncedByKeycloakId(group.getId()).get(); + var childSynced = repository.findSyncedByKeycloakId(subGroup.getId()).get(); + + assertThat(parentSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + group.getId(), + group.getName(), + group.getOrganisationsEinheitId(), + SyncResult.DELETED, + pvogOranigastionsEinheit.getId(), + null, + Collections.emptyList(), + syncTimestamp); + assertThat(childSynced) + .matches(organisationsEinheit -> Objects.nonNull(organisationsEinheit.getId())) + .matches(organisationsEinheit -> Objects.isNull(organisationsEinheit.getSettings().getSignatur())) + .extracting( + OrganisationsEinheit::getKeycloakId, + OrganisationsEinheit::getName, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getZufiId, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getChildren, + OrganisationsEinheit::getLastSyncTimestamp + ).containsExactly( + subGroup.getId(), + subGroup.getName(), + subGroup.getOrganisationsEinheitId(), + SyncResult.DELETED, + pvogChildOranigastionsEinheit.getId(), + parentSynced.getId(), + Collections.emptyList(), + syncTimestamp); + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncServiceTest.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f9b3a0011fc4767b653a49ea3d1c3b85f65034e8 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/SyncServiceTest.java @@ -0,0 +1,650 @@ +package de.ozgcloud.admin.organisationseinheit; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.admin.keycloak.AddGroupData; +import de.ozgcloud.admin.keycloak.AddGroupDataTestFactory; +import de.ozgcloud.admin.keycloak.Group; +import de.ozgcloud.admin.keycloak.GroupTestFactory; +import de.ozgcloud.admin.keycloak.KeycloakRemoteService; +import de.ozgcloud.admin.keycloak.ResourceCreationException; + +class SyncServiceTest { + + @Spy + @InjectMocks + private SyncService service; + + @Mock + private OrganisationsEinheitRepository repository; + + @Mock + private KeycloakRemoteService keycloakRemoteService; + + @Mock + private OrganisationsEinheitRemoteService organisationsEinheitRemoteService; + + @Mock + private OrganisationsEinheitMapper organisationsEinheitMapper; + + @Nested + class TestSyncOrganisationsEinheitenFromKeycloak { + + private final long syncTimestamp = Instant.now().toEpochMilli(); + private final Group group = GroupTestFactory.create(); + + @BeforeEach + void setUp() { + when(keycloakRemoteService.getGroupsWithOrganisationsEinheitId()).thenReturn(Stream.of(group)); + doNothing().when(service).syncGroupsWithSubGroups(any(), any(), anyLong()); + } + + @Test + void shouldGetGroupsFromKeycloak() { + callService(); + + verify(keycloakRemoteService).getGroupsWithOrganisationsEinheitId(); + } + + @Test + void shouldSyncGroupsFromKeycloak() { + callService(); + + verify(service).syncGroupsWithSubGroups(group, null, syncTimestamp); + } + + @Test + void shouldSetUnsyncedAsDeleted() { + callService(); + + verify(repository).setUnsyncedAsDeleted(syncTimestamp); + } + + private void callService() { + service.syncOrganisationsEinheitenFromKeycloak(syncTimestamp); + } + + } + + @Nested + class TestSyncGroupsWithSubGroups { + + private final long syncTimestamp = Instant.now().toEpochMilli(); + private final Group group = GroupTestFactory.create(); + private final OrganisationsEinheit syncedOrganisationsEinheitGroup = OrganisationsEinheitTestFactory.create(); + private final OrganisationsEinheit savedOrganisationsEinheitGroup = OrganisationsEinheitTestFactory.createBuilder() + .id(UUID.randomUUID().toString()).build(); + + @BeforeEach + void setUp() { + doReturn(syncedOrganisationsEinheitGroup).when(service).syncGroup(group, null, syncTimestamp); + doReturn(savedOrganisationsEinheitGroup).when(service).saveSyncedOrganisationsEinheit(syncedOrganisationsEinheitGroup); + } + + @Test + void shouldSyncGroup() { + service.syncGroupsWithSubGroups(group, null, syncTimestamp); + + verify(service).syncGroup(group, null, syncTimestamp); + } + + @Test + void shouldSaveSyncedOrganisationsEinheit() { + service.syncGroupsWithSubGroups(group, null, syncTimestamp); + + verify(service).saveSyncedOrganisationsEinheit(syncedOrganisationsEinheitGroup); + } + + @Test + void shouldSyncSubGroupsWithSyncedOrganisationsEinheitAsParent() { + service.syncGroupsWithSubGroups(group, null, syncTimestamp); + + verify(service).syncGroupsWithSubGroups(group.getSubGroups().getFirst(), savedOrganisationsEinheitGroup, syncTimestamp); + } + } + + @Nested + class TestSyncGroup { + + private final long syncTimestamp = Instant.now().toEpochMilli(); + private final Group group = GroupTestFactory.create(); + private final OrganisationsEinheit parent = OrganisationsEinheitTestFactory.createBuilder().id(UUID.randomUUID().toString()).build(); + private final OrganisationsEinheit pvogOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().zufiId(null).settings(null) + .parentId(null).syncResult(null).build(); + private final String syncedName = LoremIpsum.getInstance().getWords(3); + private final SyncResult syncedSyncResult = SyncResult.OK; + private final String syncedZufiId = UUID.randomUUID().toString(); + + @Nested + class ParentGroup { + + @BeforeEach + void setUp() { + doReturn(syncedName).when(service).syncName(List.of(pvogOrganisationsEinheit), group); + doReturn(syncedSyncResult).when(service).evaluateSyncResult(List.of(pvogOrganisationsEinheit), group); + doReturn(syncedZufiId).when(service).syncZufiId(List.of(pvogOrganisationsEinheit)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOrganisationsEinheit)); + } + + @Test + void shouldSyncKeycloakId() { + var synced = service.syncGroup(group, null, syncTimestamp); + + assertThat(synced.getKeycloakId()).isEqualTo(GroupTestFactory.ID); + } + + @Test + void shouldSyncName() { + var synced = service.syncGroup(group, null, syncTimestamp); + + verify(service).syncName(List.of(pvogOrganisationsEinheit), group); + assertThat(synced.getName()).isEqualTo(syncedName); + } + + @Test + void shouldEvaluateSyncResult() { + var synced = service.syncGroup(group, null, syncTimestamp); + + verify(service).evaluateSyncResult(List.of(pvogOrganisationsEinheit), group); + assertThat(synced.getSyncResult()).isEqualTo(syncedSyncResult); + } + + @Test + void shouldGetOrganisationsEinheit() { + service.syncGroup(group, null, syncTimestamp); + + verify(organisationsEinheitRemoteService).getByOrganisationsEinheitId(GroupTestFactory.ORGANISATIONS_EINHEIT_ID); + } + + @Test + void shouldSetOrganisationsEinheitId() { + var synced = service.syncGroup(group, null, syncTimestamp); + + assertThat(synced.getOrganisationsEinheitId()).isEqualTo(group.getOrganisationsEinheitId()); + } + + @Test + void shouldSyncZufiId() { + var synced = service.syncGroup(group, null, syncTimestamp); + + verify(service).syncZufiId(List.of(pvogOrganisationsEinheit)); + assertThat(synced.getZufiId()).isEqualTo(syncedZufiId); + } + + @Test + void shouldSetLastSyncTimestamp() { + var synced = service.syncGroup(group, null, syncTimestamp); + + assertThat(synced.getLastSyncTimestamp()).isEqualTo(syncTimestamp); + } + } + + @Nested + class SubGroup { + + @BeforeEach + void setUp() { + doReturn(syncedName).when(service).syncName(List.of(pvogOrganisationsEinheit), group.getSubGroups().getFirst()); + doReturn(syncedSyncResult).when(service).evaluateSyncResult(List.of(pvogOrganisationsEinheit), group.getSubGroups().getFirst()); + doReturn(syncedZufiId).when(service).syncZufiId(List.of(pvogOrganisationsEinheit)); + when(organisationsEinheitRemoteService.getByOrganisationsEinheitId(GroupTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID)).thenReturn( + List.of(pvogOrganisationsEinheit)); + } + + @Test + void shouldSyncKeycloakId() { + var synced = service.syncGroup(group.getSubGroups().getFirst(), parent, syncTimestamp); + + assertThat(synced.getKeycloakId()).isEqualTo(GroupTestFactory.SUB_GROUP_ID); + } + + @Test + void shouldSyncName() { + var synced = service.syncGroup(group.getSubGroups().getFirst(), parent, syncTimestamp); + + verify(service).syncName(List.of(pvogOrganisationsEinheit), group.getSubGroups().getFirst()); + assertThat(synced.getName()).isEqualTo(syncedName); + } + + @Test + void shouldEvaluateSyncResult() { + var synced = service.syncGroup(group.getSubGroups().getFirst(), parent, syncTimestamp); + + verify(service).evaluateSyncResult(List.of(pvogOrganisationsEinheit), group.getSubGroups().getFirst()); + assertThat(synced.getSyncResult()).isEqualTo(syncedSyncResult); + } + + @Test + void shouldGetOrganisationsEinheit() { + var synced = service.syncGroup(group.getSubGroups().getFirst(), parent, syncTimestamp); + + verify(organisationsEinheitRemoteService).getByOrganisationsEinheitId(GroupTestFactory.SUB_GROUP_ORGANISATIONS_EINHEIT_ID); + assertThat(synced.getOrganisationsEinheitId()).isEqualTo(group.getSubGroups().getFirst().getOrganisationsEinheitId()); + } + + @Test + void shouldSetParent() { + var synced = service.syncGroup(group.getSubGroups().getFirst(), parent, syncTimestamp); + + assertThat(synced.getParentId()).contains(parent.getId()); + } + + @Test + void shouldSyncZufiId() { + var synced = service.syncGroup(group.getSubGroups().getFirst(), parent, syncTimestamp); + + verify(service).syncZufiId(List.of(pvogOrganisationsEinheit)); + assertThat(synced.getZufiId()).isEqualTo(syncedZufiId); + } + + @Test + void shouldSetLastSyncTimestamp() { + var synced = service.syncGroup(group.getSubGroups().getFirst(), parent, syncTimestamp); + + assertThat(synced.getLastSyncTimestamp()).isEqualTo(syncTimestamp); + } + } + } + + @Nested + class TestSyncName { + + @Test + void shouldReturnPvogName() { + var name = service.syncName(List.of(OrganisationsEinheitTestFactory.create()), GroupTestFactory.create()); + + assertThat(name).isEqualTo(OrganisationsEinheitTestFactory.NAME); + + } + + @Test + void shouldReturnGroupNameIfOrganisatonsEinheitNotFoundInPvog() { + var name = service.syncName(Collections.emptyList(), GroupTestFactory.create()); + + assertThat(name).isEqualTo(GroupTestFactory.NAME); + } + + @Test + void shouldReturnGroupNameIfMultipleOrganisatonsEinheitenFoundInPvog() { + var name = service.syncName(List.of(OrganisationsEinheitTestFactory.create(), OrganisationsEinheitTestFactory.create()), + GroupTestFactory.create()); + + assertThat(name).isEqualTo(GroupTestFactory.NAME); + } + } + + @Nested + class TestEvaluateSyncResult { + + @Test + void shouldReturnOk() { + var syncResult = service.evaluateSyncResult(List.of(OrganisationsEinheitTestFactory.createBuilder().name(GroupTestFactory.NAME).build()), + GroupTestFactory.create()); + + assertThat(syncResult).isEqualTo(SyncResult.OK); + } + + @Test + void shouldReturnNotFoundInPvog() { + var syncResult = service.evaluateSyncResult(Collections.emptyList(), GroupTestFactory.create()); + + assertThat(syncResult).isEqualTo(SyncResult.NOT_FOUND_IN_PVOG); + } + + @Test + void shouldReturnNameMismatch() { + var syncResult = service.evaluateSyncResult(List.of(OrganisationsEinheitTestFactory.create()), GroupTestFactory.create()); + + assertThat(syncResult).isEqualTo(SyncResult.NAME_MISMATCH); + } + + @Test + void shouldReturnOrganisationseinheitIdNotUnique() { + var syncResult = service.evaluateSyncResult(List.of(OrganisationsEinheitTestFactory.create(), OrganisationsEinheitTestFactory.create()), + GroupTestFactory.create()); + + assertThat(syncResult).isEqualTo(SyncResult.ORGANISATIONSEINHEIT_ID_NOT_UNIQUE); + } + + } + + @Nested + class TestSyncZufiId { + + @Test + void shouldReturnZufiId() { + var zufiId = service.syncZufiId(List.of(OrganisationsEinheitTestFactory.create())); + + assertThat(zufiId).isEqualTo(OrganisationsEinheitTestFactory.ID); + } + + @Test + void shouldReturnNullIfOrganisationsEinheitNotInPvog() { + var zufiId = service.syncZufiId(Collections.emptyList()); + + assertThat(zufiId).isNull(); + } + + @Test + void shouldReturnNullIfOrganisationsEinheitNotUnique() { + var zufiId = service.syncZufiId(List.of(OrganisationsEinheitTestFactory.create(), OrganisationsEinheitTestFactory.create())); + + assertThat(zufiId).isNull(); + } + } + + @Nested + class TestSaveSyncedOrganisationsEinheit { + + private final OrganisationsEinheit syncedOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().id(null).settings(null) + .build(); + private final OrganisationsEinheit savedOrganisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().settings(null) + .build(); + + @Test + void shouldFindSyncedOrganisationsEinheitInDatabase() { + service.saveSyncedOrganisationsEinheit(syncedOrganisationsEinheit); + + verify(repository).findSyncedByKeycloakId(syncedOrganisationsEinheit.getKeycloakId()); + } + + @Test + void shouldReturnSaved() { + when(repository.save(syncedOrganisationsEinheit)).thenReturn(savedOrganisationsEinheit); + + var saved = service.saveSyncedOrganisationsEinheit(syncedOrganisationsEinheit); + + assertThat(saved).isEqualTo(savedOrganisationsEinheit); + } + + @Nested + class SyncedOrganisationsEinheitDoesNotExist { + + @BeforeEach + void setUp() { + when(repository.findSyncedByKeycloakId( + syncedOrganisationsEinheit.getKeycloakId())).thenReturn(Optional.empty()); + } + + @Test + void shouldSave() { + service.saveSyncedOrganisationsEinheit(syncedOrganisationsEinheit); + + verify(repository).save(syncedOrganisationsEinheit); + } + } + + @Nested + class SyncedOrganisationsEinheitExists { + + private final OrganisationsEinheit existingOrganisationsEinheit = OrganisationsEinheitTestFactory.createNewBuilder().build(); + + @Captor + private ArgumentCaptor<OrganisationsEinheit> savedOrganisationsEinheitArgumentCaptor; + + @BeforeEach + void setUp() { + when(repository.findSyncedByKeycloakId( + syncedOrganisationsEinheit.getKeycloakId())).thenReturn(Optional.of(existingOrganisationsEinheit)); + } + + @Test + void shouldSaveSyncedOrganisationsEinheitWithSettings() { + service.saveSyncedOrganisationsEinheit(syncedOrganisationsEinheit); + + verify(repository).save(savedOrganisationsEinheitArgumentCaptor.capture()); + assertThat(savedOrganisationsEinheitArgumentCaptor.getValue()) + .extracting( + OrganisationsEinheit::getId, + OrganisationsEinheit::getOrganisationsEinheitId, + OrganisationsEinheit::getSettings, + OrganisationsEinheit::getName, + OrganisationsEinheit::getSyncResult, + OrganisationsEinheit::getParentId, + OrganisationsEinheit::getZufiId) + .containsExactly( + existingOrganisationsEinheit.getId(), + syncedOrganisationsEinheit.getOrganisationsEinheitId(), + existingOrganisationsEinheit.getSettings(), + syncedOrganisationsEinheit.getName(), + syncedOrganisationsEinheit.getSyncResult(), + syncedOrganisationsEinheit.getParentId(), + syncedOrganisationsEinheit.getZufiId() + ); + } + } + } + + @Nested + class TestSyncAddedOrganisationsEinheiten { + + private final long syncTimestamp = Instant.now().toEpochMilli(); + private final OrganisationsEinheit withoutSyncResult1 = OrganisationsEinheitTestFactory.createBuilder().id("A").build(); + private final OrganisationsEinheit withoutSyncResult2 = OrganisationsEinheitTestFactory.createBuilder().id("B").build(); + private final OrganisationsEinheit[] organisationsEinheiten = new OrganisationsEinheit[] { withoutSyncResult2, withoutSyncResult1 }; + + @Captor + private ArgumentCaptor<Stream<OrganisationsEinheit>> streamArgumentCaptor; + + @BeforeEach + void setUp() { + when(repository.findAllWithoutSyncResult()).thenReturn(Stream.of(organisationsEinheiten)); + doNothing().when(service).syncAddedOrganisationsEinheit(any(), anyLong()); + } + + @Test + void shouldFindAllWithoutSyncResult() { + callService(); + + verify(repository).findAllWithoutSyncResult(); + } + + @Test + void shouldSynchronizeFirstOrganisationsEinheit() { + callService(); + + verify(service).syncAddedOrganisationsEinheit(withoutSyncResult1, syncTimestamp); + } + + @Test + void shouldSynchronizeSecondOrganisationsEinheit() { + callService(); + + verify(service).syncAddedOrganisationsEinheit(withoutSyncResult2, syncTimestamp); + } + + private void callService() { + service.syncAddedOrganisationsEinheiten(syncTimestamp); + } + } + + @Nested + class TestSyncAddedOrganisationsEinheit { + + private final long syncTimestamp = Instant.now().toEpochMilli(); + private final String addedGroupId = UUID.randomUUID().toString(); + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.create(); + + @Test + void shouldAddAsGroupInKeycloak() { + doReturn(Optional.empty()).when(service).addAsGroupInKeycloak(organisationsEinheit); + + callService(); + + verify(service).addAsGroupInKeycloak(organisationsEinheit); + } + + @Test + void shouldUpdateAfterSuccessfulGroupCreation() { + doReturn(Optional.of(addedGroupId)).when(service).addAsGroupInKeycloak(organisationsEinheit); + doNothing().when(service).updateAfterSuccessfulGroupCreation(organisationsEinheit, addedGroupId, syncTimestamp); + + callService(); + + verify(service).updateAfterSuccessfulGroupCreation(organisationsEinheit, addedGroupId, syncTimestamp); + } + + @Test + void shouldNotUpdate() { + doReturn(Optional.empty()).when(service).addAsGroupInKeycloak(organisationsEinheit); + + callService(); + + verify(service, never()).updateAfterSuccessfulGroupCreation(any(), any(), anyLong()); + } + + private void callService() { + service.syncAddedOrganisationsEinheit(organisationsEinheit, syncTimestamp); + } + } + + @Nested + class TestAddAsGroupInKeycloak { + + @Nested + class OnParentIdIsNotSet { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.createBuilder().parentId(null).build(); + private final AddGroupData addGroupData = AddGroupDataTestFactory.create(); + private final String addedGroupId = UUID.randomUUID().toString(); + + @BeforeEach + void init() { + when(organisationsEinheitMapper.toAddGroupData(organisationsEinheit)).thenReturn(addGroupData); + doReturn(Optional.of(addedGroupId)).when(service).addGroupInKeycloak(addGroupData); + } + + @Test + void shouldCreateAddGroupData() { + callService(); + + verify(organisationsEinheitMapper).toAddGroupData(organisationsEinheit); + } + + @Test + void shouldAddGroupInKeycloak() { + callService(); + + verify(service).addGroupInKeycloak(addGroupData); + } + + @Test + void shouldReturnAddedGroupId() { + var returnedGroupId = callService(); + + assertThat(returnedGroupId).get().isEqualTo(addedGroupId); + } + + private Optional<String> callService() { + return service.addAsGroupInKeycloak(organisationsEinheit); + } + } + + @Nested + class OnParentIdIsSet { + + @Test + void shouldThrowException() { + var withParentId = OrganisationsEinheitTestFactory.create(); + assertThatExceptionOfType(OrganisationsEinheitSynchronizationException.class) + .isThrownBy(() -> service.addAsGroupInKeycloak(withParentId)) + .withMessage("Organisationseinheit " + withParentId.getOrganisationsEinheitId() + " has parent"); + } + } + } + + @Nested + class TestUpdateAfterSuccessfulGroupCreation { + + private final OrganisationsEinheit organisationsEinheit = OrganisationsEinheitTestFactory.createBuilder() + .keycloakId(null) + .syncResult(null) + .lastSyncTimestamp(null) + .build(); + private final long syncTimestamp = Instant.now().toEpochMilli(); + private final String keycloakId = UUID.randomUUID().toString(); + + @Captor + private ArgumentCaptor<OrganisationsEinheit> organisationsEinheitArgumentCaptor; + + @Test + void shouldSaveUpdatedOrganisationsEinheit() { + service.updateAfterSuccessfulGroupCreation(organisationsEinheit, keycloakId, syncTimestamp); + + verify(repository).save(organisationsEinheitArgumentCaptor.capture()); + assertThat(organisationsEinheitArgumentCaptor.getValue()) + .extracting(OrganisationsEinheit::getKeycloakId, OrganisationsEinheit::getSyncResult, OrganisationsEinheit::getLastSyncTimestamp) + .containsExactly(keycloakId, SyncResult.OK, syncTimestamp); + } + } + + @Nested + class TestAddGroupInKeycloak { + + private static final String FAILURE_MESSAGE = LoremIpsum.getInstance().getWords(5); + private final Throwable resourceCreationException = new ResourceCreationException(FAILURE_MESSAGE); + + private final String keycloakId = GroupTestFactory.ID; + private final AddGroupData addGroupData = AddGroupDataTestFactory.create(); + + @Test + void shouldAddGroup() { + givenAddGroupSuccessful(); + + callService(); + + verify(keycloakRemoteService).addGroup(addGroupData); + } + + @Test + void shouldReturnKeycloakId() { + givenAddGroupSuccessful(); + + var keycloakId = callService(); + + assertThat(keycloakId).isPresent().get().isEqualTo(this.keycloakId); + } + + @Test + void shouldThrowExceptionInCaseOfResourceCreationException() { + givenAddGroupFailed(); + + assertThatExceptionOfType(OrganisationsEinheitSynchronizationException.class).isThrownBy(this::callService) + .withMessage("Error creating group %s in Keycloak", addGroupData.toString()).withCause(resourceCreationException); + } + + private void givenAddGroupSuccessful() { + when(keycloakRemoteService.addGroup(addGroupData)).thenReturn(keycloakId); + } + + private void givenAddGroupFailed() { + when(keycloakRemoteService.addGroup(addGroupData)).thenThrow(resourceCreationException); + } + + private Optional<String> callService() { + return service.addGroupInKeycloak(addGroupData); + } + } + +} \ No newline at end of file diff --git a/src/test/resources/application-itcase.yaml b/src/test/resources/application-itcase.yaml index 3082babc0c50e52484ef75f5a650980975eea15d..1217c868aaa994671a26171005505ce110d401a5 100644 --- a/src/test/resources/application-itcase.yaml +++ b/src/test/resources/application-itcase.yaml @@ -1,3 +1,8 @@ +spring: + data: + mongodb: + database: config-db + mongock: enabled: false @@ -6,3 +11,7 @@ ozgcloud: auth-server-url: https://sso.it-case.de realm: by-kiel-dev resource: admin + keycloak: + api: + user: administrationApiUser + password: administrationApiUser diff --git a/src/test/resources/keycloak/realm-export.json b/src/test/resources/keycloak/realm-export.json index 1a35e536dee166f56fe2a5b52dd5d6cb3452e21c..9bb03f072583dec19005dc96e8af90465d6ff7e5 100755 --- a/src/test/resources/keycloak/realm-export.json +++ b/src/test/resources/keycloak/realm-export.json @@ -44,6 +44,94 @@ "quickLoginCheckMilliSeconds": 1000, "maxDeltaTimeSeconds": 43200, "failureFactor": 30, + "groups": [ + { + "id": "GroupWithChild-id", + "name": "GroupWithChild", + "path": "/GroupWithChild", + "subGroups": [ + { + "id": "ChildLevel1WithChild-id", + "name": "ChildLevel1WithChild", + "path": "/GroupWithChild/ChildLevel1WithChild", + "parentId": "GroupWithChild-id", + "subGroups": [ + { + "id": "ChildLevel2-id", + "name": "ChildLevel2", + "path": "/GroupWithChild/ChildLevel1WithChild/ChildLevel2", + "parentId": "ChildLevel1WithChild-id", + "subGroups": [], + "attributes": { + "organisationseinheitId": [ + "ChildLevel2-oid" + ] + }, + "realmRoles": [], + "clientRoles": {} + } + ], + "attributes": { + "organisationseinheitId": [ + "ChildLevel1WithChild-oid" + ] + }, + "realmRoles": [], + "clientRoles": {} + }, + { + "id": "ChildLevel1WithoutOid-id", + "name": "ChildLevel1WithoutOid", + "path": "/GroupWithChild/ChildLevel1WithoutOid", + "parentId": "GroupWithChild-id", + "subGroups": [], + "attributes": {}, + "realmRoles": [], + "clientRoles": {} + } + ], + "attributes": { + "organisationseinheitId": [ + "GroupWithChild-oid" + ] + }, + "realmRoles": [], + "clientRoles": {} + }, + { + "id": "GroupWithoutOid-id", + "name": "GroupWithoutOid", + "path": "/GroupWithoutOid", + "subGroups": [], + "attributes": {}, + "realmRoles": [], + "clientRoles": {} + }, + { + "id": "GroupWithoutOidWithChild-id", + "name": "GroupWithoutOidWithChild", + "path": "/GroupWithoutOidWithChild", + "subGroups": [ + { + "id": "6683b1ee-3f27-4f42-aced-35b0d271f4df", + "name": "ChildLevel1", + "path": "/GroupWithoutOidWithChild/ChildLevel1", + "parentId": "GroupWithoutOidWithChild-id", + "subGroups": [], + "attributes": { + "organisationseinheitId": [ + "ChildLevel1-oid" + ] + }, + "realmRoles": [], + "clientRoles": {} + } + ], + "attributes": {}, + "realmRoles": [], + "clientRoles": {} + } + ], "defaultRole": { "id": "dd27b699-836d-4ed8-9111-ee34acbaf5ce", "name": "default-roles-by-kiel-dev", @@ -2037,16 +2125,20 @@ }, "users": [ { - "username": "admin-test", + "id": "b46def26-a599-4940-a32f-e070c478750d", + "username": "administrationApiUser", "firstName": "Vorname", "lastName": "Nachname", "enabled": true, "credentials": [ { "type": "password", - "value": "Password" + "value": "administrationApiUser" } - ] + ], + "clientRoles" : { + "realm-management" : [ "view-users", "manage-users" ] + } } ] } \ No newline at end of file