mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
Feat/namespace member display username (#236)
* feat: display username and email in namespace member management MemberResponse DTO now includes displayName and email fields. NamespacePortalQueryAppService batch-loads UserAccount data via findByIdIn to avoid N+1 queries. Frontend member table shows username (with userId as fallback subtitle) and email columns instead of raw user IDs. * test(namespace): add displayName/email assertions and new test coverage - Controller: add displayName and email assertions to addMember and updateMemberRole tests - QueryAppService: add listMembers tests for batch user enrichment and null degradation - CommandAppService: add addMember/updateMemberRole tests for displayName/email population and graceful degradation Addresses PR #236 review feedback comments: - Issue 2: Controller tests now assert displayName and email from mocked UserAccount - Issue 3: Service layer tests now cover batch query and null user degradation
This commit is contained in:
parent
010c1a4e46
commit
739e21e0fd
11 changed files with 236 additions and 11 deletions
|
|
@ -2,6 +2,7 @@ package com.iflytek.skillhub.dto;
|
|||
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
|
@ -9,6 +10,8 @@ public record MemberResponse(
|
|||
Long id,
|
||||
Long namespaceId,
|
||||
String userId,
|
||||
String displayName,
|
||||
String email,
|
||||
NamespaceRole role,
|
||||
Instant createdAt,
|
||||
Instant updatedAt
|
||||
|
|
@ -18,6 +21,21 @@ public record MemberResponse(
|
|||
member.getId(),
|
||||
member.getNamespaceId(),
|
||||
member.getUserId(),
|
||||
null,
|
||||
null,
|
||||
member.getRole(),
|
||||
member.getCreatedAt(),
|
||||
member.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public static MemberResponse from(NamespaceMember member, UserAccount user) {
|
||||
return new MemberResponse(
|
||||
member.getId(),
|
||||
member.getNamespaceId(),
|
||||
member.getUserId(),
|
||||
user != null ? user.getDisplayName() : null,
|
||||
user != null ? user.getEmail() : null,
|
||||
member.getRole(),
|
||||
member.getCreatedAt(),
|
||||
member.getUpdatedAt()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.MemberResponse;
|
||||
import com.iflytek.skillhub.dto.MessageResponse;
|
||||
import com.iflytek.skillhub.dto.NamespaceLifecycleRequest;
|
||||
|
|
@ -28,15 +30,18 @@ public class NamespacePortalCommandAppService {
|
|||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceGovernanceService namespaceGovernanceService;
|
||||
private final NamespaceMemberService namespaceMemberService;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
|
||||
public NamespacePortalCommandAppService(NamespaceService namespaceService,
|
||||
NamespaceRepository namespaceRepository,
|
||||
NamespaceGovernanceService namespaceGovernanceService,
|
||||
NamespaceMemberService namespaceMemberService) {
|
||||
NamespaceMemberService namespaceMemberService,
|
||||
UserAccountRepository userAccountRepository) {
|
||||
this.namespaceService = namespaceService;
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceGovernanceService = namespaceGovernanceService;
|
||||
this.namespaceMemberService = namespaceMemberService;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -135,7 +140,8 @@ public class NamespacePortalCommandAppService {
|
|||
role,
|
||||
operatorUserId
|
||||
);
|
||||
return MemberResponse.from(member);
|
||||
UserAccount user = userAccountRepository.findById(memberUserId).orElse(null);
|
||||
return MemberResponse.from(member, user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -157,7 +163,7 @@ public class NamespacePortalCommandAppService {
|
|||
request.role(),
|
||||
operatorUserId
|
||||
);
|
||||
return MemberResponse.from(member);
|
||||
return MemberResponse.from(member, userAccountRepository.findById(userId).orElse(null));
|
||||
}
|
||||
|
||||
private boolean canCreateNamespace(PlatformPrincipal principal) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.MemberResponse;
|
||||
import com.iflytek.skillhub.dto.MyNamespaceResponse;
|
||||
import com.iflytek.skillhub.dto.NamespaceResponse;
|
||||
|
|
@ -15,6 +17,8 @@ import com.iflytek.skillhub.dto.PageResponse;
|
|||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -31,15 +35,18 @@ public class NamespacePortalQueryAppService {
|
|||
private final NamespaceService namespaceService;
|
||||
private final NamespaceMemberService namespaceMemberService;
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
|
||||
public NamespacePortalQueryAppService(NamespaceRepository namespaceRepository,
|
||||
NamespaceService namespaceService,
|
||||
NamespaceMemberService namespaceMemberService,
|
||||
NamespaceAccessPolicy namespaceAccessPolicy) {
|
||||
NamespaceAccessPolicy namespaceAccessPolicy,
|
||||
UserAccountRepository userAccountRepository) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceService = namespaceService;
|
||||
this.namespaceMemberService = namespaceMemberService;
|
||||
this.namespaceAccessPolicy = namespaceAccessPolicy;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
|
@ -78,6 +85,18 @@ public class NamespacePortalQueryAppService {
|
|||
Namespace namespace = namespaceService.getNamespaceBySlug(slug);
|
||||
namespaceService.assertMember(namespace.getId(), userId);
|
||||
Page<NamespaceMember> members = namespaceMemberService.listMembers(namespace.getId(), pageable);
|
||||
return PageResponse.from(members.map(MemberResponse::from));
|
||||
|
||||
List<String> memberUserIds = members.getContent().stream()
|
||||
.map(NamespaceMember::getUserId)
|
||||
.toList();
|
||||
|
||||
Map<String, UserAccount> userMap = memberUserIds.isEmpty()
|
||||
? Map.of()
|
||||
: userAccountRepository.findByIdIn(memberUserIds).stream()
|
||||
.collect(Collectors.toMap(UserAccount::getId, Function.identity()));
|
||||
|
||||
return PageResponse.from(members.map(member ->
|
||||
MemberResponse.from(member, userMap.get(member.getUserId()))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.NamespaceCandidateUserResponse;
|
||||
import com.iflytek.skillhub.service.NamespaceMemberCandidateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -71,6 +73,9 @@ class NamespacePortalControllerTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private UserAccountRepository userAccountRepository;
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_returnsFrozenAndArchivedNamespacesWithCurrentRole() throws Exception {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ARCHIVED, NamespaceType.TEAM);
|
||||
|
|
@ -188,9 +193,12 @@ class NamespacePortalControllerTest {
|
|||
void addMember_returnsCreatedMember() throws Exception {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.ADMIN);
|
||||
UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null);
|
||||
given(namespaceService.getNamespaceBySlug("team-a")).willReturn(namespace);
|
||||
given(namespaceMemberService.addMember(1L, "user-2", NamespaceRole.ADMIN, "owner-1"))
|
||||
.willReturn(member);
|
||||
given(userAccountRepository.findById("user-2"))
|
||||
.willReturn(java.util.Optional.of(user));
|
||||
|
||||
mockMvc.perform(post("/api/v1/namespaces/team-a/members")
|
||||
.with(csrf())
|
||||
|
|
@ -203,7 +211,9 @@ class NamespacePortalControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.userId").value("user-2"))
|
||||
.andExpect(jsonPath("$.data.role").value("ADMIN"));
|
||||
.andExpect(jsonPath("$.data.role").value("ADMIN"))
|
||||
.andExpect(jsonPath("$.data.displayName").value("Alice"))
|
||||
.andExpect(jsonPath("$.data.email").value("alice@example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -224,9 +234,12 @@ class NamespacePortalControllerTest {
|
|||
void updateMemberRole_returnsUpdatedMember() throws Exception {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.OWNER);
|
||||
UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null);
|
||||
given(namespaceService.getNamespaceBySlug("team-a")).willReturn(namespace);
|
||||
given(namespaceMemberService.updateMemberRole(1L, "user-2", NamespaceRole.OWNER, "owner-1"))
|
||||
.willReturn(member);
|
||||
given(userAccountRepository.findById("user-2"))
|
||||
.willReturn(java.util.Optional.of(user));
|
||||
|
||||
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put("/api/v1/namespaces/team-a/members/user-2/role")
|
||||
.with(csrf())
|
||||
|
|
@ -239,7 +252,9 @@ class NamespacePortalControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.userId").value("user-2"))
|
||||
.andExpect(jsonPath("$.data.role").value("OWNER"));
|
||||
.andExpect(jsonPath("$.data.role").value("OWNER"))
|
||||
.andExpect(jsonPath("$.data.displayName").value("Alice"))
|
||||
.andExpect(jsonPath("$.data.email").value("alice@example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -9,17 +9,24 @@ import static org.mockito.Mockito.when;
|
|||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceGovernanceService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.MemberResponse;
|
||||
import com.iflytek.skillhub.dto.NamespaceLifecycleRequest;
|
||||
import com.iflytek.skillhub.dto.NamespaceRequest;
|
||||
import com.iflytek.skillhub.dto.UpdateMemberRoleRequest;
|
||||
import com.iflytek.skillhub.exception.ForbiddenException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
class NamespacePortalCommandAppServiceTest {
|
||||
|
|
@ -28,11 +35,13 @@ class NamespacePortalCommandAppServiceTest {
|
|||
private final NamespaceRepository namespaceRepository = mock(NamespaceRepository.class);
|
||||
private final NamespaceGovernanceService namespaceGovernanceService = mock(NamespaceGovernanceService.class);
|
||||
private final NamespaceMemberService namespaceMemberService = mock(NamespaceMemberService.class);
|
||||
private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class);
|
||||
private final NamespacePortalCommandAppService service = new NamespacePortalCommandAppService(
|
||||
namespaceService,
|
||||
namespaceRepository,
|
||||
namespaceGovernanceService,
|
||||
namespaceMemberService
|
||||
namespaceMemberService,
|
||||
userAccountRepository
|
||||
);
|
||||
|
||||
@Test
|
||||
|
|
@ -71,4 +80,92 @@ class NamespacePortalCommandAppServiceTest {
|
|||
namespace.setType(NamespaceType.TEAM);
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@Test
|
||||
void addMember_populatesDisplayNameAndEmail() {
|
||||
Namespace ns = namespace(1L, "team-a");
|
||||
NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.ADMIN);
|
||||
ReflectionTestUtils.setField(member, "id", 10L);
|
||||
UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null);
|
||||
|
||||
when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns);
|
||||
when(namespaceMemberService.addMember(1L, "user-2", NamespaceRole.ADMIN, "owner-1"))
|
||||
.thenReturn(member);
|
||||
when(userAccountRepository.findById("user-2"))
|
||||
.thenReturn(Optional.of(user));
|
||||
|
||||
MemberResponse result = service.addMember("team-a", "user-2", NamespaceRole.ADMIN, "owner-1");
|
||||
|
||||
assertThat(result.userId()).isEqualTo("user-2");
|
||||
assertThat(result.displayName()).isEqualTo("Alice");
|
||||
assertThat(result.email()).isEqualTo("alice@example.com");
|
||||
assertThat(result.role()).isEqualTo(NamespaceRole.ADMIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addMember_withoutUserAccount_degradesGracefully() {
|
||||
Namespace ns = namespace(1L, "team-a");
|
||||
NamespaceMember member = new NamespaceMember(1L, "ghost", NamespaceRole.MEMBER);
|
||||
ReflectionTestUtils.setField(member, "id", 20L);
|
||||
|
||||
when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns);
|
||||
when(namespaceMemberService.addMember(1L, "ghost", NamespaceRole.MEMBER, "owner-1"))
|
||||
.thenReturn(member);
|
||||
when(userAccountRepository.findById("ghost"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
MemberResponse result = service.addMember("team-a", "ghost", NamespaceRole.MEMBER, "owner-1");
|
||||
|
||||
assertThat(result.userId()).isEqualTo("ghost");
|
||||
assertThat(result.displayName()).isNull();
|
||||
assertThat(result.email()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMemberRole_populatesDisplayNameAndEmail() {
|
||||
Namespace ns = namespace(1L, "team-a");
|
||||
NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.OWNER);
|
||||
ReflectionTestUtils.setField(member, "id", 10L);
|
||||
UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null);
|
||||
|
||||
when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns);
|
||||
when(namespaceMemberService.updateMemberRole(1L, "user-2", NamespaceRole.OWNER, "owner-1"))
|
||||
.thenReturn(member);
|
||||
when(userAccountRepository.findById("user-2"))
|
||||
.thenReturn(Optional.of(user));
|
||||
|
||||
MemberResponse result = service.updateMemberRole(
|
||||
"team-a", "user-2",
|
||||
new UpdateMemberRoleRequest(NamespaceRole.OWNER),
|
||||
"owner-1"
|
||||
);
|
||||
|
||||
assertThat(result.userId()).isEqualTo("user-2");
|
||||
assertThat(result.displayName()).isEqualTo("Alice");
|
||||
assertThat(result.email()).isEqualTo("alice@example.com");
|
||||
assertThat(result.role()).isEqualTo(NamespaceRole.OWNER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMemberRole_withoutUserAccount_degradesGracefully() {
|
||||
Namespace ns = namespace(1L, "team-a");
|
||||
NamespaceMember member = new NamespaceMember(1L, "ghost", NamespaceRole.MEMBER);
|
||||
ReflectionTestUtils.setField(member, "id", 20L);
|
||||
|
||||
when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns);
|
||||
when(namespaceMemberService.updateMemberRole(1L, "ghost", NamespaceRole.ADMIN, "owner-1"))
|
||||
.thenReturn(member);
|
||||
when(userAccountRepository.findById("ghost"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
MemberResponse result = service.updateMemberRole(
|
||||
"team-a", "ghost",
|
||||
new UpdateMemberRoleRequest(NamespaceRole.ADMIN),
|
||||
"owner-1"
|
||||
);
|
||||
|
||||
assertThat(result.userId()).isEqualTo("ghost");
|
||||
assertThat(result.displayName()).isNull();
|
||||
assertThat(result.email()).isNull();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,28 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceAccessPolicy;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.dto.MemberResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -25,11 +34,13 @@ class NamespacePortalQueryAppServiceTest {
|
|||
private final NamespaceService namespaceService = mock(NamespaceService.class);
|
||||
private final NamespaceMemberService namespaceMemberService = mock(NamespaceMemberService.class);
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy = mock(NamespaceAccessPolicy.class);
|
||||
private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class);
|
||||
private final NamespacePortalQueryAppService service = new NamespacePortalQueryAppService(
|
||||
namespaceRepository,
|
||||
namespaceService,
|
||||
namespaceMemberService,
|
||||
namespaceAccessPolicy
|
||||
namespaceAccessPolicy,
|
||||
userAccountRepository
|
||||
);
|
||||
|
||||
@Test
|
||||
|
|
@ -67,4 +78,48 @@ class NamespacePortalQueryAppServiceTest {
|
|||
namespace.setType(NamespaceType.TEAM);
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMembers_withUserAccount_returnsDisplayNameAndEmail() {
|
||||
Namespace ns = namespace(1L, "team-a");
|
||||
NamespaceMember member = new NamespaceMember(1L, "user-2", NamespaceRole.ADMIN);
|
||||
ReflectionTestUtils.setField(member, "id", 10L);
|
||||
UserAccount user = new UserAccount("user-2", "Alice", "alice@example.com", null);
|
||||
|
||||
when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns);
|
||||
when(namespaceMemberService.listMembers(eq(1L), any(PageRequest.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(member), PageRequest.of(0, 20), 1));
|
||||
when(userAccountRepository.findByIdIn(List.of("user-2")))
|
||||
.thenReturn(List.of(user));
|
||||
|
||||
PageResponse<MemberResponse> result = service.listMembers("team-a", PageRequest.of(0, 20), "owner-1");
|
||||
|
||||
assertThat(result.items()).hasSize(1);
|
||||
MemberResponse mr = result.items().get(0);
|
||||
assertThat(mr.userId()).isEqualTo("user-2");
|
||||
assertThat(mr.displayName()).isEqualTo("Alice");
|
||||
assertThat(mr.email()).isEqualTo("alice@example.com");
|
||||
assertThat(mr.role()).isEqualTo(NamespaceRole.ADMIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMembers_withoutUserAccount_returnsNullFields() {
|
||||
Namespace ns = namespace(1L, "team-a");
|
||||
NamespaceMember member = new NamespaceMember(1L, "ghost-user", NamespaceRole.MEMBER);
|
||||
ReflectionTestUtils.setField(member, "id", 20L);
|
||||
|
||||
when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(ns);
|
||||
when(namespaceMemberService.listMembers(eq(1L), any(PageRequest.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(member), PageRequest.of(0, 20), 1));
|
||||
when(userAccountRepository.findByIdIn(List.of("ghost-user")))
|
||||
.thenReturn(List.of());
|
||||
|
||||
PageResponse<MemberResponse> result = service.listMembers("team-a", PageRequest.of(0, 20), "owner-1");
|
||||
|
||||
assertThat(result.items()).hasSize(1);
|
||||
MemberResponse mr = result.items().get(0);
|
||||
assertThat(mr.userId()).isEqualTo("ghost-user");
|
||||
assertThat(mr.displayName()).isNull();
|
||||
assertThat(mr.email()).isNull();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
web/src/api/generated/schema.d.ts
vendored
2
web/src/api/generated/schema.d.ts
vendored
|
|
@ -3037,6 +3037,8 @@ export interface components {
|
|||
/** Format: int64 */
|
||||
namespaceId?: number;
|
||||
userId?: string;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
/** @enum {string} */
|
||||
role?: "OWNER" | "ADMIN" | "MEMBER";
|
||||
/** Format: date-time */
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ export interface ManagedNamespace extends Namespace {
|
|||
export interface NamespaceMember {
|
||||
id: number
|
||||
userId: string
|
||||
displayName?: string
|
||||
email?: string
|
||||
role: NamespaceRole
|
||||
createdAt: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -983,6 +983,8 @@
|
|||
"savingRole": "Saving...",
|
||||
"changeRole": "Change role",
|
||||
"colUserId": "User ID",
|
||||
"colUsername": "Username",
|
||||
"colEmail": "Email",
|
||||
"colRole": "Role",
|
||||
"colJoinedAt": "Joined At",
|
||||
"colActions": "Actions",
|
||||
|
|
|
|||
|
|
@ -983,6 +983,8 @@
|
|||
"savingRole": "保存中...",
|
||||
"changeRole": "修改角色",
|
||||
"colUserId": "用户 ID",
|
||||
"colUsername": "用户名",
|
||||
"colEmail": "邮箱",
|
||||
"colRole": "角色",
|
||||
"colJoinedAt": "加入时间",
|
||||
"colActions": "操作",
|
||||
|
|
|
|||
|
|
@ -178,7 +178,8 @@ export function NamespaceMembersPage() {
|
|||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border/40">
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colUserId')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colUsername')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colEmail')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colRole')}</th>
|
||||
<th className="text-left p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colJoinedAt')}</th>
|
||||
<th className="text-right p-4 font-medium font-heading text-sm text-muted-foreground">{t('members.colActions')}</th>
|
||||
|
|
@ -193,7 +194,13 @@ export function NamespaceMembersPage() {
|
|||
|
||||
return (
|
||||
<tr key={member.id} className="border-b border-border/40 last:border-b-0 hover:bg-secondary/30 transition-colors">
|
||||
<td className="p-4 font-medium font-mono">{member.userId}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{member.displayName || member.userId}</span>
|
||||
<span className="text-xs text-muted-foreground">{member.userId}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-muted-foreground">{member.email || '-'}</td>
|
||||
<td className="p-4">
|
||||
{canManageMembers && !isOwner ? (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue