diff --git a/pom.xml b/pom.xml index 86a4b89..a22f338 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ dev.vality org-management-proto - 1.21-23aa5d8 + 1.22-4a6c2e7 dev.vality diff --git a/src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java b/src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java index c48adff..8b9f2ff 100644 --- a/src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java +++ b/src/main/java/dev/vality/orgmanager/converter/AdminManagementConverter.java @@ -9,6 +9,7 @@ import dev.vality.orgmanagement.OrganizationStatus; import dev.vality.orgmanagement.RoleAssignment; import dev.vality.orgmanagement.RoleScope; +import dev.vality.orgmanagement.User; import dev.vality.orgmanager.entity.InvitationEntity; import dev.vality.orgmanager.entity.MemberEntity; import dev.vality.orgmanager.entity.MemberRoleEntity; @@ -16,6 +17,7 @@ import dev.vality.orgmanager.entity.OrganizationRoleEntity; import dev.vality.orgmanager.entity.StoredInvitationStatus; import dev.vality.orgmanager.service.dto.MemberWithRoleDto; +import dev.vality.orgmanager.service.dto.UserDto; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @@ -49,6 +51,22 @@ public Organization toOrganization(OrganizationEntity entity) { return organization; } + public User toUser(MemberEntity entity) { + return toUser(entity.getId(), entity.getEmail()); + } + + public User toUser(UserDto row) { + return toUser(row.getId(), row.getEmail()); + } + + private User toUser(String id, String email) { + User user = new User(id); + if (email != null) { + user.setEmail(email); + } + return user; + } + public Member toMember(MemberEntity entity, String organizationId) { List roles = collectionOrEmpty(entity.getRoles()).stream() .filter(MemberRoleEntity::isActive) @@ -56,11 +74,7 @@ public Member toMember(MemberEntity entity, String organizationId) { .sorted(Comparator.comparing(MemberRoleEntity::getId)) .map(this::toMemberRole) .toList(); - Member member = new Member(entity.getId(), roles); - if (entity.getEmail() != null) { - member.setEmail(entity.getEmail()); - } - return member; + return new Member(toUser(entity), roles); } /** @@ -70,13 +84,9 @@ public Member toMember(MemberEntity entity, String organizationId) { public List toMembers(List rows) { Map members = new LinkedHashMap<>(); for (MemberWithRoleDto row : collectionOrEmpty(rows)) { - Member member = members.computeIfAbsent(row.getId(), id -> { - Member created = new Member(id, new ArrayList<>()); - if (row.getEmail() != null) { - created.setEmail(row.getEmail()); - } - return created; - }); + Member member = members.computeIfAbsent( + row.getId(), + id -> new Member(toUser(id, row.getEmail()), new ArrayList<>())); if (row.getMemberRoleId() != null) { member.getRoles().add(toMemberRole(row)); } diff --git a/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java b/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java index cc25461..1a9651a 100644 --- a/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java +++ b/src/main/java/dev/vality/orgmanager/repository/MemberRepository.java @@ -2,6 +2,7 @@ import dev.vality.orgmanager.entity.MemberEntity; import dev.vality.orgmanager.service.dto.MemberWithRoleDto; +import dev.vality.orgmanager.service.dto.UserDto; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.NativeQuery; @@ -72,6 +73,17 @@ public interface MemberRepository extends JpaRepository { " ORDER BY m.id, mr.id ") List getOrgMemberListWithRoles(String orgId, Collection memberIds); + /** + * Страница пользователей по возрастанию id + */ + @NativeQuery("SELECT m.id, " + + " m.email " + + " FROM org_manager.member m " + + " WHERE (CAST(?1 AS VARCHAR) IS NULL OR m.id > ?1) " + + " AND (CAST(?2 AS VARCHAR) IS NULL OR LOWER(m.email) = LOWER(CAST(?2 AS VARCHAR))) " + + " ORDER BY m.id ") + List getUserPage(String continuationToken, String email, Pageable pageable); + boolean existsById(String id); diff --git a/src/main/java/dev/vality/orgmanager/service/AdminManagementService.java b/src/main/java/dev/vality/orgmanager/service/AdminManagementService.java index d129124..62ad852 100644 --- a/src/main/java/dev/vality/orgmanager/service/AdminManagementService.java +++ b/src/main/java/dev/vality/orgmanager/service/AdminManagementService.java @@ -11,6 +11,7 @@ public class AdminManagementService implements AdminManagementSrv.Iface { private final AdminOrganizationService adminOrganizationService; + private final AdminUserService adminUserService; private final AdminMemberService adminMemberService; private final AdminInvitationService adminInvitationService; @@ -53,6 +54,16 @@ public Organization activateOrganization(String organizationId) return adminOrganizationService.activate(organizationId); } + @Override + public ListUsersResult listUsers(ListUsersRequest request) { + return adminUserService.list(request); + } + + @Override + public User getUser(String userId) throws UnknownUser { + return adminUserService.get(userId); + } + @Override public OrganizationRole getOrganizationRole(String organizationId, String roleId) throws OrganizationNotFound, RoleNotFound { diff --git a/src/main/java/dev/vality/orgmanager/service/AdminUserService.java b/src/main/java/dev/vality/orgmanager/service/AdminUserService.java new file mode 100644 index 0000000..ef35629 --- /dev/null +++ b/src/main/java/dev/vality/orgmanager/service/AdminUserService.java @@ -0,0 +1,58 @@ +package dev.vality.orgmanager.service; + +import dev.vality.orgmanagement.ListUsersRequest; +import dev.vality.orgmanagement.ListUsersResult; +import dev.vality.orgmanagement.UnknownUser; +import dev.vality.orgmanagement.User; +import dev.vality.orgmanager.converter.AdminManagementConverter; +import dev.vality.orgmanager.repository.MemberRepository; +import dev.vality.orgmanager.service.dto.AdminPage; +import dev.vality.orgmanager.service.dto.UserDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static dev.vality.orgmanager.service.AdminCommonService.pageLimit; +import static java.util.Objects.requireNonNullElseGet; + +/** Пользователи в административном контракте: страница пользователей и чтение по идентификатору. */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AdminUserService { + + private final MemberRepository memberRepository; + private final AdminManagementConverter converter; + + @Transactional(readOnly = true) + public ListUsersResult list(ListUsersRequest request) { + log.info("List users: request={}", request); + ListUsersRequest safeRequest = requireNonNullElseGet(request, ListUsersRequest::new); + int limit = pageLimit(safeRequest.getLimit()); + AdminPage page = AdminPage.of( + memberRepository.getUserPage( + safeRequest.getContinuationToken(), + safeRequest.getEmail(), + PageRequest.ofSize(limit + 1)), + limit, + UserDto::getId); + ListUsersResult result = new ListUsersResult(page.items().stream() + .map(converter::toUser) + .toList()); + page.continuationToken().ifPresent(result::setContinuationToken); + return result; + } + + @Transactional(readOnly = true) + public User get(String userId) throws UnknownUser { + log.info("Get user: userId={}", userId); + if (userId == null || userId.isBlank()) { + throw new UnknownUser(); + } + return memberRepository.findById(userId) + .map(converter::toUser) + .orElseThrow(UnknownUser::new); + } +} diff --git a/src/main/java/dev/vality/orgmanager/service/dto/UserDto.java b/src/main/java/dev/vality/orgmanager/service/dto/UserDto.java new file mode 100644 index 0000000..6394dec --- /dev/null +++ b/src/main/java/dev/vality/orgmanager/service/dto/UserDto.java @@ -0,0 +1,9 @@ +package dev.vality.orgmanager.service.dto; + +public interface UserDto { + + String getId(); + + String getEmail(); + +} diff --git a/src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java b/src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java index 4d91461..e0e03f9 100644 --- a/src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java +++ b/src/test/java/dev/vality/orgmanager/service/AdminManagementServiceTest.java @@ -16,6 +16,7 @@ import dev.vality.orgmanager.repository.OrganizationRoleRepository; import dev.vality.orgmanager.repository.ScopeRepository; import dev.vality.orgmanager.service.dto.MemberWithRoleDto; +import dev.vality.orgmanager.service.dto.UserDto; import dev.vality.orgmanager.util.JsonCodec; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -84,6 +85,7 @@ void setUp() { scopeRepository, converter, commonService), + new AdminUserService(memberRepository, converter), new AdminMemberService( organizationRepository, memberRepository, @@ -286,7 +288,8 @@ void shouldAddMemberAndManageRoleWithoutLastRoleRestriction() throws Exception { var role = service.assignMemberRole("org", "user", new AssignMemberRoleRequest("manager")); service.removeMemberRole("org", "user", role.getId()); - assertEquals("user@example.com", member.getEmail()); + assertEquals("user", member.getUser().getId()); + assertEquals("user@example.com", member.getUser().getEmail()); assertTrue(service.getMember("org", "user").getRoles().isEmpty()); } @@ -306,7 +309,7 @@ void shouldUpdateEmailWhenMemberIsAddedTwice() throws Exception { var member = service.addMember("org", new AddMemberRequest("user", "new@example.com")); - assertEquals("new@example.com", member.getEmail()); + assertEquals("new@example.com", member.getUser().getEmail()); assertEquals(1, organization.getMembers().size()); } @@ -487,11 +490,11 @@ void shouldListMembersIncludingOnesWithoutRoles() throws Exception { var result = service.listMembers("org", new ListMembersRequest()); var members = result.getMembers(); - assertEquals(List.of("user-1", "user-2"), members.stream().map(it -> it.getId()).toList()); + assertEquals(List.of("user-1", "user-2"), members.stream().map(it -> it.getUser().getId()).toList()); assertEquals(List.of("manager", "accountant"), members.get(0).getRoles().stream().map(it -> it.getRoleId()).toList()); assertTrue(members.get(1).getRoles().isEmpty()); - assertEquals("user-2@example.com", members.get(1).getEmail()); + assertEquals("user-2@example.com", members.get(1).getUser().getEmail()); assertFalse(result.isSetContinuationToken()); } @@ -505,7 +508,7 @@ void shouldReturnContinuationTokenWhenMoreMembersExist() throws Exception { var result = service.listMembers("org", new ListMembersRequest().setLimit(1)); - assertEquals(List.of("user-1"), result.getMembers().stream().map(it -> it.getId()).toList()); + assertEquals(List.of("user-1"), result.getMembers().stream().map(it -> it.getUser().getId()).toList()); assertEquals("user-1", result.getContinuationToken()); } @@ -521,7 +524,7 @@ void shouldReadNextMembersPageByContinuationToken() throws Exception { "org", new ListMembersRequest().setLimit(1).setContinuationToken("user-1")); - assertEquals(List.of("user-2"), result.getMembers().stream().map(it -> it.getId()).toList()); + assertEquals(List.of("user-2"), result.getMembers().stream().map(it -> it.getUser().getId()).toList()); assertFalse(result.isSetContinuationToken()); } @@ -544,6 +547,94 @@ void shouldRejectListingMembersOfUnknownOrganization() { verifyNoInteractions(memberRepository); } + @Test + void shouldListUsersWithoutOrganizationContext() throws Exception { + when(memberRepository.getUserPage(isNull(), isNull(), any(Pageable.class))).thenReturn(List.of( + userRow("user-1", "user-1@example.com"), + userRow("user-2", null))); + + var result = service.listUsers(new ListUsersRequest()); + + assertEquals(List.of("user-1", "user-2"), result.getUsers().stream().map(User::getId).toList()); + assertEquals("user-1@example.com", result.getUsers().get(0).getEmail()); + assertFalse(result.getUsers().get(1).isSetEmail()); + assertFalse(result.isSetContinuationToken()); + } + + @Test + void shouldPassEmailFilterToQuery() throws Exception { + when(memberRepository.getUserPage(isNull(), eq("user-1@example.com"), any(Pageable.class))) + .thenReturn(List.of(userRow("user-1", "user-1@example.com"))); + + var result = service.listUsers(new ListUsersRequest().setEmail("user-1@example.com")); + + assertEquals(List.of("user-1"), result.getUsers().stream().map(User::getId).toList()); + } + + @Test + void shouldReturnContinuationTokenWhenMoreUsersExist() throws Exception { + when(memberRepository.getUserPage(isNull(), isNull(), any(Pageable.class))).thenReturn(List.of( + userRow("user-1", "user-1@example.com"), + userRow("user-2", "user-2@example.com"))); + + var result = service.listUsers(new ListUsersRequest().setLimit(1)); + + assertEquals(List.of("user-1"), result.getUsers().stream().map(User::getId).toList()); + assertEquals("user-1", result.getContinuationToken()); + } + + @Test + void shouldReadNextUsersPageByContinuationToken() throws Exception { + when(memberRepository.getUserPage(eq("user-1"), isNull(), any(Pageable.class))) + .thenReturn(List.of(userRow("user-2", "user-2@example.com"))); + + var result = service.listUsers(new ListUsersRequest().setLimit(1).setContinuationToken("user-1")); + + assertEquals(List.of("user-2"), result.getUsers().stream().map(User::getId).toList()); + assertFalse(result.isSetContinuationToken()); + } + + @Test + void shouldGetUserById() throws Exception { + when(memberRepository.findById("user")).thenReturn(Optional.of(MemberEntity.builder() + .id("user") + .email("user@example.com") + .roles(new HashSet<>()) + .build())); + + var user = service.getUser("user"); + + assertEquals("user", user.getId()); + assertEquals("user@example.com", user.getEmail()); + } + + @Test + void shouldRejectUnknownUser() { + when(memberRepository.findById("user")).thenReturn(Optional.empty()); + + assertThrows(UnknownUser.class, () -> service.getUser("user")); + } + + @Test + void shouldRejectBlankUserIdWithoutQuery() { + assertThrows(UnknownUser.class, () -> service.getUser(" ")); + verifyNoInteractions(memberRepository); + } + + private UserDto userRow(String userId, String email) { + return new UserDto() { + @Override + public String getId() { + return userId; + } + + @Override + public String getEmail() { + return email; + } + }; + } + private MemberWithRoleDto memberRow(String memberId, String email, String roleId, String role) { return new MemberWithRoleDto() { @Override