test: implement extra unit test to read 85% goal
Unit test coverage / gradle-test (push) Successful in 3m36s
CI for publishing docker image / build-and-publish (push) Successful in 3m47s

This commit is contained in:
2025-07-23 14:10:14 +08:00
parent d39455eaca
commit 81b010ea66
4 changed files with 446 additions and 14 deletions
@@ -57,7 +57,6 @@ public class UserEndpointV1 extends AbstractEndpoint {
}
// this should be non-null
final var id = argId.textValue();
assert id != null;
try {
return this.keycloakAdminClient.getUserDetails(id).block();
} catch (final NoSuchElementException ex) {
@@ -262,8 +262,7 @@ public class KeycloakIdentityManagerService implements IdentityManagerService {
return domain;
})
.collect(Collectors.toSet());
newOrg.getDomains().clear();
newOrg.getDomains().addAll(domainRepresentations);
domainRepresentations.forEach(newOrg::addDomain);
}
return this.keycloakAdminClient.createOrganization(newOrg)
.map(this::toOrganizationResponse);
@@ -298,8 +297,10 @@ public class KeycloakIdentityManagerService implements IdentityManagerService {
return domain;
})
.collect(Collectors.toSet());
if (existingOrg.getDomains() != null) {
existingOrg.getDomains().clear();
existingOrg.getDomains().addAll(domainRepresentations);
}
domainRepresentations.forEach(existingOrg::addDomain);
}
return this.keycloakAdminClient.updateOrganization(orgId, existingOrg);
});
@@ -337,8 +338,9 @@ public class KeycloakIdentityManagerService implements IdentityManagerService {
}
@Override
public Mono<GroupResponse> createSubGroup(final String parentGroupId,
final CreateGroupRequest request) {
public Mono<GroupResponse> createSubGroup(
final String parentGroupId, final CreateGroupRequest request
) {
GroupRepresentation newGroup = new GroupRepresentation();
newGroup.setName(request.getName());
newGroup.setAttributes(request.getAttributes());
@@ -0,0 +1,119 @@
package com.cleverthis.authservice.controller.rabbitmq.v1.user;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.cleverthis.authservice.controller.rabbitmq.exception.EndpointBadRequestException;
import com.cleverthis.authservice.controller.rabbitmq.exception.EndpointException;
import com.cleverthis.authservice.controller.rabbitmq.model.EndpointRequest;
import com.cleverthis.authservice.controller.rabbitmq.model.EndpointResponses;
import com.cleverthis.authservice.service.impl.KeycloakAdminReactiveClient;
import com.cleverthis.clevermicro.client.amqp.CleverMicroAmqpClient;
import com.cleverthis.clevermicro.client.amqp.handler.HandlerContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.keycloak.representations.idm.UserRepresentation;
import reactor.core.publisher.Mono;
class UserEndpointV1Test {
private final CleverMicroAmqpClient client = mock();
private final ObjectMapper objectMapper = spy(new ObjectMapper());
private final KeycloakAdminReactiveClient keycloakAdminClient = mock();
private UserEndpointV1 userEndpointV1;
@BeforeEach
void setup() throws IOException {
when(this.client.getHandlerManager()).thenReturn(mock());
this.userEndpointV1 = spy(new UserEndpointV1(
this.client, this.objectMapper, this.keycloakAdminClient));
}
@Test
void testHandler_MethodNotFound() {
final var req = mock(EndpointRequest.class);
when(req.getMethod()).thenReturn("method that doesn't exist");
final var ex = assertThrows(EndpointBadRequestException.class,
() -> this.userEndpointV1.handleRequest(mock(), req));
assertEquals("Method not found", ex.getMessage());
}
private EndpointRequest createRequest(String method, Map<String, ?> args) {
final String json;
try {
json = this.objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(args);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
final LinkedHashMap<String, JsonNode> convertedArgs;
try {
convertedArgs = this.objectMapper.readValue(json, new TypeReference<>() {
});
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return EndpointRequest.builder().method(method).args(convertedArgs).build();
}
@Test
void testHandler_QueryUserById() throws Exception {
final var handlerContext = mock(HandlerContext.class);
// normal path
var req = createRequest("queryUserById", Map.of("id", "test-id"));
final var returnedUser = new UserRepresentation();
returnedUser.setId("test-id");
returnedUser.setUsername("test-username");
when(this.keycloakAdminClient.getUserDetails("test-id"))
.thenReturn(Mono.just(returnedUser));
this.userEndpointV1.handleRequest(handlerContext, req);
verify(handlerContext).finish(
this.objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(
EndpointResponses.ok(returnedUser)
)
);
// missing args
req = createRequest("queryUserById", Map.of("not-id", "test-id"));
{ // scope for final copy
final var finalReq = req;
assertThrows(EndpointBadRequestException.class,
() -> this.userEndpointV1.handleRequest(handlerContext, finalReq));
}
// invalid args
req = createRequest("queryUserById", Map.of("id", 123));
{ // scope for final copy
final var finalReq = req;
assertThrows(EndpointBadRequestException.class,
() -> this.userEndpointV1.handleRequest(handlerContext, finalReq));
}
// user not found
when(this.keycloakAdminClient.getUserDetails("test-id"))
.thenReturn(Mono.error(new NoSuchElementException("test exception")));
req = createRequest("queryUserById", Map.of("id", "test-id"));
{ // scope for final copy
final var finalReq = req;
final var ex = assertThrows(EndpointException.class,
() -> this.userEndpointV1.handleRequest(handlerContext, finalReq));
assertEquals("cleverthis.clevermicro.auth.user_not_found", ex.getEndpointException());
}
}
}
@@ -1,23 +1,36 @@
package com.cleverthis.authservice.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import com.cleverthis.authservice.config.KeycloakClientConfigProperties;
import com.cleverthis.authservice.dto.TokenResponse;
import com.cleverthis.authservice.dto.VerificationResponse;
import com.cleverthis.authservice.dto.group.CreateGroupRequest;
import com.cleverthis.authservice.dto.keycloakadmin.KeycloakInvitationRequest;
import com.cleverthis.authservice.dto.organization.CreateOrganizationRequest;
import com.cleverthis.authservice.dto.organization.InviteUserRequest;
import com.cleverthis.authservice.dto.organization.UpdateOrganizationRequest;
import com.cleverthis.authservice.exception.AuthenticationException;
import com.cleverthis.authservice.exception.TokenVerificationException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.mockito.Mockito;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
class KeycloakIdentityManagerServiceTest {
@@ -246,4 +259,303 @@ class KeycloakIdentityManagerServiceTest {
.expectNext(false)
.verifyComplete();
}
@Test
void createOrganization_success() {
// Arrange
final var request = new CreateOrganizationRequest(
"Test Organization", List.of("test.org"), "", Map.of()
);
final var mockResponse = new OrganizationRepresentation();
mockResponse.setId("123");
mockResponse.setName("Test Organization");
mockResponse.addDomain(new OrganizationDomainRepresentation("test.org"));
mockResponse.setDescription("");
mockResponse.setEnabled(true);
Mockito.when(this.adminClient.createOrganization(any()))
.thenReturn(Mono.just(mockResponse));
// Act
final var result = this.service.createOrganization(request);
// Assert
StepVerifier.create(result)
.expectNextMatches(response -> "123".equals(response.getId()) &&
"Test Organization".equals(response.getName()))
.verifyComplete();
}
@Test
void getOrganizations_success() {
// Arrange
final var mockOrganizations = List.of(
new OrganizationRepresentation() {{
setId("org1");
setName("Organization 1");
addDomain(new OrganizationDomainRepresentation("domain1.org"));
}},
new OrganizationRepresentation() {{
setId("org2");
setName("Organization 2");
addDomain(new OrganizationDomainRepresentation("domain2.org"));
}}
);
Mockito.when(this.adminClient.getOrganizations())
.thenReturn(Mono.just(mockOrganizations));
// Act
final var result = this.service.getOrganizations();
// Assert
StepVerifier.create(result)
.expectNextMatches(orgList -> orgList.size() == 2
&& "Organization 1".equals(orgList.getFirst().getName())
&& "domain1.org".equals(
orgList.getFirst().getDomains().iterator().next().getName())
&& "Organization 2".equals(orgList.get(1).getName()))
.verifyComplete();
}
@Test
void getOrganizationById_success() {
// Arrange
final var orgId = "abc123";
final var orgRepresentation = new OrganizationRepresentation();
orgRepresentation.setId(orgId);
orgRepresentation.setName("Test Organization");
orgRepresentation.addDomain(new OrganizationDomainRepresentation("test.org"));
orgRepresentation.setDescription("Test Description");
orgRepresentation.setEnabled(true);
Mockito.when(this.adminClient.getOrganizationById(orgId))
.thenReturn(Mono.just(orgRepresentation));
// Act
final var result = this.service.getOrganizationById(orgId);
// Assert
StepVerifier.create(result)
.expectNextMatches(response -> "abc123".equals(response.getId())
&& "Test Organization".equals(response.getName())
&& "Test Description".equals(response.getDescription())
&& Boolean.TRUE.equals(response.getEnabled())
&& response.getDomains().iterator().next().getName().equals("test.org"))
.verifyComplete();
}
@Test
void updateOrganization_success() {
// Arrange
final var orgId = "org123";
final var updateRequest = new UpdateOrganizationRequest(
List.of("updated.org"), "Updated Description", Map.of("key", List.of("value"))
);
final var existingOrg = new OrganizationRepresentation();
existingOrg.setId(orgId);
existingOrg.setDescription("Old Description");
existingOrg.addDomain(new OrganizationDomainRepresentation("old.org"));
Mockito.when(this.adminClient.getOrganizationById(orgId))
.thenReturn(Mono.just(existingOrg));
Mockito.when(this.adminClient.updateOrganization(orgId, existingOrg))
.thenReturn(Mono.empty());
// Act
final var result = this.service.updateOrganization(orgId, updateRequest);
// Assert
StepVerifier.create(result)
.verifyComplete();
Mockito.verify(this.adminClient).updateOrganization(orgId, existingOrg);
assertEquals("Updated Description", existingOrg.getDescription());
assertEquals("value", existingOrg.getAttributes().get("key").getFirst());
assertEquals(1, existingOrg.getDomains().size());
assertEquals("updated.org", existingOrg.getDomains().iterator().next().getName());
}
@Test
void deleteOrganization_success() {
// Arrange
final var orgId = "test-org-id";
Mockito.when(this.adminClient.deleteOrganization(orgId))
.thenReturn(Mono.empty());
// Act
final var result = this.service.deleteOrganization(orgId);
// Assert
StepVerifier.create(result)
.verifyComplete();
Mockito.verify(this.adminClient).deleteOrganization(orgId);
}
@Test
void inviteUserToOrganization_success() {
// Arrange
final var orgId = "test-org-id";
final var request = new InviteUserRequest("test@example.com", "John", "Wick");
final var invitationRequest = new KeycloakInvitationRequest();
invitationRequest.setEmail(request.getEmail());
invitationRequest.setFirstName(request.getFirstName());
invitationRequest.setLastName(request.getLastName());
Mockito.when(this.adminClient.inviteUserToOrganization(any(), any()))
.thenReturn(Mono.empty());
// Act
final var result = this.service.inviteUserToOrganization(orgId, request);
// Assert
StepVerifier.create(result)
.verifyComplete();
Mockito.verify(this.adminClient).inviteUserToOrganization(orgId, invitationRequest);
}
@Test
void getOrganizationMembers_success() {
// Arrange
final var orgId = "test-org-id";
final var mockMembers = List.of(
// I asked Gemini, and this is an antipattern that no one mentions
// when I learned Java. Basically, it will create a new anonymous
// class for every instance. In this case, we have two.
// Gemini suggests we use a method to create a user and then put it in the list,
// but a dedicated method is not as compact as double brace initialization.
// This test is generated by gemini 2.5 pro. I think it's ok to use here
// because it's a unit test, and I blame keycloak to not offer a builder.
// DO NOT use this pattern in the main source set.
new MemberRepresentation() {{
setId("user1");
setUsername("user1Username");
setFirstName("User1");
setLastName("One");
setEmail("user1@example.com");
setEnabled(true);
}},
new MemberRepresentation() {{
setId("user2");
setUsername("user2Username");
setFirstName("User2");
setLastName("Two");
setEmail("user2@example.com");
setEnabled(true);
}}
);
Mockito.when(this.adminClient.getOrganizationMembers(orgId))
.thenReturn(Mono.just(mockMembers));
// Act
final var result = this.service.getOrganizationMembers(orgId);
// Assert
StepVerifier.create(result)
.expectNextMatches(members -> members.size() == 2
&& "user1".equals(members.getFirst().getId())
&& "user1@example.com".equals(members.getFirst().getEmail())
&& "user2".equals(members.get(1).getId())
&& "user2@example.com".equals(members.get(1).getEmail()))
.verifyComplete();
Mockito.verify(this.adminClient).getOrganizationMembers(orgId);
}
@Test
void addMemberToOrganization_success() {
// Arrange
final var orgId = "test-org-id";
final var userId = "test-user-id";
Mockito.when(this.adminClient.addMemberToOrganization(orgId, userId))
.thenReturn(Mono.empty());
// Act
final var result = this.service.addMemberToOrganization(orgId, userId);
// Assert
StepVerifier.create(result)
.verifyComplete();
Mockito.verify(this.adminClient).addMemberToOrganization(orgId, userId);
}
@Test
void removeMemberFromOrganization_success() {
// Arrange
final var orgId = "test-org-id";
final var userId = "test-user-id";
Mockito.when(this.adminClient.removeMemberFromOrganization(orgId, userId))
.thenReturn(Mono.empty());
// Act
final var result = this.service.removeMemberFromOrganization(orgId, userId);
// Assert
StepVerifier.create(result)
.verifyComplete();
Mockito.verify(this.adminClient).removeMemberFromOrganization(orgId, userId);
}
@Test
void createSubGroup_success() {
// Arrange
final var parentGroupId = "parent-group-id";
final var request = new CreateGroupRequest(
"Test SubGroup", "", Map.of("key", List.of("value")));
final var mockGroupRepresentation = new GroupRepresentation();
mockGroupRepresentation.setId("sub-group-id");
mockGroupRepresentation.setName(request.getName());
mockGroupRepresentation.setAttributes(request.getAttributes());
Mockito.when(this.adminClient.createSubGroup(parentGroupId, mockGroupRepresentation))
.thenReturn(Mono.empty());
// Act
final var result = this.service.createSubGroup(parentGroupId, request);
// Assert
StepVerifier.create(result)
.expectNextMatches(response -> "Test SubGroup".equals(response.getName()) &&
response.getAttributes().get("key").contains("value"))
.verifyComplete();
Mockito.verify(this.adminClient).createSubGroup(Mockito.eq(parentGroupId), Mockito.any());
}
@Test
void getGroupById_success() {
// Arrange
final var groupId = "test-group-id";
final var mockGroup = new GroupRepresentation();
mockGroup.setId(groupId);
mockGroup.setName("Test Group");
mockGroup.setPath("/test-group");
mockGroup.setDescription("Test Group Description");
mockGroup.setAttributes(Map.of("key", List.of("value1", "value2")));
Mockito.when(this.adminClient.getGroupById(groupId))
.thenReturn(Mono.just(mockGroup));
// Act
final var result = this.service.getGroupById(groupId);
// Assert
StepVerifier.create(result)
.expectNextMatches(response -> response.getId().equals(groupId) &&
response.getName().equals("Test Group") &&
response.getPath().equals("/test-group") &&
response.getDescription().equals("Test Group Description") &&
response.getAttributes().get("key").contains("value1"))
.verifyComplete();
Mockito.verify(this.adminClient).getGroupById(groupId);
}
}