diff --git a/build.gradle b/build.gradle index 520f42ae..b454c156 100644 --- a/build.gradle +++ b/build.gradle @@ -336,7 +336,7 @@ ext['tomcat.version'] = '10.1.43' dependencies { - implementation 'uk.gov.hmcts:opal-common-lib:0.2.6' + implementation 'uk.gov.hmcts:opal-common-lib:0.3.1' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsControllerGetIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsControllerGetIntegrationTest.java index d4d229c6..fc2fe1a6 100644 --- a/src/integrationTest/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsControllerGetIntegrationTest.java +++ b/src/integrationTest/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsControllerGetIntegrationTest.java @@ -1,23 +1,5 @@ package uk.gov.hmcts.reform.opal.controllers; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static uk.gov.hmcts.opal.common.dto.ToJsonString.toPrettyJson; - -import java.time.Instant; -import java.util.Map; -import java.util.Optional; - import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -35,6 +17,26 @@ import uk.gov.hmcts.opal.common.logging.EventLoggingService; import uk.gov.hmcts.reform.opal.AbstractIntegrationTest; +import java.time.Instant; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static uk.gov.hmcts.opal.common.dto.ToJsonString.toPrettyJson; + @ActiveProfiles({"integration"}) @Slf4j(topic = "opal.UserPermissionsControllerIntegrationTest") @Sql(scripts = "classpath:db.reset/clean_test_data.sql", executionPhase = BEFORE_TEST_CLASS) @@ -370,6 +372,100 @@ void getUserState_returnsMultiplePermissions(Boolean newLogin) throws Exception verify(eventLoggingService, mode).logEvent(any(), any(), any(), any(), any(), any()); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + @DisplayName("V2 Should return 200 and full V2 user state for a user with permissions") + void getV2UserState_returnsFullState(boolean newLogin) throws Exception { + long userIdWithPermissions = 500000000L; + + MockHttpServletRequestBuilder builder = get("/v2" + URL_BASE + "/" + userIdWithPermissions + "/state") + .principal(createJwtPrincipal()); + addLoginHeader(newLogin, builder); + ResultActions actions = mockMvc.perform(builder); + + String body = actions.andReturn().getResponse().getContentAsString(); + log.info(":getUserState_whenUserHasPermissions_returns200AndCorrectPayload: Response body:\n{}", + toPrettyJson(body)); + + actions.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); + + assertThat(body).isEqualTo(objectMapper.readTree(""" + { + "user_id": 500000000, + "username": "opal-test@HMCTS.NET", + "name": null, + "status": "ACTIVE", + "version": 0, + "cache_name": null, + "domains": { + "fines": { + "business_unit_users": [ + { + "business_unit_user_id": "L065JG", + "business_unit_id": 70, + "permissions": [ + { + "permission_id": 1, + "permission_name": "Create and Manage Draft Accounts" + }, + { + "permission_id": 3, + "permission_name": "Account Enquiry" + }, + { + "permission_id": 4, + "permission_name": "Collection Order" + }, + { + "permission_id": 5, + "permission_name": "Check and Validate Draft Accounts" + }, + { + "permission_id": 6, + "permission_name": "Search and View Accounts" + } + ] + }, + { + "business_unit_user_id": "L065JG", + "business_unit_id": 70, + "permissions": [ + { + "permission_id": 1, + "permission_name": "Create and Manage Draft Accounts" + }, + { + "permission_id": 3, + "permission_name": "Account Enquiry" + }, + { + "permission_id": 4, + "permission_name": "Collection Order" + }, + { + "permission_id": 5, + "permission_name": "Check and Validate Draft Accounts" + }, + { + "permission_id": 6, + "permission_name": "Search and View Accounts" + } + ] + } + ] + } + } + } + """).toString()); + if (newLogin) { + verify(eventLoggingService).logEvent(any(), any(), any(), any(), any(), any()); + } else { + verifyNoInteractions(eventLoggingService); + } + + } + private JwtAuthenticationToken createJwtPrincipal() { return createJwtPrincipal("jjqwGAERGW43","test-user@HMCTS.NET", "Pablo"); } diff --git a/src/integrationTest/java/uk/gov/hmcts/reform/opal/repository/UserRepositoryDatabaseIntegrationTest.java b/src/integrationTest/java/uk/gov/hmcts/reform/opal/repository/UserRepositoryDatabaseIntegrationTest.java new file mode 100644 index 00000000..61eb037b --- /dev/null +++ b/src/integrationTest/java/uk/gov/hmcts/reform/opal/repository/UserRepositoryDatabaseIntegrationTest.java @@ -0,0 +1,110 @@ +package uk.gov.hmcts.reform.opal.repository; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; +import uk.gov.hmcts.reform.opal.BaseIntegrationTest; +import uk.gov.hmcts.reform.opal.entity.UserEntity; +import uk.gov.hmcts.reform.opal.mappers.UserStateMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +@ActiveProfiles({"integration"}) +@Sql(scripts = "classpath:db.reset/clean_test_data.sql", executionPhase = BEFORE_TEST_CLASS) +@Sql(scripts = "classpath:db.insertData/insert_user_state_data.sql", executionPhase = BEFORE_TEST_CLASS) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) +@DisplayName("UserRepository database integration tests") +class UserRepositoryDatabaseIntegrationTest extends BaseIntegrationTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserStateMapper mapper; + + @Autowired + ObjectMapper objectMapper; + + @Test + @DisplayName("Test UserStateV2Dto production in isolation") + void testUserStateV2DtoProductionInIsolation() throws JsonProcessingException { + UserEntity user = userRepository.findIdWithPermissions(500000000L).orElseThrow(); + UserStateV2Dto dto = mapper.toUserStateV2Dto(user); + assertThat(objectMapper.readTree(objectMapper.writeValueAsString(dto))) + .isEqualTo(objectMapper.readTree(""" + { + "user_id": 500000000, + "username": "opal-test@HMCTS.NET", + "name": null, + "status": "ACTIVE", + "version": 0, + "cache_name": null, + "domains": { + "fines": { + "business_unit_users": [ + { + "business_unit_user_id": "L065JG", + "business_unit_id": 70, + "permissions": [ + { + "permission_id": 1, + "permission_name": "Create and Manage Draft Accounts" + }, + { + "permission_id": 3, + "permission_name": "Account Enquiry" + }, + { + "permission_id": 4, + "permission_name": "Collection Order" + }, + { + "permission_id": 5, + "permission_name": "Check and Validate Draft Accounts" + }, + { + "permission_id": 6, + "permission_name": "Search and View Accounts" + } + ] + }, + { + "business_unit_user_id": "L065JG", + "business_unit_id": 70, + "permissions": [ + { + "permission_id": 1, + "permission_name": "Create and Manage Draft Accounts" + }, + { + "permission_id": 3, + "permission_name": "Account Enquiry" + }, + { + "permission_id": 4, + "permission_name": "Collection Order" + }, + { + "permission_id": 5, + "permission_name": "Check and Validate Draft Accounts" + }, + { + "permission_id": 6, + "permission_name": "Search and View Accounts" + } + ] + } + ] + } + } + } + """)); + } +} diff --git a/src/integrationTest/resources/db.insertData/insert_user_state_data.sql b/src/integrationTest/resources/db.insertData/insert_user_state_data.sql index 122c46ae..5df3388b 100644 --- a/src/integrationTest/resources/db.insertData/insert_user_state_data.sql +++ b/src/integrationTest/resources/db.insertData/insert_user_state_data.sql @@ -49,3 +49,16 @@ VALUES (112687, 'L065JG', 41), -- BU 70 gets 'Account Enquiry - Account Notes' (112683, 'L065JG', 54), -- BU 70 gets 'Account Enquiry' (112921, 'L066JG', 41), -- BU 68 gets 'Account Enquiry - Account Notes' (500001, 'L080JG', 500); -- BU 61 gets 'Collection Order' + +INSERT INTO roles (role_id, version_number, opal_domain_id, role_name, is_active, application_function_list) +VALUES (1,1, 1, 'Fines_Role_1', true, ARRAY['CREATE_MANAGE_DRAFT_ACCOUNTS', 'ACCOUNT_ENQUIRY_NOTES']), + (1,2, 1, 'Fines_Role_1', true, ARRAY['CREATE_MANAGE_DRAFT_ACCOUNTS', 'ACCOUNT_ENQUIRY']), + (2,1, 1, 'Fines_Role_2', true, ARRAY['COLLECTION_ORDER']), + (2,2, 1, 'Fines_Role_2', true, ARRAY['CHECK_VALIDATE_DRAFT_ACCOUNTS', 'SEARCH_AND_VIEW_ACCOUNTS']), + (2,3, 1, 'Fines_Role_2', true, ARRAY['COLLECTION_ORDER', 'CHECK_VALIDATE_DRAFT_ACCOUNTS', 'SEARCH_AND_VIEW_ACCOUNTS']), + (3,1, 2, 'Confiscation_Role_3', true, ARRAY['CREATE_MANAGE_DRAFT_ACCOUNTS']), + (3,2, 2, 'Confiscation_Role_3', true, ARRAY['CREATE_MANAGE_DRAFT_ACCOUNTS', 'COLLECTION_ORDER']); + +INSERT INTO business_unit_user_roles(business_unit_user_role_id, business_unit_user_id, role_id) +VALUES (1,'L065JG', 1), + (2,'L065JG', 2); diff --git a/src/main/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsV2Controller.java b/src/main/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsV2Controller.java new file mode 100644 index 00000000..0ffce82d --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsV2Controller.java @@ -0,0 +1,37 @@ +package uk.gov.hmcts.reform.opal.controllers; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; +import uk.gov.hmcts.reform.opal.service.UserPermissionsService; + +import static uk.gov.hmcts.reform.opal.util.HttpUtil.buildResponse; + +// Spring Boot 4 handles versioned endpoints better, so won't need separate controller then + +@RestController +@RequestMapping("/v2/users") +@RequiredArgsConstructor +@Slf4j(topic = "opal.UserPermissionsV2Controller") +public class UserPermissionsV2Controller { + + private static final String X_NEW_LOGIN = "X-New-Login"; + private final UserPermissionsService userPermissionsService; + + @GetMapping("/{userId}/state") + public ResponseEntity getUserStateV2( + @PathVariable Long userId, Authentication authentication, + @RequestHeader(value = X_NEW_LOGIN, required = false) Boolean newLogin) { + + log.debug(":GET:getUserState: userId: {}, new login: {}", userId, newLogin); + return buildResponse(userPermissionsService + .getUserStateV2(userId, authentication, userPermissionsService, newLogin)); + } +} diff --git a/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitEntity.java b/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitEntity.java index 01f1c246..a46d8669 100644 --- a/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitEntity.java +++ b/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitEntity.java @@ -49,8 +49,9 @@ public class BusinessUnitEntity { @JoinColumn(name = "parent_business_unit_id") private BusinessUnitEntity parentBusinessUnit; - @Column(name = "opal_domain_id") - private Short opalDomainId; + @ManyToOne + @JoinColumn(name = "opal_domain_id") + private DomainEntity domain; @Column(name = "welsh_language") private Boolean welshLanguage; diff --git a/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitUserEntity.java b/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitUserEntity.java index 3c3687e8..c945088d 100644 --- a/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitUserEntity.java +++ b/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitUserEntity.java @@ -9,6 +9,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; @@ -19,6 +20,9 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.util.HashSet; +import java.util.Set; + @Entity @Table(name = "business_unit_users") @@ -26,6 +30,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "businessUnitUserId") @XmlRootElement @@ -34,20 +39,22 @@ public class BusinessUnitUserEntity { @Id @Column(name = "business_unit_user_id", length = 6) + @EqualsAndHashCode.Include private String businessUnitUserId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "business_unit_id", nullable = false) - @EqualsAndHashCode.Exclude private BusinessUnitEntity businessUnit; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) - @EqualsAndHashCode.Exclude private UserEntity user; + @OneToMany(mappedBy = "businessUnitUser") + @Builder.Default + private Set businessUnitUserRoleList = new HashSet<>(); + public Short getBusinessUnitId() { return businessUnit.getBusinessUnitId(); } - } diff --git a/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitUserRoleEntity.java b/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitUserRoleEntity.java new file mode 100644 index 00000000..50aa8d5b --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/opal/entity/BusinessUnitUserRoleEntity.java @@ -0,0 +1,35 @@ +package uk.gov.hmcts.reform.opal.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "business_unit_user_roles") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Builder +public class BusinessUnitUserRoleEntity { + + @Id + @Column(name = "business_unit_user_role_id") + private Long businessUnitUserRoleId; + + @ManyToOne + @JoinColumn(name = "business_unit_user_id", nullable = false) + private BusinessUnitUserEntity businessUnitUser; + + @ManyToOne + @JoinColumn(name = "role_id", referencedColumnName = "role_id") + private RoleEntity role; +} diff --git a/src/main/java/uk/gov/hmcts/reform/opal/entity/DomainEntity.java b/src/main/java/uk/gov/hmcts/reform/opal/entity/DomainEntity.java new file mode 100644 index 00000000..5515cec0 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/opal/entity/DomainEntity.java @@ -0,0 +1,27 @@ +package uk.gov.hmcts.reform.opal.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "domain") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DomainEntity { + + @Id + @Column(name = "opal_domain_id") + private Short id; + + @Column(name = "opal_domain_name") + private String name; + +} diff --git a/src/main/java/uk/gov/hmcts/reform/opal/entity/RoleEntity.java b/src/main/java/uk/gov/hmcts/reform/opal/entity/RoleEntity.java new file mode 100644 index 00000000..ffda5ef2 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/reform/opal/entity/RoleEntity.java @@ -0,0 +1,43 @@ +package uk.gov.hmcts.reform.opal.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Entity +@Table(name = "v_current_roles") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RoleEntity { + + @Id + @Column(name = "role_id", nullable = false) + private Long roleId; + + @Column(name = "version_number", nullable = false) + private Long versionNumber; + + @ManyToOne + @JoinColumn(name = "opal_domain_id", nullable = false) + private DomainEntity domain; + + @Column(name = "role_name", nullable = false) + private String name; + + @Column(name = "is_active", nullable = false) + private boolean isActive; + + @Column(name = "application_function_list", nullable = false) + private List applicationFunctionList; +} diff --git a/src/main/java/uk/gov/hmcts/reform/opal/entity/UserEntity.java b/src/main/java/uk/gov/hmcts/reform/opal/entity/UserEntity.java index 362d2927..2392ae0b 100644 --- a/src/main/java/uk/gov/hmcts/reform/opal/entity/UserEntity.java +++ b/src/main/java/uk/gov/hmcts/reform/opal/entity/UserEntity.java @@ -1,6 +1,7 @@ package uk.gov.hmcts.reform.opal.entity; import java.math.BigInteger; +import java.util.List; import java.util.Optional; import com.fasterxml.jackson.annotation.JsonIdentityInfo; @@ -13,6 +14,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.persistence.Version; @@ -57,9 +59,6 @@ public class UserEntity implements Versioned { @EqualsAndHashCode.Exclude private String description; - @Column(name = "opal_domain_id") - private Short opalDomainId; - @Column(name = "status", length = 25) @Enumerated(EnumType.STRING) private UserStatus status; @@ -74,6 +73,10 @@ public class UserEntity implements Versioned { @Version private Long versionNumber; + //We have to treat this list a bag as there is no suitable id to use with @OrderColumn + @OneToMany(mappedBy = "user") + private List businessUnitUsers; + @Override public BigInteger getVersion() { return Optional.ofNullable(versionNumber).map(BigInteger::valueOf).orElse(BigInteger.ZERO); diff --git a/src/main/java/uk/gov/hmcts/reform/opal/mappers/UserStateMapper.java b/src/main/java/uk/gov/hmcts/reform/opal/mappers/UserStateMapper.java index a1ee109b..3d5ffa7f 100644 --- a/src/main/java/uk/gov/hmcts/reform/opal/mappers/UserStateMapper.java +++ b/src/main/java/uk/gov/hmcts/reform/opal/mappers/UserStateMapper.java @@ -1,19 +1,35 @@ package uk.gov.hmcts.reform.opal.mappers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import uk.gov.hmcts.opal.common.user.authorisation.client.dto.BusinessUnitUserDto; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.DomainDto; import uk.gov.hmcts.opal.common.user.authorisation.client.dto.PermissionDto; import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateDto; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; +import uk.gov.hmcts.reform.opal.authorisation.model.Permissions; import uk.gov.hmcts.reform.opal.entity.BusinessUnitUserEntity; +import uk.gov.hmcts.reform.opal.entity.BusinessUnitUserRoleEntity; +import uk.gov.hmcts.reform.opal.entity.RoleEntity; import uk.gov.hmcts.reform.opal.entity.UserEntitlementEntity; import uk.gov.hmcts.reform.opal.entity.UserEntity; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; @Mapper(componentModel = "spring", implementationName = "UserStateMapperImplementation") public interface UserStateMapper { + Logger log = LoggerFactory.getLogger(UserStateMapper.class); + /** * Main mapping method to build the complete UserStateDto. * The service now provides a pre-built list of BusinessUnitUserDto objects. @@ -26,6 +42,15 @@ public interface UserStateMapper { @Mapping(source = "userEntity.version", target = "version") UserStateDto toUserStateDto(UserEntity userEntity, List businessUnitUsers); + @Mapping(source = "userEntity.userId", target = "userId") + @Mapping(source = "userEntity.username", target = "username") + @Mapping(source = "userEntity.tokenName", target = "name") + @Mapping(target = "status", constant = "ACTIVE") + @Mapping(source = "userEntity.version", target = "version") + @Mapping(target = "cacheName", ignore = true) + @Mapping(source = "businessUnitUsers", target = "domains") + UserStateV2Dto toUserStateV2Dto(UserEntity userEntity); + /** * Helper method called by the service to map a BusinessUnitUserEntity and its associated * list of entitlements into a BusinessUnitUserDto. @@ -36,10 +61,61 @@ public interface UserStateMapper { BusinessUnitUserDto toBusinessUnitUserDto(BusinessUnitUserEntity businessUnitUser, List entitlements); + @Mapping(source = "businessUnitUserId", target = "businessUnitUserId") + @Mapping(source = "businessUnitId", target = "businessUnitId") + @Mapping(source = "businessUnitUserRoleList", target = "permissions") + BusinessUnitUserDto toBusinessUnitUserDto(BusinessUnitUserEntity businessUnitUser); + /** * Helper method to map a UserEntitlementEntity to a PermissionDto. */ @Mapping(source = "applicationFunction.applicationFunctionId", target = "permissionId") @Mapping(source = "applicationFunction.functionName", target = "permissionName") PermissionDto toPermissionDto(UserEntitlementEntity entitlementEntity); + + default Map map(List businessUnitUsers) { + + if (businessUnitUsers == null || businessUnitUsers.isEmpty()) { + return Collections.emptyMap(); + } + + return businessUnitUsers.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(buu -> buu.getBusinessUnit().getDomain())) + .entrySet() + .stream() + .filter(entry -> entry.getKey() != null) + .collect(Collectors.toMap( + entry -> entry.getKey().getName().toLowerCase(Locale.ROOT), + entry -> DomainDto.builder() + .businessUnitUsers( + entry.getValue().stream() + .map(this::toBusinessUnitUserDto) + .toList() + ) + .build() + )); + } + + default List map(Set businessUnitUserRoleList) { + return businessUnitUserRoleList.stream() + .map(BusinessUnitUserRoleEntity::getRole) + .filter(RoleEntity::isActive) + .flatMap(role -> role.getApplicationFunctionList().stream()) + .map(this::toPermissionOrNull) + .filter(Objects::nonNull) + .distinct() + .sorted(Comparator.comparingLong((Permissions permission) -> permission.id)) + .map(permission -> new PermissionDto(permission.id, permission.description)) + .toList(); + } + + private Permissions toPermissionOrNull(String function) { + try { + return Permissions.valueOf(function); + } catch (IllegalArgumentException e) { + log.error("Permission could not be mapped: {}", function); + return null; + } + } } diff --git a/src/main/java/uk/gov/hmcts/reform/opal/repository/UserRepository.java b/src/main/java/uk/gov/hmcts/reform/opal/repository/UserRepository.java index 2a2434f6..a2b77d62 100644 --- a/src/main/java/uk/gov/hmcts/reform/opal/repository/UserRepository.java +++ b/src/main/java/uk/gov/hmcts/reform/opal/repository/UserRepository.java @@ -2,6 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import uk.gov.hmcts.reform.opal.entity.UserEntity; @@ -15,4 +16,25 @@ public interface UserRepository extends JpaRepository, Optional findByTokenSubject(String tokenSubject); + @Query(""" + select u from UserEntity u + join fetch u.businessUnitUsers buu + join fetch buu.businessUnit bu + join fetch bu.domain d + join fetch buu.businessUnitUserRoleList buurl + join fetch buurl.role r + where u.userId = ?1 + """) + Optional findIdWithPermissions(Long id); + + @Query(""" + select u from UserEntity u + join fetch u.businessUnitUsers buu + join fetch buu.businessUnit bu + join fetch bu.domain d + join fetch buu.businessUnitUserRoleList buurl + join fetch buurl.role r + where u.tokenSubject = ?1 + """) + Optional findByTokenSubjectWithPermissions(String subject); } diff --git a/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsProxy.java b/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsProxy.java index 84b07715..7a1089c6 100644 --- a/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsProxy.java +++ b/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsProxy.java @@ -2,6 +2,7 @@ import org.springframework.security.core.Authentication; import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateDto; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; import uk.gov.hmcts.reform.opal.dto.UserDto; import uk.gov.hmcts.reform.opal.entity.UserEntity; @@ -10,12 +11,21 @@ public interface UserPermissionsProxy { UserEntity getUser(String token); + UserEntity getUserV2(Long userId); + + UserEntity getUserV2(String token); + Long getUserId(Authentication authentication, UserPermissionsProxy proxy); UserStateDto getUserState(Authentication authentication, UserPermissionsProxy proxy, Boolean newLogin); UserStateDto getUserState(Long userId, Authentication authentication, UserPermissionsProxy proxy, Boolean newLogin); + UserStateV2Dto getUserStateV2(Authentication authentication, UserPermissionsProxy proxy, Boolean newLogin); + + UserStateV2Dto getUserStateV2(Long userId, Authentication authentication, UserPermissionsProxy proxy, + Boolean newLogin); + UserStateDto buildUserState(UserEntity user); UserDto updateUser(String authHeaderValue, UserPermissionsProxy proxy, String ifMatch); diff --git a/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsService.java b/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsService.java index e8ede566..036329a3 100644 --- a/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsService.java +++ b/src/main/java/uk/gov/hmcts/reform/opal/service/UserPermissionsService.java @@ -27,6 +27,7 @@ import uk.gov.hmcts.opal.common.user.authentication.service.AccessTokenService; import uk.gov.hmcts.opal.common.user.authorisation.client.dto.BusinessUnitUserDto; import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateDto; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; import uk.gov.hmcts.reform.opal.dto.UserDto; import uk.gov.hmcts.reform.opal.entity.BusinessUnitUserEntity; import uk.gov.hmcts.reform.opal.entity.UserEntitlementEntity; @@ -54,7 +55,7 @@ public class UserPermissionsService implements UserPermissionsProxy { private final BusinessUnitUserRepository businessUnitUserRepository; private final UserEntitlementRepository userEntitlementRepository; private final UserRepository userRepository; - private final UserStateMapper userStateMapperImplementation; + private final UserStateMapper userStateMapper; private final UserMapper userMapper; private final AccessTokenService tokenService; private final SecurityEventLoggingService securityEventLoggingService; @@ -95,6 +96,42 @@ public UserStateDto getUserState(Long userId, Authentication authentication, Use } } + @Transactional(readOnly = true) + public UserStateV2Dto getUserStateV2(Authentication authentication, UserPermissionsProxy proxy, Boolean newLogin) { + Jwt jwt = getJwtToken(authentication); + String subject = extractSubject(jwt); + UserEntity user = proxy.getUserV2(subject); + + String username = extractClaim(jwt, PREFERRED_USERNAME_CLAIM); + compare(username, user.getUsername(), user.getUserId(), "Preferred Username mismatch:", user); + String name = extractClaim(jwt, NAME_CLAIM); + compare(name, user.getTokenName(), user.getUserId(), "Name mismatch:", user); + + log.debug(":getUserState: found User: {}", username); + UserStateV2Dto dto = userStateMapper.toUserStateV2Dto(user); + if (Optional.ofNullable(newLogin).orElse(false)) { + logUserAuthenticationEvent(user.getUserId()); + } + return dto; + } + + @Transactional(readOnly = true) + public UserStateV2Dto getUserStateV2(Long userId, Authentication authentication, UserPermissionsProxy proxy, + Boolean newLogin) { + log.debug(":getUserState: userId: {}", userId); + if (userId == 0) { + return proxy.getUserStateV2(authentication, proxy, newLogin); + } else { + UserEntity user = proxy.getUserV2(userId); + UserStateV2Dto dto = userStateMapper.toUserStateV2Dto(user); + if (Optional.ofNullable(newLogin).orElse(false)) { + Long clientUserId = proxy.getUserId(authentication, proxy); + logUserAuthenticationEvent(clientUserId); + } + return dto; + } + } + @Transactional(readOnly = true) public Long getUserId(Authentication authentication, UserPermissionsProxy proxy) { Jwt jwt = getJwtToken(authentication); @@ -121,13 +158,13 @@ public UserStateDto buildUserState(UserEntity user) { List businessUnitUsers = businessUnitUserRepository .findAllByUser_UserId(user.getUserId()) .stream() - .map(businessUnitUser -> userStateMapperImplementation.toBusinessUnitUserDto( + .map(businessUnitUser -> userStateMapper.toBusinessUnitUserDto( businessUnitUser, Collections.emptyList() )) .toList(); - return userStateMapperImplementation.toUserStateDto(user, businessUnitUsers); + return userStateMapper.toUserStateDto(user, businessUnitUsers); } // 3. Group entitlements by the BusinessUnitUser's ID (a String) @@ -144,12 +181,12 @@ public UserStateDto buildUserState(UserEntity user) { List buuDtos = buuMap.values().stream() .map(buu -> { List buuEntitlements = entitlementsByBuuId.get(buu.getBusinessUnitUserId()); - return userStateMapperImplementation.toBusinessUnitUserDto(buu, buuEntitlements); + return userStateMapper.toBusinessUnitUserDto(buu, buuEntitlements); }) .toList(); // 6. Pass the user entity and the BUU list to the mapper. - return userStateMapperImplementation.toUserStateDto(user, buuDtos); + return userStateMapper.toUserStateDto(user, buuDtos); } private void compare(String fromToken, String fromDb, Long userId, String reason, Versioned versioned) { @@ -171,6 +208,18 @@ public UserEntity getUser(String subject) { .orElseThrow(() -> new EntityNotFoundException("User not found with subject: " + subject)); } + @Transactional(readOnly = true) + public UserEntity getUserV2(Long userId) { + return userRepository.findIdWithPermissions(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found with id: " + userId)); + } + + @Transactional(readOnly = true) + public UserEntity getUserV2(String subject) { + return userRepository.findByTokenSubjectWithPermissions(subject) + .orElseThrow(() -> new EntityNotFoundException("User not found with subject: " + subject)); + } + @Transactional public UserDto addUser(String authHeaderValue) { log.debug(":createUser:"); diff --git a/src/main/resources/db/migration/V20260408_053__create_roles_view.sql b/src/main/resources/db/migration/V20260408_053__create_roles_view.sql new file mode 100644 index 00000000..1e18bf61 --- /dev/null +++ b/src/main/resources/db/migration/V20260408_053__create_roles_view.sql @@ -0,0 +1,24 @@ +/** +* CGI OPAL Program +* +* MODULE : create_roles_view.sql +* +* DESCRIPTION : Create a view of roles which shows only the latest versions +* +* VERSION HISTORY: +* +* Date Author Version Nature of Change +* ---------- ----------- -------- ---------------------------------------------------------------------------- +* 31/03/2026 S Reed 1.0 PO-2816 - Update user state to use Role based access control (RBAC), domains + and cache (Opal Mode) +* +**/ + +CREATE OR REPLACE VIEW v_current_roles AS +SELECT R.* +FROM public.roles R +INNER JOIN ( + SELECT role_id, max(version_number) AS max_version_number + FROM public.roles + GROUP BY role_id +) roles_agg ON R.role_id=roles_agg.role_id AND R.version_number=roles_agg.max_version_number; diff --git a/src/test/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsV2ControllerTest.java b/src/test/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsV2ControllerTest.java new file mode 100644 index 00000000..2fbdf1a4 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/opal/controllers/UserPermissionsV2ControllerTest.java @@ -0,0 +1,48 @@ +package uk.gov.hmcts.reform.opal.controllers; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; +import uk.gov.hmcts.reform.opal.service.UserPermissionsService; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Slf4j(topic = "opal.UserPermissionsV2ControllerTest") +@ExtendWith(MockitoExtension.class) +class UserPermissionsV2ControllerTest { + + @Mock + private UserPermissionsService userPermissionsService; + + @InjectMocks + private UserPermissionsV2Controller controller; + + @Test + @DisplayName("controller.getUserStateV2 should return DTO from the service") + void testGetUserStateV2() { + // Arrange + Long userId = 123L; + Authentication authentication = mock(Authentication.class); + Boolean newLogin = true; + UserStateV2Dto dto = new UserStateV2Dto(); + when(userPermissionsService.getUserStateV2(userId, authentication, userPermissionsService, newLogin)) + .thenReturn(dto); + + // Act + ResponseEntity response = controller.getUserStateV2(userId, authentication, newLogin); + + // Assert + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(dto); + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/opal/mappers/UserStateMapperTest.java b/src/test/java/uk/gov/hmcts/reform/opal/mappers/UserStateMapperTest.java new file mode 100644 index 00000000..5e9aa08b --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/opal/mappers/UserStateMapperTest.java @@ -0,0 +1,167 @@ +package uk.gov.hmcts.reform.opal.mappers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; +import uk.gov.hmcts.reform.opal.authorisation.model.Permissions; +import uk.gov.hmcts.reform.opal.entity.BusinessUnitEntity; +import uk.gov.hmcts.reform.opal.entity.BusinessUnitUserEntity; +import uk.gov.hmcts.reform.opal.entity.BusinessUnitUserRoleEntity; +import uk.gov.hmcts.reform.opal.entity.DomainEntity; +import uk.gov.hmcts.reform.opal.entity.RoleEntity; +import uk.gov.hmcts.reform.opal.entity.UserEntity; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class UserStateMapperTest { + + private final UserStateMapper mapper = new UserStateMapperImplementation(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void toUserStateV2Dto() throws JsonProcessingException { + + //Arrange + DomainEntity fines = DomainEntity.builder().name("fines").build(); + DomainEntity confiscations = DomainEntity.builder().name("confiscations").build(); + + String permAE = Permissions.ACCOUNT_ENQUIRY.name(); + String permAEN = Permissions.ACCOUNT_ENQUIRY_NOTES.name(); + String permCVDA = Permissions.CHECK_VALIDATE_DRAFT_ACCOUNTS.name(); + String permCO = Permissions.COLLECTION_ORDER.name(); + String permSAVA = Permissions.SEARCH_AND_VIEW_ACCOUNTS.name(); + String permBadName = "BAD_NAME"; + + RoleEntity role1 = buildRole("role1", List.of(permAE, permBadName, permAEN), true); + RoleEntity role2 = buildRole("role2", List.of(permAE, permCVDA, permCO), true); + RoleEntity role3inactive = buildRole("role3inactive", List.of(permSAVA, permCO), false); + RoleEntity role4 = buildRole("role4", List.of(permCO, permAEN), true); + RoleEntity role5 = buildRole("role5", List.of(permSAVA, permAE), true); + + List businessUnitUserEntityList = List.of( + buildBusinessUnitUserEntity("ABC123", fines, (short) 41, Set.of(role1, role2)), + buildBusinessUnitUserEntity("DEF456", fines, (short) 42, Set.of(role3inactive, role4)), + buildBusinessUnitUserEntity("GHI789", confiscations, (short) 51, Set.of(role5)) + ); + + UserEntity user = UserEntity.builder() + .userId(123L) + .tokenName("token") + .tokenSubject("subject") + .username("username") + .businessUnitUsers(businessUnitUserEntityList) + .versionNumber(321L) + .build(); + + + //Act + UserStateV2Dto dto = mapper.toUserStateV2Dto(user); + + //Assert + String expected = """ + { + "user_id": 123, + "username": "username", + "name": "token", + "status": "ACTIVE", + "version": 321, + "cache_name": null, + "domains": { + "confiscations": { + "business_unit_users": [ + { + "business_unit_user_id": "GHI789", + "business_unit_id": 51, + "permissions": [ + { + "permission_id": 3, + "permission_name": "Account Enquiry" + }, + { + "permission_id": 6, + "permission_name": "Search and View Accounts" + } + ] + } + ] + }, + "fines": { + "business_unit_users": [ + { + "business_unit_user_id": "ABC123", + "business_unit_id": 41, + "permissions": [ + { + "permission_id": 2, + "permission_name": "Account Enquiry - Account Notes" + }, + { + "permission_id": 3, + "permission_name": "Account Enquiry" + }, + { + "permission_id": 4, + "permission_name": "Collection Order" + }, + { + "permission_id": 5, + "permission_name": "Check and Validate Draft Accounts" + } + ] + }, + { + "business_unit_user_id": "DEF456", + "business_unit_id": 42, + "permissions": [ + { + "permission_id": 2, + "permission_name": "Account Enquiry - Account Notes" + }, + { + "permission_id": 4, + "permission_name": "Collection Order" + } + ] + } + ] + } + } + } + """; + + assertThat(objectMapper.readTree(objectMapper.writeValueAsString(dto))) + .isEqualTo(objectMapper.readTree(expected)); + assertThat(expected).doesNotContain(permBadName); + + } + + private RoleEntity buildRole(String name, List permNames, boolean active) { + return RoleEntity.builder() + .name(name) + .applicationFunctionList(permNames) + .isActive(active) + .build(); + } + + private BusinessUnitUserEntity buildBusinessUnitUserEntity(String id, + DomainEntity domain, + Short businessUnitId, + Set roles) { + BusinessUnitEntity bu = BusinessUnitEntity.builder().domain(domain).businessUnitId(businessUnitId).build(); + BusinessUnitUserEntity buu = BusinessUnitUserEntity.builder() + .businessUnitUserId(id) + .businessUnit(bu) + .build(); + for (RoleEntity role : roles) { + buu.getBusinessUnitUserRoleList().add( + BusinessUnitUserRoleEntity.builder().role(role).businessUnitUser(buu).build() + ); + } + return buu; + } +} diff --git a/src/test/java/uk/gov/hmcts/reform/opal/service/UserPermissionsServiceTest.java b/src/test/java/uk/gov/hmcts/reform/opal/service/UserPermissionsServiceTest.java index 5cb103cf..708163a1 100644 --- a/src/test/java/uk/gov/hmcts/reform/opal/service/UserPermissionsServiceTest.java +++ b/src/test/java/uk/gov/hmcts/reform/opal/service/UserPermissionsServiceTest.java @@ -378,4 +378,5 @@ private JwtAuthenticationToken createJwtAuthenticatedToken() { return new JwtAuthenticationToken(jwt); } + } diff --git a/src/test/java/uk/gov/hmcts/reform/opal/service/UserPermissionsServiceV2Test.java b/src/test/java/uk/gov/hmcts/reform/opal/service/UserPermissionsServiceV2Test.java new file mode 100644 index 00000000..9a440198 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/reform/opal/service/UserPermissionsServiceV2Test.java @@ -0,0 +1,311 @@ +package uk.gov.hmcts.reform.opal.service; + +import jakarta.persistence.EntityNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.jaas.JaasAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.server.ResponseStatusException; +import uk.gov.hmcts.opal.common.logging.SecurityEventLoggingService; +import uk.gov.hmcts.opal.common.user.authentication.service.AccessTokenService; +import uk.gov.hmcts.opal.common.user.authentication.service.TokenValidator; +import uk.gov.hmcts.opal.common.user.authorisation.client.dto.UserStateV2Dto; +import uk.gov.hmcts.reform.opal.entity.UserEntity; +import uk.gov.hmcts.reform.opal.mappers.UserMapper; +import uk.gov.hmcts.reform.opal.mappers.UserMapperImpl; +import uk.gov.hmcts.reform.opal.mappers.UserStateMapper; +import uk.gov.hmcts.reform.opal.mappers.UserStateMapperImplementation; +import uk.gov.hmcts.reform.opal.repository.BusinessUnitUserRepository; +import uk.gov.hmcts.reform.opal.repository.UserEntitlementRepository; +import uk.gov.hmcts.reform.opal.repository.UserRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UserPermissionsServiceV2Test { + + @Mock + private UserEntitlementRepository userEntitlementRepository; + + @Mock + private BusinessUnitUserRepository businessUnitUserRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private UserStateMapper userStateMapper = new UserStateMapperImplementation(); + + @Mock + private UserMapper userMapper = new UserMapperImpl(); + + @Mock + private AccessTokenService tokenService; + + @Mock + private TokenValidator tokenValidator; + + @Mock + private SecurityEventLoggingService securityEventLoggingService; + + @Mock + private Jwt jwt; + + @Mock + private UserPermissionsProxy proxy; + + @InjectMocks + private UserPermissionsService service; + + private static final long USER_ID = 42L; + private static final String TOKEN_PREFERRED_USERNAME = "opal-user@hmcts.net"; + private static final String TOKEN_NAME = "John Smith"; + private static final String TOKEN_SUBJECT = "hcv732JFVWhf3Fd"; + + private UserEntity userEntity; + private UserStateV2Dto dto; + + @BeforeEach + void setUp() { + userEntity = UserEntity.builder() + .userId(USER_ID) + .username(TOKEN_PREFERRED_USERNAME) + .tokenName(TOKEN_NAME) + .tokenSubject(TOKEN_SUBJECT) + .versionNumber(4L) + .build(); + dto = UserStateV2Dto.builder().build(); + } + + @Test + void getUserV2_longReturnsUserWhenFound() { + // Arrange + when(userRepository.findIdWithPermissions(USER_ID)).thenReturn(Optional.of(userEntity)); + + // Act + UserEntity result = service.getUserV2(USER_ID); + + // Assert + assertEquals(userEntity, result); + verify(userRepository).findIdWithPermissions(USER_ID); + } + + @Test + void getUserV2_longThrowsWhenUserMissing() { + // Arrange + when(userRepository.findIdWithPermissions(USER_ID)).thenReturn(Optional.empty()); + + // Act & Assert + EntityNotFoundException ex = assertThrows( + EntityNotFoundException.class, + () -> service.getUserV2(USER_ID) + ); + assertEquals("User not found with id: " + USER_ID, ex.getMessage()); + } + + @Test + void getUserV2_stringReturnsUserWhenFound() { + // Arrange + when(userRepository.findByTokenSubjectWithPermissions(TOKEN_SUBJECT)).thenReturn(Optional.of(userEntity)); + + // Act + UserEntity result = service.getUserV2(TOKEN_SUBJECT); + + // Assert + assertEquals(userEntity, result); + } + + @Test + void getUserV2_stringThrowsWhenUserMissing() { + // Arrange + when(userRepository.findByTokenSubjectWithPermissions(TOKEN_SUBJECT)).thenReturn(Optional.empty()); + + // Act & Assert + EntityNotFoundException ex = assertThrows( + EntityNotFoundException.class, + () -> service.getUserV2(TOKEN_SUBJECT) + ); + assertEquals("User not found with subject: " + TOKEN_SUBJECT, ex.getMessage()); + } + + @Test + void getUserStateV2_NewLogin() { + + // Arrange + JwtAuthenticationToken authentication = mock(JwtAuthenticationToken.class); + when(authentication.getToken()).thenReturn(jwt); + when(jwt.getSubject()).thenReturn(TOKEN_SUBJECT); + when(proxy.getUserV2(TOKEN_SUBJECT)).thenReturn(userEntity); + when(jwt.getClaimAsString("preferred_username")).thenReturn(TOKEN_PREFERRED_USERNAME); + when(jwt.getClaimAsString("name")).thenReturn(TOKEN_NAME); + when(userStateMapper.toUserStateV2Dto(userEntity)).thenReturn(dto); + + // Act + UserStateV2Dto result = service.getUserStateV2(authentication, proxy, true); + + // Assert + assertThat(result).isEqualTo(dto); + verify(securityEventLoggingService).logEvent( + eq("User Authentication"), + eq("Success"), + isNull(), + eq("Authentication"), + any(), + argThat(data -> + data != null + && data.size() == 1 + && Long.valueOf(USER_ID).equals(data.get("UserIdentifier")) + ) + ); + } + + @Test + void getUserStateV2_NotNewLogin() { + + // Arrange + JwtAuthenticationToken authentication = mock(JwtAuthenticationToken.class); + when(authentication.getToken()).thenReturn(jwt); + when(jwt.getSubject()).thenReturn(TOKEN_SUBJECT); + when(proxy.getUserV2(TOKEN_SUBJECT)).thenReturn(userEntity); + when(jwt.getClaimAsString("preferred_username")).thenReturn(TOKEN_PREFERRED_USERNAME); + when(jwt.getClaimAsString("name")).thenReturn(TOKEN_NAME); + when(userStateMapper.toUserStateV2Dto(userEntity)).thenReturn(dto); + + // Act + UserStateV2Dto result = service.getUserStateV2(authentication, proxy, false); + + // Assert + assertThat(result).isEqualTo(dto); + verifyNoInteractions(securityEventLoggingService); + } + + @Test + void getUserStateV2_NewLogin_NotJwtAuthenticationToken() { + + // Arrange + JaasAuthenticationToken authentication = mock(JaasAuthenticationToken.class); + + // Act & Assert + ResponseStatusException ex = assertThrows( + ResponseStatusException.class, + () -> service.getUserStateV2(authentication, proxy, true) + ); + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + assertEquals("401 UNAUTHORIZED \"Authentication Token not of type Jwt.\"", ex.getMessage()); + } + + @Test + void getUserStateV2_SubjectClaimMissing() { + + // Arrange + JwtAuthenticationToken authentication = mock(JwtAuthenticationToken.class); + when(authentication.getToken()).thenReturn(jwt); + when(jwt.getSubject()).thenReturn(null); + + // Act & Assert + ResponseStatusException ex = assertThrows( + ResponseStatusException.class, + () -> service.getUserStateV2(authentication, proxy, true) + ); + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + assertEquals("401 UNAUTHORIZED \"Subject not found.\"", ex.getMessage()); + } + + @Test + void getUserStateV2_PreferredUsernameClaimMissing() { + + // Arrange + JwtAuthenticationToken authentication = mock(JwtAuthenticationToken.class); + when(authentication.getToken()).thenReturn(jwt); + when(jwt.getSubject()).thenReturn(TOKEN_SUBJECT); + when(proxy.getUserV2(TOKEN_SUBJECT)).thenReturn(userEntity); + when(jwt.getClaimAsString("preferred_username")).thenReturn(null); + + // Act & Assert + ResponseStatusException ex = assertThrows( + ResponseStatusException.class, + () -> service.getUserStateV2(authentication, proxy, true) + ); + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + assertEquals("401 UNAUTHORIZED \"Claim not found: preferred_username\"", ex.getMessage()); + } + + @Test + void getUserStateV2IdMethod_NewLoginWhenIdIsNonZero() { + + // Arrange + Long clientUserId = 123L; + Authentication authentication = mock(JwtAuthenticationToken.class); + when(proxy.getUserV2(USER_ID)).thenReturn(userEntity); + when(userStateMapper.toUserStateV2Dto(userEntity)).thenReturn(dto); + when(proxy.getUserId(authentication, proxy)).thenReturn(clientUserId); + + // Act + UserStateV2Dto result = service.getUserStateV2(USER_ID, authentication, proxy, true); + + // Assert + assertThat(result).isEqualTo(dto); + verify(securityEventLoggingService).logEvent( + eq("User Authentication"), + eq("Success"), + isNull(), + eq("Authentication"), + any(), + argThat(data -> + data != null + && data.size() == 1 + && clientUserId.equals(data.get("UserIdentifier")) + ) + ); + } + + @Test + void getUserStateV2IdMethod_NonNewLoginWhenIdIsNonZero() { + + // Arrange + Authentication authentication = mock(JwtAuthenticationToken.class); + when(proxy.getUserV2(USER_ID)).thenReturn(userEntity); + when(userStateMapper.toUserStateV2Dto(userEntity)).thenReturn(dto); + + // Act + UserStateV2Dto result = service.getUserStateV2(USER_ID, authentication, proxy, false); + + // Assert + assertThat(result).isEqualTo(dto); + verifyNoInteractions(securityEventLoggingService); + } + + @Test + void getUserStateV2IdMethod_WhenIdIsZero() { + + // Arrange + Authentication authentication = mock(JwtAuthenticationToken.class); + when(proxy.getUserStateV2(authentication, proxy, true)).thenReturn(dto); + + // Act + UserStateV2Dto result = service.getUserStateV2(0L, authentication, proxy, true); + + // Assert + assertThat(result).isEqualTo(dto); + verifyNoInteractions(securityEventLoggingService); + } +}