From cfd94760a79d77e4723c9ed1379c9deede20d925 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Sat, 17 Jun 2023 09:32:13 +0200
Subject: [PATCH] OZG-3961 - add user to keycloak, cleanup

---
 pom.xml                                       | 56 ++++----------
 .../operator/keycloak/KeycloakClient.java     | 40 +++++++---
 .../operator/keycloak/KeycloakException.java  |  9 +++
 .../user/KeycloakClientUserRemoteService.java | 19 -----
 .../user/KeycloakClientUserService.java       | 15 ----
 .../keycloak/user/KeycloakUserMapper.java     | 37 ++++++++++
 .../keycloak/user/KeycloakUserReconciler.java | 58 +++++----------
 .../user/KeycloakUserRemoteService.java       | 28 +++++++
 .../keycloak/user/KeycloakUserService.java    | 22 ++++++
 .../keycloak/user/KeycloakUserStatus.java     |  2 +-
 .../KeycloakClientUserRemoteServiceTest.java  | 31 --------
 .../keycloak/user/KeycloakUserMapperTest.java | 39 ++++++++++
 .../user/KeycloakUserReconcilerTest.java      | 55 ++++++++------
 .../user/KeycloakUserRemoteServiceTest.java   | 74 +++++++++++++++++++
 .../user/KeycloakUserServiceTest.java         | 46 ++++++++++++
 .../user/KeycloakUserSpecUserTestFactory.java |  7 +-
 .../user/KeycloakUserTestFactory.java         |  9 ++-
 .../user/UserRepresentationTestFactory.java   | 18 +++++
 .../org.junit.jupiter.api.extension.Extension |  1 +
 src/test/resources/junit-platform.properties  |  1 +
 20 files changed, 378 insertions(+), 189 deletions(-)
 create mode 100644 src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java
 delete mode 100644 src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteService.java
 delete mode 100644 src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserService.java
 create mode 100644 src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java
 create mode 100644 src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
 create mode 100644 src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
 delete mode 100644 src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteServiceTest.java
 create mode 100644 src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java
 create mode 100644 src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java
 create mode 100644 src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java
 create mode 100644 src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java
 create mode 100644 src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
 create mode 100644 src/test/resources/junit-platform.properties

diff --git a/pom.xml b/pom.xml
index 04d90dc..35173b6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,21 +2,23 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 	<modelVersion>4.0.0</modelVersion>
+	
 	<parent>
-		<groupId>org.springframework.boot</groupId>
-		<artifactId>spring-boot-starter-parent</artifactId>
-		<version>3.1.0</version>
+		<groupId>de.itvsh.kop.common</groupId>
+		<artifactId>kop-common-parent</artifactId>
+		<version>2.1.0</version>
 		<relativePath/> <!-- lookup parent from repository -->
 	</parent>
+
+
+
 	<groupId>de.ozgcloud</groupId>
 	<artifactId>ozg-operator</artifactId>
 	<version>1.0.0-SNAPSHOT</version>
 	<name>OZG Cloud Operator</name>
 	<description>OZG Cloud Operator</description>
 	<properties>
-		<java.version>17</java.version>
 		<operator-sdk.version>5.0.0</operator-sdk.version>
-<!--		<operator-sdk.version>4.2.8</operator-sdk.version>-->
 		<spring-boot.build-image.imageName>docker.ozg-sh.de/ozg-operator:build-latest</spring-boot.build-image.imageName>
 	</properties>
 	<dependencies>
@@ -24,17 +26,14 @@
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter</artifactId>
 		</dependency>
-<!--		<dependency>-->
-<!--			<groupId>org.springframework.boot</groupId>-->
-<!--			<artifactId>spring-boot-starter-web-services</artifactId>-->
-<!--		</dependency>-->
-
-<!-- https://mvnrepository.com/artifact/org.keycloak/keycloak-admin-client -->
-<dependency>
-    <groupId>org.keycloak</groupId>
-    <artifactId>keycloak-admin-client</artifactId>
-    <version>21.1.1</version>
-</dependency>
+		<dependency>
+		    <groupId>org.keycloak</groupId>
+		    <artifactId>keycloak-admin-client</artifactId>
+		</dependency>
+		<dependency>
+		    <groupId>org.mapstruct</groupId>
+		    <artifactId>mapstruct</artifactId>
+		</dependency>
 
 
 		<dependency>
@@ -46,41 +45,16 @@
 			<groupId>org.projectlombok</groupId>
 			<artifactId>lombok</artifactId>
 		</dependency>
-<!--		<dependency>-->
-<!--			<groupId>org.apache.logging.log4j</groupId>-->
-<!--			<artifactId>log4j-core</artifactId>-->
-<!--		</dependency>-->
-<!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
 		<dependency>
 		    <groupId>org.reflections</groupId>
 		    <artifactId>reflections</artifactId>
 		    <version>0.10.2</version>
 		</dependency>
-<!--		<dependency>-->
-<!--			<groupId>org.springframework.boot</groupId>-->
-<!--			<artifactId>spring-boot-starter-actuator</artifactId>-->
-<!--		</dependency>-->
-
-
-<!--		<dependency>-->
-<!--		    <groupId>io.fabric8</groupId>-->
-<!--		    <artifactId>crd-generator-apt</artifactId>-->
-<!--		    <scope>provided</scope>-->
-<!--		    <version>6.7.1</version>-->
-<!--		</dependency>-->
-
-
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-test</artifactId>
 			<scope>test</scope>
 		</dependency>
-<!--		<dependency>-->
-<!--		    <groupId>io.javaoperatorsdk</groupId>-->
-<!--		    <artifactId>operator-framework-spring-boot-starter-test</artifactId>-->
-<!--		    <version>${operator-sdk.version}</version>-->
-<!--			<scope>test</scope>-->
-<!--		</dependency>-->
 	</dependencies>
 
 	<build>
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java b/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
index cc9f184..1797ee6 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
+++ b/src/main/java/de/ozgcloud/operator/keycloak/KeycloakClient.java
@@ -1,33 +1,49 @@
 package de.ozgcloud.operator.keycloak;
 
+import java.util.Base64;
+
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.admin.client.KeycloakBuilder;
-import org.keycloak.representations.idm.UserRepresentation;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.dsl.Resource;
 import lombok.extern.java.Log;
 
 @Log
 @Component
 public class KeycloakClient {
 
+	@Autowired
+	private KubernetesClient kubernetesClient;
+
 	public Keycloak getKeycloak() {
 
-		Keycloak keycloak = KeycloakBuilder.builder() //
-				.serverUrl("https://sso.dev.by.ozg-cloud.de/") //
-				.realm("master") //
-//				.grantType(OAuth2Constants.PASSWORD) //
-				.username("admin") //
-				.password(" ") //
+		return KeycloakBuilder.builder()
+				.serverUrl("http://keycloak-keycloakx-http.keycloak")
+				.realm("master")
+				.username("admin")
+				.password(getKeycloakAdminPassword())
 				.clientId("admin-cli")
 				.build();
+	}
 
-		log.info(keycloak.realms().findAll().toString());
+	String getKeycloakAdminPassword() {
+		return decodeBase64(getKeycloakRealmAdminSecret()
+				.get()
+				.getData()
+				.get("password"));
+	}
 
-		UserRepresentation user = new UserRepresentation();
-		user.setUsername("helge");
-		keycloak.realm("by-torsten-ozg-operator-dev").users().create(user);
+	Resource<Secret> getKeycloakRealmAdminSecret() {
+		return kubernetesClient.secrets()
+				.inNamespace("keycloak")
+				.withName("keycloak-admin-secret");
+	}
 
-		return keycloak;
+	String decodeBase64(String base64String) {
+		return new String(Base64.getDecoder().decode(base64String));
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java b/src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java
new file mode 100644
index 0000000..d95cccb
--- /dev/null
+++ b/src/main/java/de/ozgcloud/operator/keycloak/KeycloakException.java
@@ -0,0 +1,9 @@
+package de.ozgcloud.operator.keycloak;
+
+@SuppressWarnings("serial")
+public class KeycloakException extends RuntimeException {
+
+	public KeycloakException(String string) {
+		super(string);
+	}
+}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteService.java
deleted file mode 100644
index 63ce46f..0000000
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteService.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.ozgcloud.operator.keycloak.user;
-
-import org.keycloak.representations.account.UserRepresentation;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import de.ozgcloud.operator.keycloak.KeycloakClient;
-
-@Component
-public class KeycloakClientUserRemoteService {
-
-	@Autowired
-	private KeycloakClient keycloakClient;
-
-	void createUser(UserRepresentation user) {
-
-		keycloakClient.getKeycloak();
-	}
-}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserService.java
deleted file mode 100644
index a87f0ed..0000000
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserService.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.ozgcloud.operator.keycloak.user;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-@Component
-public class KeycloakClientUserService {
-
-	@Autowired
-	KeycloakClientUserRemoteService remoteService;
-
-	void addUser(KeycloakUserSpec userSpec) {
-
-	}
-}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java
new file mode 100644
index 0000000..3d855fa
--- /dev/null
+++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapper.java
@@ -0,0 +1,37 @@
+package de.ozgcloud.operator.keycloak.user;
+
+import org.keycloak.representations.idm.UserRepresentation;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper
+interface KeycloakUserMapper {
+
+	@Mapping(target = "access", ignore = true)
+	@Mapping(target = "attributes", ignore = true)
+	@Mapping(target = "clientConsents", ignore = true)
+	@Mapping(target = "clientRoles", ignore = true)
+	@Mapping(target = "createdTimestamp", ignore = true)
+	@Mapping(target = "credentials", ignore = true)
+	@Mapping(target = "disableableCredentialTypes", ignore = true)
+	@Mapping(target = "email", ignore = true)
+	@Mapping(target = "emailVerified", ignore = true)
+	@Mapping(target = "enabled", ignore = true)
+	@Mapping(target = "federatedIdentities", ignore = true)
+	@Mapping(target = "federationLink", ignore = true)
+	@Mapping(target = "groups", ignore = true)
+	@Mapping(target = "id", ignore = true)
+	@Mapping(target = "notBefore", ignore = true)
+	@Mapping(target = "origin", ignore = true)
+	@Mapping(target = "realmRoles", ignore = true)
+	@Mapping(target = "requiredActions", ignore = true)
+	@Mapping(target = "self", ignore = true)
+	@Mapping(target = "serviceAccountClientId", ignore = true)
+	@Mapping(target = "socialLinks", ignore = true)
+	@Mapping(target = "totp", ignore = true)
+	@Mapping(target = "applicationRoles", ignore = true)
+	@Mapping(target = "username", source = "keycloakUser.username")
+	@Mapping(target = "firstName", source = "keycloakUser.firstName")
+	@Mapping(target = "lastName", source = "keycloakUser.lastName")
+	UserRepresentation toUserRepresentation(KeycloakUserSpec user);
+}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
index 3aff0f6..a9a8672 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
+++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconciler.java
@@ -1,11 +1,9 @@
 package de.ozgcloud.operator.keycloak.user;
 
-import java.time.LocalDate;
-import java.util.Base64;
+import java.util.logging.Level;
+
+import org.springframework.beans.factory.annotation.Autowired;
 
-import io.fabric8.kubernetes.api.model.Secret;
-import io.fabric8.kubernetes.client.KubernetesClient;
-import io.fabric8.kubernetes.client.dsl.Resource;
 import io.javaoperatorsdk.operator.api.reconciler.Context;
 import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
 import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
@@ -13,50 +11,30 @@ import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
 import lombok.extern.java.Log;
 
 @ControllerConfiguration
-//@Log4j2
 @Log
 public class KeycloakUserReconciler implements Reconciler<KeycloakUser> {
 
-	private final KubernetesClient kubernetesClient;
+	public static final String STATUS_OK = "OK";
+	public static final String STATUS_ERROR = "ERROR";
 
-	public KeycloakUserReconciler(KubernetesClient kubernetesClient) {
-		this.kubernetesClient = kubernetesClient;
-	}
+	@Autowired
+	private KeycloakUserService keycloakUserService;
 
 	@Override
-	public UpdateControl<KeycloakUser> reconcile(KeycloakUser crd, Context<KeycloakUser> context) throws Exception {
-
-//		String name = crd.getMetadata().getName();
-		String name = crd.getSpec().getKeycloakUser().getUsername();
-		String namespace = crd.getMetadata().getNamespace();
-		log.warning("Reconciling: " + name + "/" + namespace);
-		log.warning("SecretTest:" + getKeycloakRealmAdminPassword(namespace));
-		crd.setStatus(KeycloakUserStatus.builder().status("Updated status for " + name + LocalDate.now().toString()).phase("Pending").build());
-		return UpdateControl.updateStatus(crd);
-	}
-
-	String getKeycloakRealmAdminPassword(String namespace) {
+	public UpdateControl<KeycloakUser> reconcile(KeycloakUser crd, Context<KeycloakUser> context) {
 
-		Resource<Secret> secret = getKeycloakRealmAdminSecret(namespace);
+		try {
+			String namespace = crd.getMetadata().getNamespace();
 
-//		if (!secret.isReady()) {
-//			throw new RuntimeException("Secret not exists " + namespace + "-admin-credentials");
-//		}
+			keycloakUserService.addUser(crd.getSpec(), namespace);
 
-		return decodeBase64(secret
-				.get()
-				.getData()
-				.get("username"));
-//				.toString();
-	}
-
-	Resource<Secret> getKeycloakRealmAdminSecret(String namespace) {
-		return kubernetesClient.secrets()
-				.inNamespace("keycloak")
-				.withName("keycloak-admin-secret");
-	}
+			crd.setStatus(KeycloakUserStatus.builder().status(STATUS_OK).errorMessage(null).build());
+			return UpdateControl.updateStatus(crd);
 
-	String decodeBase64(String base64String) {
-		return new String(Base64.getDecoder().decode(base64String));
+		} catch (Exception e) {
+			log.log(Level.SEVERE, "Could not reconcile user", e);
+			crd.setStatus(KeycloakUserStatus.builder().status(STATUS_ERROR).errorMessage(e.getMessage()).build());
+			return UpdateControl.updateStatus(crd);
+		}
 	}
 }
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
new file mode 100644
index 0000000..ac5a68c
--- /dev/null
+++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java
@@ -0,0 +1,28 @@
+package de.ozgcloud.operator.keycloak.user;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status.Family;
+
+import org.keycloak.representations.idm.UserRepresentation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import de.ozgcloud.operator.keycloak.KeycloakClient;
+import de.ozgcloud.operator.keycloak.KeycloakException;
+
+@Component
+class KeycloakUserRemoteService {
+
+	@Autowired
+	private KeycloakClient keycloakClient;
+
+	void createUser(UserRepresentation user, String realm) {
+
+		try (Response response = keycloakClient.getKeycloak().realm(realm).users().create(user)) {
+
+			if (!response.getStatusInfo().getFamily().equals(Family.SUCCESSFUL)) {
+				throw new KeycloakException("Could not update user " + user.getUsername() + ";" + response.getStatusInfo());
+			}
+		}
+	}
+}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
new file mode 100644
index 0000000..717a1be
--- /dev/null
+++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java
@@ -0,0 +1,22 @@
+package de.ozgcloud.operator.keycloak.user;
+
+import org.keycloak.representations.idm.UserRepresentation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class KeycloakUserService {
+
+	@Autowired
+	private KeycloakUserRemoteService remoteService;
+
+	@Autowired
+	private KeycloakUserMapper userMapper;
+
+	void addUser(KeycloakUserSpec userSpec, String namespace) {
+
+		UserRepresentation keycloakUser = userMapper.toUserRepresentation(userSpec);
+
+		remoteService.createUser(keycloakUser, namespace);
+	}
+}
diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserStatus.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserStatus.java
index 7e83e84..43a5e5f 100644
--- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserStatus.java
+++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserStatus.java
@@ -16,5 +16,5 @@ public class KeycloakUserStatus extends ObservedGenerationAwareStatus {
 
 	private String status;
 
-	private String phase;
+	private String errorMessage;
 }
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteServiceTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteServiceTest.java
deleted file mode 100644
index 86cd6ff..0000000
--- a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakClientUserRemoteServiceTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package de.ozgcloud.operator.keycloak.user;
-
-import static org.mockito.Mockito.*;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.Spy;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import de.ozgcloud.operator.keycloak.KeycloakClient;
-
-@ExtendWith(MockitoExtension.class)
-class KeycloakClientUserRemoteServiceTest {
-
-	@Spy
-	@InjectMocks
-	KeycloakClientUserRemoteService userRemoteService;
-
-	@Mock
-	KeycloakClient keycloakClient;
-
-	@Test
-	void test() {
-
-		userRemoteService.createUser(null);
-
-		verify(keycloakClient).getKeycloak();
-	}
-}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java
new file mode 100644
index 0000000..6b84fdf
--- /dev/null
+++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserMapperTest.java
@@ -0,0 +1,39 @@
+package de.ozgcloud.operator.keycloak.user;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+import org.mockito.Spy;
+
+class KeycloakUserMapperTest {
+
+	@Spy
+	private KeycloakUserMapper mapper = Mappers.getMapper(KeycloakUserMapper.class);
+
+	@Nested
+	class TesttoUserRepresentation {
+
+		@Test
+		void shouldMapUsername() {
+			var keycloakUser = mapper.toUserRepresentation(KeycloakUserSpecTestFactory.create());
+
+			assertThat(keycloakUser.getUsername()).isEqualTo(KeycloakUserSpecUserTestFactory.USERNAME);
+		}
+
+		@Test
+		void shouldMapFirstname() {
+			var keycloakUser = mapper.toUserRepresentation(KeycloakUserSpecTestFactory.create());
+
+			assertThat(keycloakUser.getFirstName()).isEqualTo(KeycloakUserSpecUserTestFactory.FIRSTNAME);
+		}
+
+		@Test
+		void shouldMapLastname() {
+			var keycloakUser = mapper.toUserRepresentation(KeycloakUserSpecTestFactory.create());
+
+			assertThat(keycloakUser.getLastName()).isEqualTo(KeycloakUserSpecUserTestFactory.LASTNAME);
+		}
+	}
+}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java
index 9c66f82..d8df746 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java
+++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserReconcilerTest.java
@@ -1,23 +1,14 @@
 package de.ozgcloud.operator.keycloak.user;
 
 import static org.assertj.core.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
-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.operator.keycloak.user.KeycloakUser;
-import de.ozgcloud.operator.keycloak.user.KeycloakUserReconciler;
-import io.fabric8.kubernetes.client.KubernetesClient;
-import lombok.SneakyThrows;
-
-@Disabled
 class KeycloakUserReconcilerTest {
 
 	@Spy
@@ -25,26 +16,42 @@ class KeycloakUserReconcilerTest {
 	private KeycloakUserReconciler conciler;
 
 	@Mock
-	private KubernetesClient kubernetesClient;
-
-	@Nested
-	class TestKeycloalUserStatus {
+	private KeycloakUserService userService;
 
-		@BeforeEach
-		void init() {
-			doReturn("hase").when(conciler).getKeycloakRealmAdminPassword(anyString());
+	@BeforeEach
+	void init() {
+//			doReturn("hase").when(conciler).getKeycloakRealmAdminPassword(anyString());
 //			when(conciler.getKeycloakRealmAdminPassword(anyString())).thenReturn("hase");
-		}
+	}
+
+	@Test
+	void shouldCallServiceAddUser() {
+
+		KeycloakUser user = KeycloakUserTestFactory.create();
+
+		conciler.reconcile(user, null);
+
+		verify(userService).addUser(user.getSpec(), KeycloakUserTestFactory.METADATA_NAMESPACE);
+	}
+
+	@Test
+	void shouldUpdateStatus() {
+
+		KeycloakUser user = KeycloakUserTestFactory.create();
+
+		conciler.reconcile(user, null);
+
+		assertThat(user.getStatus().getStatus()).isEqualTo(KeycloakUserReconciler.STATUS_OK);
+	}
 
-		@Test
-		@SneakyThrows
-		void shouldSetStatus() {
+	@Test
+	void shouldSetErrorStatusOnException() {
 
-			KeycloakUser user = KeycloakUserTestFactory.create();
+		KeycloakUser user = KeycloakUserTestFactory.create();
+		user.setMetadata(null);
 
-			conciler.reconcile(user, null);
+		conciler.reconcile(user, null);
 
-			assertThat(user.getStatus().getStatus()).contains("Updated");
-		}
+		assertThat(user.getStatus().getStatus()).isEqualTo(KeycloakUserReconciler.STATUS_ERROR);
 	}
 }
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java
new file mode 100644
index 0000000..a9491af
--- /dev/null
+++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java
@@ -0,0 +1,74 @@
+package de.ozgcloud.operator.keycloak.user;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status.Family;
+import javax.ws.rs.core.Response.StatusType;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.ozgcloud.operator.keycloak.KeycloakClient;
+import de.ozgcloud.operator.keycloak.KeycloakException;
+
+class KeycloakUserRemoteServiceTest {
+
+	@Spy
+	@InjectMocks
+	private KeycloakUserRemoteService userRemoteService;
+
+	@Mock
+	private KeycloakClient keycloakClient;
+	@Mock
+	private Keycloak keycloak;
+	@Mock
+	private RealmResource realmResource;
+	@Mock
+	private UsersResource usersResource;
+	@Mock
+	private Response response;
+	@Mock
+	private StatusType statusType;
+
+	private final static UserRepresentation USER_REPRESENTATION = UserRepresentationTestFactory.create();
+	private final static String REALM = "TestRealm";
+
+	@BeforeEach
+	void init() {
+		when(keycloakClient.getKeycloak()).thenReturn(keycloak);
+		when(keycloak.realm(REALM)).thenReturn(realmResource);
+		when(realmResource.users()).thenReturn(usersResource);
+		when(usersResource.create(USER_REPRESENTATION)).thenReturn(response);
+		when(response.getStatusInfo()).thenReturn(statusType);
+		when(statusType.getFamily()).thenReturn(Family.SUCCESSFUL);
+	}
+
+	@Test
+	void shouldCallKeycloakClient() {
+
+		when(statusType.getFamily()).thenReturn(Family.SUCCESSFUL);
+
+		userRemoteService.createUser(USER_REPRESENTATION, REALM);
+
+		verify(keycloakClient).getKeycloak();
+	}
+
+	@Test
+	void shouldThrowOnResponseError() {
+
+		when(statusType.getFamily()).thenReturn(Family.SERVER_ERROR);
+
+		assertThrows(KeycloakException.class, () ->
+			userRemoteService.createUser(USER_REPRESENTATION, REALM)
+		);
+	}
+}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java
new file mode 100644
index 0000000..1587a8f
--- /dev/null
+++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java
@@ -0,0 +1,46 @@
+package de.ozgcloud.operator.keycloak.user;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+class KeycloakUserServiceTest {
+
+	private static final String TEST_NAMESPACE = "TestNamespace";
+
+	@Spy
+	@InjectMocks
+	private KeycloakUserService userService;
+
+	@Mock
+	private KeycloakUserRemoteService userRemoteService;
+
+	@Mock
+	private KeycloakUserMapper userMapper;
+
+	@Test
+	void shouldCallUserMapper() {
+
+		var testUser = KeycloakUserSpecTestFactory.create();
+
+		userService.addUser(testUser, TEST_NAMESPACE);
+
+		verify(userMapper).toUserRepresentation(testUser);
+	}
+
+	@Test
+	void shouldCallUserRemoteService() {
+
+		var userRepresentation = UserRepresentationTestFactory.create();
+		when(userMapper.toUserRepresentation(any())).thenReturn(userRepresentation);
+
+		userService.addUser(KeycloakUserSpecTestFactory.create(), TEST_NAMESPACE);
+
+		verify(userRemoteService).createUser(eq(userRepresentation), eq(TEST_NAMESPACE));
+	}
+
+}
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java
index d388671..6ed256d 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java
+++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserSpecUserTestFactory.java
@@ -5,6 +5,8 @@ import de.ozgcloud.operator.keycloak.user.KeycloakUserSpec.KeycloakUserSpecUser;
 class KeycloakUserSpecUserTestFactory {
 
 	public static final String USERNAME = "dorothea";
+	public static final String FIRSTNAME = "Dorothea";
+	public static final String LASTNAME = "Doe";
 
 	public static KeycloakUserSpecUser create() {
 		return createBuiler().build();
@@ -12,7 +14,8 @@ class KeycloakUserSpecUserTestFactory {
 
 	public static KeycloakUserSpecUser.KeycloakUserSpecUserBuilder createBuiler() {
 		return KeycloakUserSpecUser.builder()
-				.username(USERNAME);
+				.username(USERNAME)
+				.firstName(FIRSTNAME)
+				.lastName(LASTNAME);
 	}
-
 }
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserTestFactory.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserTestFactory.java
index fd3a9fb..4c71c40 100644
--- a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserTestFactory.java
+++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserTestFactory.java
@@ -1,19 +1,20 @@
 package de.ozgcloud.operator.keycloak.user;
 
-import de.ozgcloud.operator.keycloak.user.KeycloakUser;
-import de.ozgcloud.operator.keycloak.user.KeycloakUserSpec;
-import de.ozgcloud.operator.keycloak.user.KeycloakUserStatus;
-
 class KeycloakUserTestFactory {
 
 	public static final KeycloakUserStatus KEYCLOAK_USER_STATUS = KeycloakUserStatusTestFactory.create();
 
 	public static final KeycloakUserSpec KEYCLOAK_USER_SPEC = KeycloakUserSpecTestFactory.create();
 
+	public static final String METADATA_NAMESPACE = "TestNamespace";
+
 	public static KeycloakUser create() {
 		KeycloakUser keycloakUser = new KeycloakUser();
 		keycloakUser.setStatus(KEYCLOAK_USER_STATUS);
 		keycloakUser.setSpec(KEYCLOAK_USER_SPEC);
+
+		keycloakUser.getMetadata().setNamespace(METADATA_NAMESPACE);
+
 		return keycloakUser;
 	}
 
diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java b/src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java
new file mode 100644
index 0000000..022c856
--- /dev/null
+++ b/src/test/java/de/ozgcloud/operator/keycloak/user/UserRepresentationTestFactory.java
@@ -0,0 +1,18 @@
+package de.ozgcloud.operator.keycloak.user;
+
+import org.keycloak.representations.idm.UserRepresentation;
+
+public class UserRepresentationTestFactory {
+
+	public static String USERNAME = KeycloakUserSpecUserTestFactory.USERNAME;
+	public static String FIRSTNAME = KeycloakUserSpecUserTestFactory.FIRSTNAME;
+	public static String LASTNAME = KeycloakUserSpecUserTestFactory.LASTNAME;
+
+	public static UserRepresentation create() {
+		UserRepresentation user = new UserRepresentation();
+		user.setUsername(USERNAME);
+		user.setFirstName(FIRSTNAME);
+		user.setLastName(LASTNAME);
+		return user;
+	}
+}
diff --git a/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 0000000..79b126e
--- /dev/null
+++ b/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1 @@
+org.mockito.junit.jupiter.MockitoExtension
\ No newline at end of file
diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000..b059a65
--- /dev/null
+++ b/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.extensions.autodetection.enabled=true
\ No newline at end of file
-- 
GitLab