test: implement unit tests for RabbitMQ RPC endpoint
Unit test coverage / gradle-test (push) Failing after 3m7s
CI for publishing docker image / build-and-publish (push) Successful in 3m43s

This commit is contained in:
2025-07-22 17:48:37 +08:00
parent ec7052ddee
commit d39455eaca
6 changed files with 1311 additions and 115 deletions
+4
View File
@@ -62,6 +62,8 @@ dependencyManagement {
} }
} }
// https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
val mockitoAgent = configurations.create("mockitoAgent")
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-webflux")
@@ -79,9 +81,11 @@ dependencies {
testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor:reactor-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
mockitoAgent("org.mockito:mockito-core") { isTransitive = false }
} }
tasks.withType<Test> { tasks.withType<Test> {
jvmArgs("-javaagent:${mockitoAgent.asPath}")
useJUnitPlatform() useJUnitPlatform()
// generate jacoco test report // generate jacoco test report
finalizedBy("jacocoTestReport") finalizedBy("jacocoTestReport")
@@ -14,7 +14,6 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestParam;
/** /**
* Abstract class for endpoint of auth-service over RabbitMQ. * Abstract class for endpoint of auth-service over RabbitMQ.
@@ -34,7 +33,7 @@ public abstract class AbstractEndpoint implements AutoCloseable {
* Initialize this abstract class. * Initialize this abstract class.
*/ */
public AbstractEndpoint( public AbstractEndpoint(
@RequestParam final CleverMicroAmqpClient client, final CleverMicroAmqpClient client,
final ObjectMapper objectMapper, final ObjectMapper objectMapper,
final String routingKey final String routingKey
) throws IOException { ) throws IOException {
@@ -51,33 +50,38 @@ public abstract class AbstractEndpoint implements AutoCloseable {
); );
this.handlerTag = client.getHandlerManager().registerHandler( this.handlerTag = client.getHandlerManager().registerHandler(
this.queueName, "", 1, this.queueName, "", 1,
ctx -> new Thread(() -> { ctx -> new Thread(() -> handleRabbitMessage(ctx)).start(),
try {
final var req = parseRequest(ctx);
if (req == null) {
throw new EndpointBadRequestException(
"Malformed request, cannot be parsed");
}
handleRequest(ctx, req);
} catch (final EndpointException ex) {
final var resp = EndpointResponses.error(ex);
reply(ctx, resp);
} catch (final Throwable t) {
log.error(
"Error when handling request for queue {}, only refill availability",
this.queueName, t
);
final var resp = EndpointResponses.internalServerError(
"Error when handling the request", t);
reply(ctx, resp);
}
}).start(),
tag -> log.info("Endpoint canceled for queue {}: handlerTag={}", tag -> log.info("Endpoint canceled for queue {}: handlerTag={}",
this.queueName, tag) this.queueName, tag)
); );
} }
private EndpointRequest parseRequest(final HandlerContext ctx) { // VisibleForTesting
void handleRabbitMessage(final HandlerContext ctx) {
try {
final var req = parseRequest(ctx);
if (req == null) {
throw new EndpointBadRequestException(
"Malformed request, cannot be parsed");
}
handleRequest(ctx, req);
} catch (final EndpointException ex) {
final var resp = EndpointResponses.error(ex);
reply(ctx, resp);
} catch (final Throwable t) {
log.error(
"Error when handling request for queue {}, only refill availability",
this.queueName, t
);
final var resp = EndpointResponses.internalServerError(
"Error when handling the request", t);
reply(ctx, resp);
}
}
// VisibleForTesting
EndpointRequest parseRequest(final HandlerContext ctx) {
InputStream reqStream = null; InputStream reqStream = null;
if (ctx.getMessageBodySingleStream().isPresent()) { if (ctx.getMessageBodySingleStream().isPresent()) {
reqStream = ctx.getMessageBodySingleStream().get(); reqStream = ctx.getMessageBodySingleStream().get();
@@ -112,7 +116,7 @@ public abstract class AbstractEndpoint implements AutoCloseable {
/** /**
* Try to reply. Refill counter if error. * Try to reply. Refill counter if error.
*/ */
protected final void reply(final HandlerContext ctx, final Object response) { protected void reply(final HandlerContext ctx, final Object response) {
final byte[] bytes; final byte[] bytes;
try { try {
bytes = this.objectMapper.writerWithDefaultPrettyPrinter() bytes = this.objectMapper.writerWithDefaultPrettyPrinter()
@@ -6,14 +6,15 @@ import com.cleverthis.authservice.dto.keycloakadmin.KeycloakInvitationRequest;
import com.cleverthis.authservice.exception.RegistrationException; import com.cleverthis.authservice.exception.RegistrationException;
import com.cleverthis.authservice.exception.ServiceUnavailableException; import com.cleverthis.authservice.exception.ServiceUnavailableException;
import com.cleverthis.authservice.exception.UserAlreadyExistsException; import com.cleverthis.authservice.exception.UserAlreadyExistsException;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MemberRepresentation; import org.keycloak.representations.idm.MemberRepresentation;
@@ -21,6 +22,7 @@ import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
/** /**
* Service to interact with Keycloak Admin API. * Service to interact with Keycloak Admin API.
@@ -46,39 +48,6 @@ public class KeycloakAdminReactiveClient {
} }
/**
* Fetches the internal UUID of a Keycloak client given its public clientId.
*
* @param publicClientId The human-readable client_id (e.g., "cleverswarm-client").
* @return Mono emitting the internal UUID string.
*/
public Mono<String> getClientInternalId(String publicClientId) {
log.debug("Fetching internal ID for client: {}", publicClientId);
return Mono.just(this.keycloak.realm(this.realm)
.clients().findByClientId(publicClientId))
.flatMap(result -> {
if (result.isEmpty()) {
log.warn("Client id '{}' not found in Keycloak", publicClientId);
return Mono.error(new ServiceUnavailableException(
"Client not found in Keycloak: " + publicClientId));
} else {
return Mono.just(result.getFirst());
}
})
.map(ClientRepresentation::getId)
.doOnSuccess(id -> log.debug("Found internal ID '{}' for client '{}'", id,
publicClientId))
.onErrorMap(t -> {
log.error("Failed to get client '{}'",
publicClientId, t);
return new ServiceUnavailableException(
"Could not fetch client details from Keycloak, clientPublicId="
+ publicClientId, t);
});
}
/** /**
* Creates a new organization in Keycloak. * Creates a new organization in Keycloak.
* *
@@ -125,8 +94,15 @@ public class KeycloakAdminReactiveClient {
* @return A Mono emitting the representation of the found organization. * @return A Mono emitting the representation of the found organization.
*/ */
public Mono<OrganizationRepresentation> getOrganizationById(String orgId) { public Mono<OrganizationRepresentation> getOrganizationById(String orgId) {
return Mono.just(this.keycloak.realm(this.realm) return Mono.create(sink -> {
.organizations().get(orgId).toRepresentation()); try {
sink.success(this.keycloak.realm(this.realm)
.organizations().get(orgId).toRepresentation());
} catch (final NotFoundException ex) {
sink.error(new NoSuchElementException(
"Organization with id " + orgId + " not exists", ex));
}
});
} }
/** /**
@@ -153,8 +129,6 @@ public class KeycloakAdminReactiveClient {
} else { } else {
sink.success(); sink.success();
} }
} catch (final Throwable t) {
sink.error(t);
} }
}); });
} }
@@ -179,8 +153,6 @@ public class KeycloakAdminReactiveClient {
} else { } else {
sink.success(); sink.success();
} }
} catch (final Throwable t) {
sink.error(t);
} }
}); });
} }
@@ -211,8 +183,6 @@ public class KeycloakAdminReactiveClient {
} else { } else {
sink.success(); sink.success();
} }
} catch (final Throwable t) {
sink.error(t);
} }
}); });
} }
@@ -224,9 +194,16 @@ public class KeycloakAdminReactiveClient {
* @return A Mono emitting a List of user representations for the members. * @return A Mono emitting a List of user representations for the members.
*/ */
public Mono<List<MemberRepresentation>> getOrganizationMembers(String orgId) { public Mono<List<MemberRepresentation>> getOrganizationMembers(String orgId) {
return Mono.just(this.keycloak.realm(this.realm) return Mono.create(sink -> {
.organizations().get(orgId) try {
.members().list(null, Integer.MAX_VALUE)); sink.success(this.keycloak.realm(this.realm)
.organizations().get(orgId)
.members().list(null, Integer.MAX_VALUE));
} catch (final NotFoundException ex) {
sink.error(new NoSuchElementException(
"Organization with id " + orgId + " not exists", ex));
}
});
} }
/** /**
@@ -253,8 +230,6 @@ public class KeycloakAdminReactiveClient {
} else { } else {
sink.success(); sink.success();
} }
} catch (final Throwable t) {
sink.error(t);
} }
}); });
} }
@@ -281,8 +256,6 @@ public class KeycloakAdminReactiveClient {
} else { } else {
sink.success(); sink.success();
} }
} catch (final Throwable t) {
sink.error(t);
} }
}); });
} }
@@ -310,8 +283,6 @@ public class KeycloakAdminReactiveClient {
} else { } else {
sink.success(); sink.success();
} }
} catch (final Throwable t) {
sink.error(t);
} }
}); });
} }
@@ -323,8 +294,12 @@ public class KeycloakAdminReactiveClient {
* @return A Mono emitting the group representation. * @return A Mono emitting the group representation.
*/ */
public Mono<GroupRepresentation> getGroupById(String groupId) { public Mono<GroupRepresentation> getGroupById(String groupId) {
return Mono.just( return Mono.create((Consumer<MonoSink<GroupRepresentation>>) sink ->
this.keycloak.realm(this.realm).groups().group(groupId).toRepresentation()); sink.success(this.keycloak.realm(this.realm)
.groups().group(groupId).toRepresentation()))
.onErrorMap(NotFoundException.class,
ex -> new NoSuchElementException(
"Group with id " + groupId + " not exists", ex));
} }
/** /**
@@ -334,9 +309,13 @@ public class KeycloakAdminReactiveClient {
* @return A Mono emitting a list of sub-group representations. * @return A Mono emitting a list of sub-group representations.
*/ */
public Mono<List<GroupRepresentation>> listSubGroups(String parentGroupId) { public Mono<List<GroupRepresentation>> listSubGroups(String parentGroupId) {
return Mono.just(this.keycloak.realm(this.realm) return Mono.create((Consumer<MonoSink<List<GroupRepresentation>>>) sink ->
.groups().group(parentGroupId) sink.success(this.keycloak.realm(this.realm)
.getSubGroups(null, Integer.MAX_VALUE, false)); .groups().group(parentGroupId)
.getSubGroups(null, Integer.MAX_VALUE, false)))
.onErrorMap(NotFoundException.class,
ex -> new NoSuchElementException(
"Group with id " + parentGroupId + " not exists", ex));
} }
/** /**
@@ -347,10 +326,14 @@ public class KeycloakAdminReactiveClient {
* @return A Mono that completes on successful update. * @return A Mono that completes on successful update.
*/ */
public Mono<Void> updateGroup(String groupId, GroupRepresentation group) { public Mono<Void> updateGroup(String groupId, GroupRepresentation group) {
return Mono.create(sink -> { return Mono
this.keycloak.realm(this.realm).groups().group(groupId).update(group); .create((Consumer<MonoSink<Void>>) sink -> {
sink.success(); this.keycloak.realm(this.realm).groups().group(groupId).update(group);
}); sink.success();
})
.onErrorMap(NotFoundException.class, ex -> new NoSuchElementException(
"Group with id " + groupId + " not exists"
));
} }
/** /**
@@ -360,10 +343,14 @@ public class KeycloakAdminReactiveClient {
* @return A Mono that completes on successful deletion. * @return A Mono that completes on successful deletion.
*/ */
public Mono<Void> deleteGroup(String groupId) { public Mono<Void> deleteGroup(String groupId) {
return Mono.create(sink -> { return Mono
this.keycloak.realm(this.realm).groups().group(groupId).remove(); .create((Consumer<MonoSink<Void>>) sink -> {
sink.success(); this.keycloak.realm(this.realm).groups().group(groupId).remove();
}); sink.success();
})
.onErrorMap(NotFoundException.class, ex -> new NoSuchElementException(
"Group with id " + groupId + " not exists"
));
} }
/** /**
@@ -375,10 +362,14 @@ public class KeycloakAdminReactiveClient {
*/ */
public Mono<Void> addMemberToGroup(String groupId, String userId) { public Mono<Void> addMemberToGroup(String groupId, String userId) {
// Keycloak API for adding a user to a group is a PUT on the user's groups link // Keycloak API for adding a user to a group is a PUT on the user's groups link
return Mono.create(sink -> { return Mono
this.keycloak.realm(this.realm).users().get(userId).joinGroup(groupId); .create((Consumer<MonoSink<Void>>) sink -> {
sink.success(); this.keycloak.realm(this.realm).users().get(userId).joinGroup(groupId);
}); sink.success();
})
.onErrorMap(ClientErrorException.class, ex -> new IllegalArgumentException(
"Failed to add user with id " + userId + " to group with id " + groupId
));
} }
/** /**
@@ -388,8 +379,11 @@ public class KeycloakAdminReactiveClient {
* @return A Mono emitting a list of user representations. * @return A Mono emitting a list of user representations.
*/ */
public Mono<List<UserRepresentation>> getGroupMembers(String groupId) { public Mono<List<UserRepresentation>> getGroupMembers(String groupId) {
return Mono.just(this.keycloak.realm(this.realm).groups().group(groupId).members()); return Mono.create((Consumer<MonoSink<List<UserRepresentation>>>) sink -> sink.success(
this.keycloak.realm(this.realm).groups().group(groupId).members()))
.onErrorMap(NotFoundException.class, ex -> new NoSuchElementException(
"Group with id " + groupId + " not exist", ex
));
} }
/** /**
@@ -400,10 +394,14 @@ public class KeycloakAdminReactiveClient {
* @return A Mono that completes when the user is removed from the group. * @return A Mono that completes when the user is removed from the group.
*/ */
public Mono<Void> removeMemberFromGroup(String groupId, String userId) { public Mono<Void> removeMemberFromGroup(String groupId, String userId) {
return Mono.create(sink -> { return Mono
this.keycloak.realm(this.realm).users().get(userId).leaveGroup(groupId); .create((Consumer<MonoSink<Void>>) sink -> {
sink.success(); this.keycloak.realm(this.realm).users().get(userId).leaveGroup(groupId);
}); sink.success();
})
.onErrorMap(ClientErrorException.class, ex -> new IllegalArgumentException(
"Failed to remove user with id " + userId + " from group with id " + groupId
));
} }
/** /**
@@ -455,8 +453,6 @@ public class KeycloakAdminReactiveClient {
"User registration failed due to Keycloak error." "User registration failed due to Keycloak error."
+ " Status: " + resp.getStatus())); + " Status: " + resp.getStatus()));
} }
} catch (final Throwable t) {
sink.error(t);
} }
}); });
@@ -468,12 +464,10 @@ public class KeycloakAdminReactiveClient {
* @throws NoSuchElementException in mono if keycloak returns 404. * @throws NoSuchElementException in mono if keycloak returns 404.
*/ */
public Mono<UserRepresentation> getUserDetails(String userId) { public Mono<UserRepresentation> getUserDetails(String userId) {
try { return Mono.create((Consumer<MonoSink<UserRepresentation>>) sink -> sink.success(
return Mono.just(this.keycloak.realm(this.realm) this.keycloak.realm(this.realm).users().get(userId).toRepresentation()))
.users().get(userId).toRepresentation()); .onErrorMap(NotFoundException.class,
} catch (final NotFoundException ex) { ex -> new NoSuchElementException("User with id " + userId + " not exists", ex));
return Mono.error(new NoSuchElementException("User with id " + userId + " not exists"));
}
} }
} }
@@ -3,10 +3,17 @@ package com.cleverthis.authservice.config;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@SpringBootTest(properties = { @ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {KeycloakClientConfigPropertiesTest.class})
@EnableConfigurationProperties({KeycloakClientConfigProperties.class})
@TestPropertySource(properties = {
"keycloak.keycloak-host=https://keycloak.example.com", "keycloak.keycloak-host=https://keycloak.example.com",
"keycloak.keycloak-admin-host=https://keycloak-admin.example.com", "keycloak.keycloak-admin-host=https://keycloak-admin.example.com",
"keycloak.realm=cm-test", "keycloak.realm=cm-test",
@@ -0,0 +1,238 @@
package com.cleverthis.authservice.controller.rabbitmq;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.cleverthis.authservice.controller.rabbitmq.exception.EndpointException;
import com.cleverthis.authservice.controller.rabbitmq.model.EndpointErrorResponse;
import com.cleverthis.authservice.controller.rabbitmq.model.EndpointOkResponse;
import com.cleverthis.authservice.controller.rabbitmq.model.EndpointRequest;
import com.cleverthis.clevermicro.client.amqp.CleverMicroAmqpClient;
import com.cleverthis.clevermicro.client.amqp.handler.HandlerContext;
import com.cleverthis.clevermicro.client.amqp.handler.HandlerSubmodule;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.BuiltinExchangeType;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
class AbstractEndpointTest {
private final CleverMicroAmqpClient client = Mockito.mock(CleverMicroAmqpClient.class);
final ObjectMapper objectMapper = Mockito.spy(new ObjectMapper());
private class TestEndpoint extends AbstractEndpoint {
TestEndpoint() throws Exception {
super(
AbstractEndpointTest.this.client,
AbstractEndpointTest.this.objectMapper,
"test-key"
);
}
// The throws is needed for mocked object to throw exception during test
@SuppressWarnings("RedundantThrows")
@Override
protected void handleRequest(
final HandlerContext ctx, final EndpointRequest request
) throws EndpointException {
}
}
private final HandlerSubmodule handlerSubmodule = Mockito.mock(HandlerSubmodule.class);
@BeforeEach
void setup() {
when(this.client.getHandlerManager()).thenReturn(this.handlerSubmodule);
}
@Test
void testInitAndClose() throws Exception {
when(this.client.getHandlerManager().registerHandler(
anyString(), eq(""), eq(1),
notNull(), notNull()
)).thenReturn("test-tag");
final var testEndpoint = new TestEndpoint();
verify(this.client.getHandlerManager()).declareExchange(
"cleverthis.clevermicro.auth.endpoints", BuiltinExchangeType.DIRECT
);
verify(this.client.getHandlerManager()).declareQueue(
"cleverthis.clevermicro.auth.endpoints.test-key",
true, false, false, Map.of()
);
verify(this.client.getHandlerManager()).bindQueue(
"cleverthis.clevermicro.auth.endpoints.test-key",
"cleverthis.clevermicro.auth.endpoints",
"test-key", Map.of()
);
testEndpoint.close();
verify(this.client.getHandlerManager()).cancelHandler("test-tag");
}
@Test
void testHandler() throws Throwable {
final var testEndpoint = Mockito.spy(new TestEndpoint());
final var handlerContext = Mockito.mock(HandlerContext.class);
// silent reply method, it will be tested separately
doNothing().when(testEndpoint).reply(eq(handlerContext), notNull());
// parse req null
doReturn(null).when(testEndpoint).parseRequest(notNull());
testEndpoint.handleRabbitMessage(handlerContext);
verify(testEndpoint).reply(eq(handlerContext), argThat(it -> {
final var resp = (EndpointErrorResponse) it;
return resp.getException().equals("cleverthis.clevermicro.bad_request")
&& resp.getMessage().equals("Malformed request, cannot be parsed");
}));
// path: parse req ok
Mockito.clearInvocations(testEndpoint);
// handle req ok
doReturn(EndpointRequest.builder().build()).when(testEndpoint).parseRequest(notNull());
doNothing().when(testEndpoint).handleRequest(notNull(), notNull());
assertDoesNotThrow(() -> testEndpoint.handleRabbitMessage(handlerContext));
verify(testEndpoint, never()).reply(notNull(), notNull());
// handle req endpoint exception
Mockito.clearInvocations(testEndpoint);
doThrow(new EndpointException("test", "message"))
.when(testEndpoint).handleRequest(notNull(), notNull());
assertDoesNotThrow(() -> testEndpoint.handleRabbitMessage(handlerContext));
verify(testEndpoint).reply(notNull(), argThat(it -> {
final var resp = (EndpointErrorResponse) it;
return resp.getException().equals("test")
&& resp.getMessage().equals("message");
}));
// handle req any exception
Mockito.clearInvocations(testEndpoint);
doThrow(new RuntimeException("test"))
.when(testEndpoint).handleRequest(notNull(), notNull());
assertDoesNotThrow(() -> testEndpoint.handleRabbitMessage(handlerContext));
verify(testEndpoint).reply(notNull(), argThat(it -> {
final var resp = (EndpointErrorResponse) it;
return resp.getException().equals("cleverthis.clevermicro.server_internal_error");
}));
}
@Test
void testParseRequest_SingleStream() throws Exception {
//noinspection resource
final var testEndpoint = new TestEndpoint();
final var handlerContext = Mockito.mock(HandlerContext.class);
final var inputStream = Mockito.mock(InputStream.class);
when(handlerContext.getMessageBodySingleStream()).thenReturn(Optional.of(inputStream));
final var req = EndpointRequest.builder().method("testMethod").build();
doReturn(req).when(this.objectMapper).readValue(inputStream, EndpointRequest.class);
final var result = testEndpoint.parseRequest(handlerContext);
assertEquals(req, result);
}
@Test
void testParseRequest_MultiStream() throws Exception {
//noinspection resource
final var testEndpoint = new TestEndpoint();
final var handlerContext = Mockito.mock(HandlerContext.class);
final var inputStream = Mockito.mock(InputStream.class);
when(handlerContext.getMessageBodyMultiStream()).thenReturn(Optional.of(
Map.of("request", inputStream)
));
final var req = EndpointRequest.builder().method("testMethod").build();
doReturn(req).when(this.objectMapper).readValue(inputStream, EndpointRequest.class);
final var result = testEndpoint.parseRequest(handlerContext);
assertEquals(req, result);
}
@Test
void testReply_SuccessfulSerialization() throws Exception {
//noinspection resource
final var testEndpoint = new TestEndpoint();
final var handlerContext = Mockito.mock(HandlerContext.class);
final var response = Map.of("key", "value");
assertDoesNotThrow(() -> testEndpoint.reply(handlerContext, response));
verify(handlerContext).finish("""
{
"key" : "value"
}
""".trim().getBytes(StandardCharsets.UTF_8));
}
@Test
void testReply_SerializationFailure() throws Exception {
//noinspection resource
final var testEndpoint = new TestEndpoint();
final var handlerContext = Mockito.mock(HandlerContext.class);
final var response = mock(EndpointOkResponse.class);
// throw error when accessing fields, causing jackson failed to serialize
when(response.getResult()).thenThrow(new RuntimeException("test"));
assertDoesNotThrow(() -> testEndpoint.reply(handlerContext, response));
verify(handlerContext, never()).finish(any());
verify(handlerContext).refillAvailability();
}
@Test
void testReply_ReplyFailure() throws Exception {
//noinspection resource
final var testEndpoint = new TestEndpoint();
final var handlerContext = Mockito.mock(HandlerContext.class);
final var response = Map.of("key", "value");
doThrow(new IOException("Reply error")).when(handlerContext).finish(notNull());
assertDoesNotThrow(() -> testEndpoint.reply(handlerContext, response));
}
@Test
void testParseRequest_InvalidRequest() throws Exception {
//noinspection resource
final var testEndpoint = new TestEndpoint();
final var handlerContext = Mockito.mock(HandlerContext.class);
final var inputStream = Mockito.mock(InputStream.class);
when(handlerContext.getMessageBodySingleStream()).thenReturn(Optional.of(inputStream));
doThrow(new IOException("test")).when(this.objectMapper)
.readValue(inputStream, EndpointRequest.class);
final var result = testEndpoint.parseRequest(handlerContext);
assertNull(result);
}
@Test
void testParseRequest_NoStream() throws Exception {
//noinspection resource
final var testEndpoint = new TestEndpoint();
final var handlerContext = Mockito.mock(HandlerContext.class);
when(handlerContext.getMessageBodySingleStream()).thenReturn(Optional.empty());
when(handlerContext.getMessageBodyMultiStream()).thenReturn(Optional.empty());
final var result = testEndpoint.parseRequest(handlerContext);
assertNull(result);
}
}
@@ -1,25 +1,974 @@
package com.cleverthis.authservice.service.impl; package com.cleverthis.authservice.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.cleverthis.authservice.config.KeycloakClientConfigProperties; import com.cleverthis.authservice.config.KeycloakClientConfigProperties;
import com.cleverthis.authservice.dto.RegistrationRequest;
import com.cleverthis.authservice.dto.keycloakadmin.KeycloakInvitationRequest;
import com.cleverthis.authservice.exception.RegistrationException;
import com.cleverthis.authservice.exception.ServiceUnavailableException;
import com.cleverthis.authservice.exception.UserAlreadyExistsException;
import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.mockito.Mockito; import org.mockito.Mockito;
class KeycloakAdminReactiveClientTest { class KeycloakAdminReactiveClientTest {
private KeycloakAdminReactiveClient adminClient; private KeycloakAdminReactiveClient adminClient;
private final Keycloak keycloak = mock(Keycloak.class);
@BeforeEach @BeforeEach
void setup() { void setup() {
final var keycloak = Mockito.mock(Keycloak.class); when(this.keycloak.realm("test-realm")).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations()).thenReturn(mock());
when(this.keycloak.realm("test-realm").groups()).thenReturn(mock());
when(this.keycloak.realm("test-realm").users()).thenReturn(mock());
final var configProperties = KeycloakClientConfigProperties.builder() final var configProperties = KeycloakClientConfigProperties.builder()
.keycloakAdminHost("http://example.com/")
.realm("test-realm") .realm("test-realm")
.adminAccountUsername("admin")
.adminAccountPassword("admin-password")
.build(); .build();
this.adminClient = Mockito.spy(new KeycloakAdminReactiveClient(keycloak, configProperties)); this.adminClient =
Mockito.spy(new KeycloakAdminReactiveClient(this.keycloak, configProperties));
} }
// TODO: implement tests @Test
void testCreateOrganization_Success() {
// Arrange
final var orgToCreate = new OrganizationRepresentation();
orgToCreate.setId("test-id");
orgToCreate.setName("Test Organization");
final var response = mock(Response.class);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily())
.thenReturn(Response.Status.Family.SUCCESSFUL);
when(response.readEntity(OrganizationRepresentation.class)).thenReturn(orgToCreate);
when(this.keycloak.realm("test-realm")
.organizations().create(orgToCreate))
.thenReturn(response);
// Act
final var result = this.adminClient.createOrganization(orgToCreate).block();
// Assert
assertNotNull(result);
assertEquals("Test Organization", result.getName());
//noinspection resource
verify(this.keycloak.realm("test-realm").organizations()).create(orgToCreate);
}
@Test
void testCreateOrganization_Failure() {
// Arrange
final var orgToCreate = new OrganizationRepresentation();
orgToCreate.setId("test-id");
orgToCreate.setName("Test Organization");
final var response = mock(Response.class);
when(response.getStatusInfo()).thenReturn(mock());
// keycloak return non-200
when(response.getStatusInfo().getFamily())
.thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.readEntity(String.class)).thenReturn("Error occurred");
when(
this.keycloak.realm("test-realm").organizations()
.create(orgToCreate))
.thenReturn(response);
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.createOrganization(orgToCreate).block());
// client read exception
when(response.getStatusInfo().getFamily())
.thenReturn(Response.Status.Family.SUCCESSFUL);
when(response.readEntity(OrganizationRepresentation.class))
.thenThrow(new ProcessingException("test"));
when(
this.keycloak.realm("test-realm").organizations()
.create(orgToCreate))
.thenReturn(response);
// Act & Assert
Assertions.assertThrows(ProcessingException.class,
() -> this.adminClient.createOrganization(orgToCreate).block());
}
@Test
void testGetOrganizations_Success() {
// Arrange
final var organization1 = new OrganizationRepresentation();
organization1.setId("org-1");
organization1.setName("Organization 1");
final var organization2 = new OrganizationRepresentation();
organization2.setId("org-2");
organization2.setName("Organization 2");
when(this.keycloak.realm("test-realm")
.organizations().list(null, Integer.MAX_VALUE))
.thenReturn(List.of(organization1, organization2));
// Act
final var result = this.adminClient.getOrganizations().block();
// Assert
assertNotNull(result);
assertEquals(2, result.size());
assertEquals("Organization 1", result.get(0).getName());
assertEquals("Organization 2", result.get(1).getName());
verify(this.keycloak.realm("test-realm").organizations()).list(null, Integer.MAX_VALUE);
}
@Test
void testGetOrganizationById_Success() {
// Arrange
final var organization = new OrganizationRepresentation();
organization.setId("org-1");
organization.setName("Test Organization");
when(this.keycloak.realm("test-realm")
.organizations().get("org-1"))
.thenReturn(mock());
when(this.keycloak.realm("test-realm")
.organizations().get("org-1").toRepresentation())
.thenReturn(organization);
// Act
final var result = this.adminClient.getOrganizationById("org-1").block();
// Assert
assertNotNull(result);
assertEquals("org-1", result.getId());
assertEquals("Test Organization", result.getName());
verify(this.keycloak.realm("test-realm").organizations().get("org-1"))
.toRepresentation();
}
@Test
void testGetOrganizationById_NotFound() {
// Arrange
when(this.keycloak.realm("test-realm")
.organizations().get("non-existing-id"))
.thenReturn(mock());
when(this.keycloak.realm("test-realm")
.organizations().get("non-existing-id").toRepresentation())
.thenThrow(new NotFoundException("Organization not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.getOrganizationById("non-existing-id").block());
verify(this.keycloak.realm("test-realm").organizations().get("non-existing-id"))
.toRepresentation();
}
@Test
void testUpdateOrganization_Success() {
// Arrange
final var orgId = "org-1";
final var orgToUpdate = new OrganizationRepresentation();
orgToUpdate.setId("org-1");
orgToUpdate.setName("Updated Organization");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).update(orgToUpdate))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL);
// Act
this.adminClient.updateOrganization(orgId, orgToUpdate).block();
// Assert
//noinspection resource
verify(this.keycloak.realm("test-realm").organizations().get(orgId)).update(orgToUpdate);
}
@Test
void testUpdateOrganization_Failure_ClientError() {
// Arrange
final var orgId = "org-1";
final var orgToUpdate = new OrganizationRepresentation();
orgToUpdate.setId("org-1");
orgToUpdate.setName("Updated Organization");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).update(orgToUpdate))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.readEntity(String.class)).thenReturn("Client error occurred");
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.updateOrganization(orgId, orgToUpdate).block());
}
@Test
void testUpdateOrganization_Failure_ProcessingException() {
// Arrange
final var orgId = "org-1";
final var orgToUpdate = new OrganizationRepresentation();
orgToUpdate.setId("org-1");
orgToUpdate.setName("Updated Organization");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).update(orgToUpdate))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SERVER_ERROR);
when(response.readEntity(String.class))
.thenThrow(new ProcessingException("Processing error"));
// Act & Assert
Assertions.assertThrows(ProcessingException.class,
() -> this.adminClient.updateOrganization(orgId, orgToUpdate).block());
}
@Test
void testDeleteOrganization_Success() {
// Arrange
final var orgId = "org-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).delete())
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL);
// Act
this.adminClient.deleteOrganization(orgId).block();
// Assert
//noinspection resource
verify(this.keycloak.realm("test-realm").organizations().get(orgId)).delete();
}
@Test
void testDeleteOrganization_Failure_ClientError() {
// Arrange
final var orgId = "org-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).delete())
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.readEntity(String.class)).thenReturn("Client error occurred");
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.deleteOrganization(orgId).block());
}
@Test
void testDeleteOrganization_Failure_ServerError() {
// Arrange
final var orgId = "org-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).delete())
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SERVER_ERROR);
when(response.readEntity(String.class)).thenReturn("Server error occurred");
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.deleteOrganization(orgId).block());
}
@Test
void testInviteUserToOrganization_Success() {
// Arrange
final var orgId = "org-1";
final var invitation = new KeycloakInvitationRequest("test@example.com", "Test", "User");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.inviteUser(invitation.getEmail(), invitation.getFirstName(),
invitation.getLastName()))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL);
// Act
this.adminClient.inviteUserToOrganization(orgId, invitation).block();
// Assert
//noinspection resource
verify(this.keycloak.realm("test-realm").organizations().get(orgId).members())
.inviteUser(invitation.getEmail(), invitation.getFirstName(),
invitation.getLastName());
}
@Test
void testInviteUserToOrganization_Failure_ClientError() {
// Arrange
final var orgId = "org-1";
final var invitation = new KeycloakInvitationRequest("test@example.com", "Test", "User");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.inviteUser(invitation.getEmail(), invitation.getFirstName(),
invitation.getLastName()))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.readEntity(String.class)).thenReturn("Client error occurred");
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.inviteUserToOrganization(orgId, invitation).block());
}
@Test
void testInviteUserToOrganization_Failure_Exception() {
// Arrange
final var orgId = "org-1";
final var invitation = new KeycloakInvitationRequest("test@example.com", "Test", "User");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.inviteUser(invitation.getEmail(), invitation.getFirstName(),
invitation.getLastName()))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SERVER_ERROR);
when(response.readEntity(String.class)).thenThrow(new RuntimeException("Unexpected error"));
// Act & Assert
Assertions.assertThrows(RuntimeException.class,
() -> this.adminClient.inviteUserToOrganization(orgId, invitation).block());
}
@Test
void testGetOrganizationMembers_Success() {
// Arrange
final var orgId = "org-1";
final var member1 = new MemberRepresentation();
member1.setId("user-1");
member1.setUsername("member1");
final var member2 = new MemberRepresentation();
member2.setId("user-2");
member2.setUsername("member2");
when(this.keycloak.realm("test-realm").organizations().get(orgId))
.thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members())
.thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId)
.members().list(null, Integer.MAX_VALUE))
.thenReturn(List.of(member1, member2));
// Act
final var result = this.adminClient.getOrganizationMembers(orgId).block();
// Assert
assertNotNull(result);
assertEquals(2, result.size());
assertEquals("user-1", result.get(0).getId());
assertEquals("user-2", result.get(1).getId());
verify(this.keycloak.realm("test-realm").organizations().get(orgId)
.members()).list(null, Integer.MAX_VALUE);
}
@Test
void testGetOrganizationMembers_Failure_NotFound() {
// Arrange
final var orgId = "non-existing-org";
when(this.keycloak.realm("test-realm").organizations().get(orgId))
.thenThrow(new NotFoundException("Organization not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.getOrganizationMembers(orgId).block());
verify(this.keycloak.realm("test-realm").organizations()).get(orgId);
}
@Test
void testAddMemberToOrganization_Success() {
// Arrange
final var orgId = "org-1";
final var userId = "user-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.addMember(userId))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL);
// Act
this.adminClient.addMemberToOrganization(orgId, userId).block();
// Assert
//noinspection resource
verify(this.keycloak.realm("test-realm").organizations().get(orgId).members())
.addMember(userId);
}
@Test
void testAddMemberToOrganization_Failure_ClientError() {
// Arrange
final var orgId = "org-1";
final var userId = "user-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.addMember(userId))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.readEntity(String.class)).thenReturn("Client error occurred");
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.addMemberToOrganization(orgId, userId).block());
}
@Test
void testAddMemberToOrganization_Failure_UnexpectedException() {
// Arrange
final var orgId = "org-1";
final var userId = "user-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.addMember(userId))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SERVER_ERROR);
when(response.readEntity(String.class)).thenThrow(new RuntimeException("Unexpected error"));
// Act & Assert
Assertions.assertThrows(RuntimeException.class,
() -> this.adminClient.addMemberToOrganization(orgId, userId).block());
}
@Test
void testRemoveMemberFromOrganization_Success() {
// Arrange
final var orgId = "org-1";
final var userId = "user-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.removeMember(userId))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL);
// Act
this.adminClient.removeMemberFromOrganization(orgId, userId).block();
// Assert
//noinspection resource
verify(this.keycloak.realm("test-realm").organizations().get(orgId).members())
.removeMember(userId);
}
@Test
void testRemoveMemberFromOrganization_Failure_ClientError() {
// Arrange
final var orgId = "org-1";
final var userId = "user-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.removeMember(userId))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.readEntity(String.class)).thenReturn("Client error occurred");
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.removeMemberFromOrganization(orgId, userId).block());
}
@Test
void testRemoveMemberFromOrganization_Failure_Exception() {
// Arrange
final var orgId = "org-1";
final var userId = "user-1";
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").organizations().get(orgId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()).thenReturn(
mock());
when(this.keycloak.realm("test-realm").organizations().get(orgId).members()
.removeMember(userId))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SERVER_ERROR);
when(response.readEntity(String.class)).thenThrow(new RuntimeException("Unexpected error"));
// Act & Assert
Assertions.assertThrows(RuntimeException.class,
() -> this.adminClient.removeMemberFromOrganization(orgId, userId).block());
}
@Test
void testCreateSubGroup_Success() {
// Arrange
final var parentGroupId = "parent-group-1";
final var subGroup = new GroupRepresentation();
subGroup.setName("Sub Group 1");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").groups().group(parentGroupId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").groups().group(parentGroupId).subGroup(subGroup))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL);
// Act
this.adminClient.createSubGroup(parentGroupId, subGroup).block();
// Assert
//noinspection resource
verify(this.keycloak.realm("test-realm").groups().group(parentGroupId)).subGroup(subGroup);
}
@Test
void testCreateSubGroup_Failure_ClientError() {
// Arrange
final var parentGroupId = "parent-group-1";
final var subGroup = new GroupRepresentation();
subGroup.setName("Sub Group 1");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").groups().group(parentGroupId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").groups().group(parentGroupId).subGroup(subGroup))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.readEntity(String.class)).thenReturn("Client error occurred");
// Act & Assert
Assertions.assertThrows(ServiceUnavailableException.class,
() -> this.adminClient.createSubGroup(parentGroupId, subGroup).block());
}
@Test
void testCreateSubGroup_Failure_Exception() {
// Arrange
final var parentGroupId = "parent-group-1";
final var subGroup = new GroupRepresentation();
subGroup.setName("Sub Group 1");
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").groups().group(parentGroupId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").groups().group(parentGroupId).subGroup(subGroup))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SERVER_ERROR);
when(response.readEntity(String.class)).thenThrow(new RuntimeException("Unexpected error"));
// Act & Assert
Assertions.assertThrows(RuntimeException.class,
() -> this.adminClient.createSubGroup(parentGroupId, subGroup).block());
}
@Test
void testGetGroupById_Success() {
// Arrange
final var groupId = "group-1";
final var group = new GroupRepresentation();
group.setId(groupId);
group.setName("Test Group");
when(this.keycloak.realm("test-realm").groups().group(groupId))
.thenReturn(mock());
when(this.keycloak.realm("test-realm").groups().group(groupId).toRepresentation())
.thenReturn(group);
// Act
final var result = this.adminClient.getGroupById(groupId).block();
// Assert
assertNotNull(result);
assertEquals("group-1", result.getId());
assertEquals("Test Group", result.getName());
verify(this.keycloak.realm("test-realm").groups().group(groupId)).toRepresentation();
}
@Test
void testGetGroupById_NotFound() {
// Arrange
final var groupId = "non-existing-group";
when(this.keycloak.realm("test-realm").groups().group(groupId))
.thenReturn(mock());
when(this.keycloak.realm("test-realm").groups().group(groupId).toRepresentation())
.thenThrow(new NotFoundException("Group not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.getGroupById(groupId).block());
verify(this.keycloak.realm("test-realm").groups().group(groupId)).toRepresentation();
}
@Test
void testListSubGroups_Success() {
// Arrange
final var parentGroupId = "parent-group-1";
final var subGroup1 = new GroupRepresentation();
subGroup1.setId("sub-group-1");
subGroup1.setName("Sub Group 1");
final var subGroup2 = new GroupRepresentation();
subGroup2.setId("sub-group-2");
subGroup2.setName("Sub Group 2");
when(this.keycloak.realm("test-realm").groups().group(parentGroupId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").groups().group(parentGroupId)
.getSubGroups(null, Integer.MAX_VALUE, false))
.thenReturn(List.of(subGroup1, subGroup2));
// Act
final var result = this.adminClient.listSubGroups(parentGroupId).block();
// Assert
assertNotNull(result);
assertEquals(2, result.size());
assertEquals("sub-group-1", result.get(0).getId());
assertEquals("sub-group-2", result.get(1).getId());
verify(this.keycloak.realm("test-realm").groups().group(parentGroupId))
.getSubGroups(null, Integer.MAX_VALUE, false);
}
@Test
void testListSubGroups_NotFound() {
// Arrange
final var parentGroupId = "non-existing-group";
when(this.keycloak.realm("test-realm").groups().group(parentGroupId))
.thenThrow(new NotFoundException("Group not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.listSubGroups(parentGroupId).block());
verify(this.keycloak.realm("test-realm").groups()).group(parentGroupId);
}
@Test
void testUpdateGroup_Success() {
// Arrange
final var groupId = "group-1";
final var groupToUpdate = new GroupRepresentation();
groupToUpdate.setId("group-1");
groupToUpdate.setName("Updated Group");
when(this.keycloak.realm("test-realm").groups().group(groupId)).thenReturn(mock());
// Act
this.adminClient.updateGroup(groupId, groupToUpdate).block();
// Assert
verify(this.keycloak.realm("test-realm").groups().group(groupId)).update(groupToUpdate);
}
@Test
void testUpdateGroup_NotFound() {
// Arrange
final var groupId = "non-existing-group";
final var groupToUpdate = new GroupRepresentation();
groupToUpdate.setId("non-existing-group");
groupToUpdate.setName("Non-existing Group");
when(this.keycloak.realm("test-realm").groups().group(groupId))
.thenThrow(new NotFoundException("Group not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.updateGroup(groupId, groupToUpdate).block());
verify(this.keycloak.realm("test-realm").groups()).group(groupId);
}
@Test
void testDeleteGroup_Success() {
// Arrange
final var groupId = "group-1";
when(this.keycloak.realm("test-realm").groups().group(groupId)).thenReturn(mock());
// Act
this.adminClient.deleteGroup(groupId).block();
// Assert
verify(this.keycloak.realm("test-realm").groups().group(groupId)).remove();
}
@Test
void testDeleteGroup_NotFound() {
// Arrange
final var groupId = "non-existing-group";
when(this.keycloak.realm("test-realm").groups().group(groupId))
.thenThrow(new NotFoundException("Group not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.deleteGroup(groupId).block());
verify(this.keycloak.realm("test-realm").groups()).group(groupId);
}
@Test
void testAddMemberToGroup_Success() {
// Arrange
final var groupId = "group-1";
final var userId = "user-1";
when(this.keycloak.realm("test-realm").users().get(userId)).thenReturn(mock());
// Act
this.adminClient.addMemberToGroup(groupId, userId).block();
// Assert
verify(this.keycloak.realm("test-realm").users().get(userId)).joinGroup(groupId);
}
@Test
void testAddMemberToGroup_ClientError() {
// Arrange
final var groupId = "group-1";
final var userId = "user-1";
final var userResource = mock(UserResource.class);
when(this.keycloak.realm("test-realm").users().get(userId)).thenReturn(userResource);
doThrow(new ClientErrorException(404)).when(userResource).joinGroup(groupId);
// Act & Assert
Assertions.assertThrows(IllegalArgumentException.class,
() -> this.adminClient.addMemberToGroup(groupId, userId).block());
}
@Test
void testGetGroupMembers_Success() {
// Arrange
final var groupId = "group-1";
final var member1 = new UserRepresentation();
member1.setId("user-1");
member1.setUsername("member1");
final var member2 = new UserRepresentation();
member2.setId("user-2");
member2.setUsername("member2");
when(this.keycloak.realm("test-realm").groups().group(groupId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").groups().group(groupId).members())
.thenReturn(List.of(member1, member2));
// Act
final var result = this.adminClient.getGroupMembers(groupId).block();
// Assert
assertNotNull(result);
assertEquals(2, result.size());
assertEquals("user-1", result.get(0).getId());
assertEquals("user-2", result.get(1).getId());
verify(this.keycloak.realm("test-realm").groups().group(groupId)).members();
}
@Test
void testGetGroupMembers_GroupNotFound() {
// Arrange
final var groupId = "non-existing-group";
when(this.keycloak.realm("test-realm").groups().group(groupId))
.thenThrow(new NotFoundException("Group not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.getGroupMembers(groupId).block());
verify(this.keycloak.realm("test-realm").groups()).group(groupId);
}
@Test
void testRemoveMemberFromGroup_Success() {
// Arrange
final var groupId = "group-1";
final var userId = "user-1";
when(this.keycloak.realm("test-realm").users().get(userId)).thenReturn(mock());
// Act
this.adminClient.removeMemberFromGroup(groupId, userId).block();
// Assert
verify(this.keycloak.realm("test-realm").users().get(userId)).leaveGroup(groupId);
}
@Test
void testRemoveMemberFromGroup_Failure_ClientError() {
// Arrange
final var groupId = "group-1";
final var userId = "user-1";
final var userResource = mock(UserResource.class);
when(this.keycloak.realm("test-realm").users().get(userId)).thenReturn(userResource);
doThrow(new ClientErrorException(404)).when(userResource).leaveGroup(groupId);
// Act & Assert
Assertions.assertThrows(IllegalArgumentException.class,
() -> this.adminClient.removeMemberFromGroup(groupId, userId).block());
}
@Test
void testRegisterUser_Success() {
// Arrange
final var registrationRequest = new RegistrationRequest(
"John", "Doe", "john@example.com", "password123"
);
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").users().create(any(UserRepresentation.class)))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL);
// Act
this.adminClient.registerUser(registrationRequest).block();
// Assert
//noinspection resource
verify(this.keycloak.realm("test-realm").users())
.create(argThat(user -> user.getUsername().equals("john@example.com")));
}
@Test
void testRegisterUser_UserAlreadyExists() {
// Arrange
final var registrationRequest = new RegistrationRequest(
"John", "Doe", "john@example.com", "password123"
);
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").users().create(any(UserRepresentation.class)))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.CLIENT_ERROR);
when(response.getStatus()).thenReturn(409);
// Act & Assert
Assertions.assertThrows(UserAlreadyExistsException.class,
() -> this.adminClient.registerUser(registrationRequest).block());
}
@Test
void testRegisterUser_ServerError() {
// Arrange
final var registrationRequest = new RegistrationRequest(
"John", "Doe", "john@example.com", "password123"
);
final var response = mock(Response.class);
when(this.keycloak.realm("test-realm").users().create(any(UserRepresentation.class)))
.thenReturn(response);
when(response.getStatusInfo()).thenReturn(mock());
when(response.getStatusInfo().getFamily()).thenReturn(Response.Status.Family.SERVER_ERROR);
when(response.readEntity(String.class)).thenReturn("Internal Server Error");
// Act & Assert
Assertions.assertThrows(RegistrationException.class,
() -> this.adminClient.registerUser(registrationRequest).block());
}
@Test
void testGetUserDetails_Success() {
// Arrange
final var userId = "user-1";
final var userRepresentation = new UserRepresentation();
userRepresentation.setId(userId);
userRepresentation.setUsername("user1");
when(this.keycloak.realm("test-realm").users().get(userId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").users().get(userId).toRepresentation())
.thenReturn(userRepresentation);
// Act
final var result = this.adminClient.getUserDetails(userId).block();
// Assert
assertNotNull(result);
assertEquals("user-1", result.getId());
assertEquals("user1", result.getUsername());
verify(this.keycloak.realm("test-realm").users().get(userId)).toRepresentation();
}
@Test
void testGetUserDetails_UserNotFound() {
// Arrange
final var userId = "non-existing-user";
when(this.keycloak.realm("test-realm").users().get(userId)).thenReturn(mock());
when(this.keycloak.realm("test-realm").users().get(userId).toRepresentation())
.thenThrow(new NotFoundException("User not found"));
// Act & Assert
Assertions.assertThrows(NoSuchElementException.class,
() -> this.adminClient.getUserDetails(userId).block());
verify(this.keycloak.realm("test-realm").users().get(userId)).toRepresentation();
}
} }