mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
feat: add namespace lifecycle governance
This commit is contained in:
parent
1418bc4840
commit
7501bb3cf2
8 changed files with 468 additions and 2 deletions
|
|
@ -65,6 +65,7 @@ public class Namespace {
|
|||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public NamespaceStatus getStatus() { return status; }
|
||||
public void setStatus(NamespaceStatus status) { this.status = status; }
|
||||
public NamespaceType getType() { return type; }
|
||||
public void setType(NamespaceType type) { this.type = type; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class NamespaceAccessPolicy {
|
||||
|
||||
public boolean isImmutable(Namespace namespace) {
|
||||
return namespace.getType() == NamespaceType.GLOBAL;
|
||||
}
|
||||
|
||||
public boolean canMutateSettings(Namespace namespace) {
|
||||
return namespace.getType() == NamespaceType.TEAM
|
||||
&& namespace.getStatus() == NamespaceStatus.ACTIVE;
|
||||
}
|
||||
|
||||
public boolean canManageMembers(Namespace namespace) {
|
||||
return canMutateSettings(namespace);
|
||||
}
|
||||
|
||||
public boolean canTransferOwnership(Namespace namespace) {
|
||||
return canMutateSettings(namespace);
|
||||
}
|
||||
|
||||
public boolean canFreeze(Namespace namespace, NamespaceRole role) {
|
||||
return namespace.getType() == NamespaceType.TEAM
|
||||
&& namespace.getStatus() == NamespaceStatus.ACTIVE
|
||||
&& (role == NamespaceRole.OWNER || role == NamespaceRole.ADMIN);
|
||||
}
|
||||
|
||||
public boolean canUnfreeze(Namespace namespace, NamespaceRole role) {
|
||||
return namespace.getType() == NamespaceType.TEAM
|
||||
&& namespace.getStatus() == NamespaceStatus.FROZEN
|
||||
&& (role == NamespaceRole.OWNER || role == NamespaceRole.ADMIN);
|
||||
}
|
||||
|
||||
public boolean canArchive(Namespace namespace, NamespaceRole role) {
|
||||
return namespace.getType() == NamespaceType.TEAM
|
||||
&& namespace.getStatus() != NamespaceStatus.ARCHIVED
|
||||
&& role == NamespaceRole.OWNER;
|
||||
}
|
||||
|
||||
public boolean canRestore(Namespace namespace, NamespaceRole role) {
|
||||
return namespace.getType() == NamespaceType.TEAM
|
||||
&& namespace.getStatus() == NamespaceStatus.ARCHIVED
|
||||
&& role == NamespaceRole.OWNER;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class NamespaceGovernanceService {
|
||||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
public NamespaceGovernanceService(NamespaceRepository namespaceRepository,
|
||||
NamespaceMemberRepository namespaceMemberRepository,
|
||||
NamespaceAccessPolicy namespaceAccessPolicy,
|
||||
AuditLogService auditLogService) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.namespaceAccessPolicy = namespaceAccessPolicy;
|
||||
this.auditLogService = auditLogService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Namespace freezeNamespace(String slug,
|
||||
String actorUserId,
|
||||
String reason,
|
||||
String requestId,
|
||||
String clientIp,
|
||||
String userAgent) {
|
||||
Namespace namespace = loadNamespaceBySlug(slug);
|
||||
NamespaceRole role = requireRole(namespace.getId(), actorUserId);
|
||||
if (namespace.getStatus() != NamespaceStatus.ACTIVE) {
|
||||
throw new DomainBadRequestException("error.namespace.state.transition.invalid", namespace.getSlug());
|
||||
}
|
||||
if (!namespaceAccessPolicy.canFreeze(namespace, role)) {
|
||||
throw new DomainForbiddenException("error.namespace.lifecycle.forbidden", namespace.getSlug());
|
||||
}
|
||||
namespace.setStatus(NamespaceStatus.FROZEN);
|
||||
Namespace updated = namespaceRepository.save(namespace);
|
||||
record("FREEZE_NAMESPACE", actorUserId, updated.getId(), requestId, clientIp, userAgent, reason);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Namespace unfreezeNamespace(String slug,
|
||||
String actorUserId,
|
||||
String requestId,
|
||||
String clientIp,
|
||||
String userAgent) {
|
||||
Namespace namespace = loadNamespaceBySlug(slug);
|
||||
NamespaceRole role = requireRole(namespace.getId(), actorUserId);
|
||||
if (namespace.getStatus() != NamespaceStatus.FROZEN) {
|
||||
throw new DomainBadRequestException("error.namespace.state.transition.invalid", namespace.getSlug());
|
||||
}
|
||||
if (!namespaceAccessPolicy.canUnfreeze(namespace, role)) {
|
||||
throw new DomainForbiddenException("error.namespace.lifecycle.forbidden", namespace.getSlug());
|
||||
}
|
||||
namespace.setStatus(NamespaceStatus.ACTIVE);
|
||||
Namespace updated = namespaceRepository.save(namespace);
|
||||
record("UNFREEZE_NAMESPACE", actorUserId, updated.getId(), requestId, clientIp, userAgent, null);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Namespace archiveNamespace(String slug,
|
||||
String actorUserId,
|
||||
String reason,
|
||||
String requestId,
|
||||
String clientIp,
|
||||
String userAgent) {
|
||||
Namespace namespace = loadNamespaceBySlug(slug);
|
||||
NamespaceRole role = requireRole(namespace.getId(), actorUserId);
|
||||
if (namespace.getStatus() == NamespaceStatus.ARCHIVED) {
|
||||
throw new DomainBadRequestException("error.namespace.state.transition.invalid", namespace.getSlug());
|
||||
}
|
||||
if (!namespaceAccessPolicy.canArchive(namespace, role)) {
|
||||
throw new DomainForbiddenException("error.namespace.lifecycle.forbidden", namespace.getSlug());
|
||||
}
|
||||
namespace.setStatus(NamespaceStatus.ARCHIVED);
|
||||
Namespace updated = namespaceRepository.save(namespace);
|
||||
record("ARCHIVE_NAMESPACE", actorUserId, updated.getId(), requestId, clientIp, userAgent, reason);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Namespace restoreNamespace(String slug,
|
||||
String actorUserId,
|
||||
String requestId,
|
||||
String clientIp,
|
||||
String userAgent) {
|
||||
Namespace namespace = loadNamespaceBySlug(slug);
|
||||
NamespaceRole role = requireRole(namespace.getId(), actorUserId);
|
||||
if (namespace.getStatus() != NamespaceStatus.ARCHIVED) {
|
||||
throw new DomainBadRequestException("error.namespace.state.transition.invalid", namespace.getSlug());
|
||||
}
|
||||
if (!namespaceAccessPolicy.canRestore(namespace, role)) {
|
||||
throw new DomainForbiddenException("error.namespace.lifecycle.forbidden", namespace.getSlug());
|
||||
}
|
||||
namespace.setStatus(NamespaceStatus.ACTIVE);
|
||||
Namespace updated = namespaceRepository.save(namespace);
|
||||
record("RESTORE_NAMESPACE", actorUserId, updated.getId(), requestId, clientIp, userAgent, null);
|
||||
return updated;
|
||||
}
|
||||
|
||||
private Namespace loadNamespaceBySlug(String slug) {
|
||||
Namespace namespace = namespaceRepository.findBySlug(slug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
if (namespaceAccessPolicy.isImmutable(namespace)) {
|
||||
throw new DomainBadRequestException("error.namespace.system.immutable", slug);
|
||||
}
|
||||
return namespace;
|
||||
}
|
||||
|
||||
private NamespaceRole requireRole(Long namespaceId, String userId) {
|
||||
return namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
|
||||
.map(NamespaceMember::getRole)
|
||||
.orElseThrow(() -> new DomainForbiddenException("error.namespace.membership.required"));
|
||||
}
|
||||
|
||||
private void record(String action,
|
||||
String actorUserId,
|
||||
Long namespaceId,
|
||||
String requestId,
|
||||
String clientIp,
|
||||
String userAgent,
|
||||
String reason) {
|
||||
auditLogService.record(
|
||||
actorUserId,
|
||||
action,
|
||||
"NAMESPACE",
|
||||
namespaceId,
|
||||
requestId,
|
||||
clientIp,
|
||||
userAgent,
|
||||
reason == null || reason.isBlank() ? null : "{\"reason\":\"" + reason.replace("\"", "\\\"") + "\"}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,15 +13,19 @@ public class NamespaceMemberService {
|
|||
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final NamespaceService namespaceService;
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
|
||||
public NamespaceMemberService(NamespaceMemberRepository namespaceMemberRepository,
|
||||
NamespaceService namespaceService) {
|
||||
NamespaceService namespaceService,
|
||||
NamespaceAccessPolicy namespaceAccessPolicy) {
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.namespaceService = namespaceService;
|
||||
this.namespaceAccessPolicy = namespaceAccessPolicy;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public NamespaceMember addMember(Long namespaceId, String userId, NamespaceRole role, String operatorUserId) {
|
||||
assertMemberMutationAllowed(namespaceId);
|
||||
namespaceService.assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
|
||||
if (role == NamespaceRole.OWNER) {
|
||||
|
|
@ -38,6 +42,7 @@ public class NamespaceMemberService {
|
|||
|
||||
@Transactional
|
||||
public void removeMember(Long namespaceId, String userId, String operatorUserId) {
|
||||
assertMemberMutationAllowed(namespaceId);
|
||||
namespaceService.assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
|
||||
NamespaceMember member = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
|
||||
|
|
@ -52,6 +57,7 @@ public class NamespaceMemberService {
|
|||
|
||||
@Transactional
|
||||
public NamespaceMember updateMemberRole(Long namespaceId, String userId, NamespaceRole newRole, String operatorUserId) {
|
||||
assertMemberMutationAllowed(namespaceId);
|
||||
namespaceService.assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
|
||||
if (newRole == NamespaceRole.OWNER) {
|
||||
|
|
@ -67,6 +73,11 @@ public class NamespaceMemberService {
|
|||
|
||||
@Transactional
|
||||
public void transferOwnership(Long namespaceId, String currentOwnerId, String newOwnerId) {
|
||||
Namespace namespace = namespaceService.getNamespace(namespaceId);
|
||||
if (!namespaceAccessPolicy.canTransferOwnership(namespace)) {
|
||||
throw new DomainBadRequestException("error.namespace.readonly", namespace.getSlug());
|
||||
}
|
||||
|
||||
NamespaceMember currentOwner = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, currentOwnerId)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.owner.current.notFound"));
|
||||
|
||||
|
|
@ -92,4 +103,14 @@ public class NamespaceMemberService {
|
|||
public Page<NamespaceMember> listMembers(Long namespaceId, Pageable pageable) {
|
||||
return namespaceMemberRepository.findByNamespaceId(namespaceId, pageable);
|
||||
}
|
||||
|
||||
private void assertMemberMutationAllowed(Long namespaceId) {
|
||||
Namespace namespace = namespaceService.getNamespace(namespaceId);
|
||||
if (!namespaceAccessPolicy.canManageMembers(namespace)) {
|
||||
if (namespaceAccessPolicy.isImmutable(namespace)) {
|
||||
throw new DomainBadRequestException("error.namespace.system.immutable", namespace.getSlug());
|
||||
}
|
||||
throw new DomainBadRequestException("error.namespace.readonly", namespace.getSlug());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,14 @@ public class NamespaceService {
|
|||
|
||||
private final NamespaceRepository namespaceRepository;
|
||||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
|
||||
public NamespaceService(NamespaceRepository namespaceRepository,
|
||||
NamespaceMemberRepository namespaceMemberRepository) {
|
||||
NamespaceMemberRepository namespaceMemberRepository,
|
||||
NamespaceAccessPolicy namespaceAccessPolicy) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.namespaceAccessPolicy = namespaceAccessPolicy;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -42,6 +45,7 @@ public class NamespaceService {
|
|||
Namespace namespace = namespaceRepository.findById(namespaceId)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.id.notFound", namespaceId));
|
||||
assertAdminOrOwner(namespaceId, operatorUserId);
|
||||
assertMutable(namespace);
|
||||
|
||||
if (displayName != null) {
|
||||
namespace.setDisplayName(displayName);
|
||||
|
|
@ -61,6 +65,11 @@ public class NamespaceService {
|
|||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
public Namespace getNamespace(Long namespaceId) {
|
||||
return namespaceRepository.findById(namespaceId)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.id.notFound", namespaceId));
|
||||
}
|
||||
|
||||
void assertAdminOrOwner(Long namespaceId, String userId) {
|
||||
NamespaceRole role = namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId)
|
||||
.map(NamespaceMember::getRole)
|
||||
|
|
@ -69,4 +78,13 @@ public class NamespaceService {
|
|||
throw new DomainForbiddenException("error.namespace.admin.required");
|
||||
}
|
||||
}
|
||||
|
||||
void assertMutable(Namespace namespace) {
|
||||
if (namespaceAccessPolicy.isImmutable(namespace)) {
|
||||
throw new DomainBadRequestException("error.namespace.system.immutable", namespace.getSlug());
|
||||
}
|
||||
if (!namespaceAccessPolicy.canMutateSettings(namespace)) {
|
||||
throw new DomainBadRequestException("error.namespace.readonly", namespace.getSlug());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
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 java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class NamespaceGovernanceServiceTest {
|
||||
|
||||
@Mock
|
||||
private NamespaceRepository namespaceRepository;
|
||||
|
||||
@Mock
|
||||
private NamespaceMemberRepository namespaceMemberRepository;
|
||||
|
||||
@Mock
|
||||
private NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
|
||||
@Mock
|
||||
private AuditLogService auditLogService;
|
||||
|
||||
@InjectMocks
|
||||
private NamespaceGovernanceService governanceService;
|
||||
|
||||
@Test
|
||||
void freezeNamespace_allowsAdminOnActiveTeamNamespace() {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceType.TEAM, NamespaceStatus.ACTIVE);
|
||||
when(namespaceRepository.findBySlug("team-a")).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "admin-1"))
|
||||
.thenReturn(Optional.of(new NamespaceMember(1L, "admin-1", NamespaceRole.ADMIN)));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.canFreeze(namespace, NamespaceRole.ADMIN)).thenReturn(true);
|
||||
when(namespaceRepository.save(namespace)).thenReturn(namespace);
|
||||
|
||||
Namespace updated = governanceService.freezeNamespace("team-a", "admin-1", null, null, null, null);
|
||||
|
||||
assertEquals(NamespaceStatus.FROZEN, updated.getStatus());
|
||||
verify(namespaceRepository).save(namespace);
|
||||
}
|
||||
|
||||
@Test
|
||||
void archiveNamespace_rejectsAdminAndAllowsOnlyOwner() {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceType.TEAM, NamespaceStatus.ACTIVE);
|
||||
when(namespaceRepository.findBySlug("team-a")).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "admin-1"))
|
||||
.thenReturn(Optional.of(new NamespaceMember(1L, "admin-1", NamespaceRole.ADMIN)));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.canArchive(namespace, NamespaceRole.ADMIN)).thenReturn(false);
|
||||
|
||||
assertThrows(DomainForbiddenException.class,
|
||||
() -> governanceService.archiveNamespace("team-a", "admin-1", "cleanup", null, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreNamespace_movesArchivedNamespaceBackToActive() {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceType.TEAM, NamespaceStatus.ARCHIVED);
|
||||
when(namespaceRepository.findBySlug("team-a")).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "owner-1"))
|
||||
.thenReturn(Optional.of(new NamespaceMember(1L, "owner-1", NamespaceRole.OWNER)));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.canRestore(namespace, NamespaceRole.OWNER)).thenReturn(true);
|
||||
when(namespaceRepository.save(namespace)).thenReturn(namespace);
|
||||
|
||||
Namespace updated = governanceService.restoreNamespace("team-a", "owner-1", null, null, null);
|
||||
|
||||
assertEquals(NamespaceStatus.ACTIVE, updated.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void freezeNamespace_rejectsGlobalNamespace() {
|
||||
Namespace namespace = namespace(1L, "global", NamespaceType.GLOBAL, NamespaceStatus.ACTIVE);
|
||||
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(true);
|
||||
|
||||
assertThrows(DomainBadRequestException.class,
|
||||
() -> governanceService.freezeNamespace("global", "admin-1", null, null, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unfreezeNamespace_rejectsIllegalTransition() {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceType.TEAM, NamespaceStatus.ACTIVE);
|
||||
when(namespaceRepository.findBySlug("team-a")).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "owner-1"))
|
||||
.thenReturn(Optional.of(new NamespaceMember(1L, "owner-1", NamespaceRole.OWNER)));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
|
||||
assertThrows(DomainBadRequestException.class,
|
||||
() -> governanceService.unfreezeNamespace("team-a", "owner-1", null, null, null));
|
||||
}
|
||||
|
||||
private Namespace namespace(Long id, String slug, NamespaceType type, NamespaceStatus status) {
|
||||
Namespace namespace = new Namespace(slug, "Team A", "owner-1");
|
||||
setField(namespace, "id", id);
|
||||
namespace.setType(type);
|
||||
namespace.setStatus(status);
|
||||
return namespace;
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ class NamespaceMemberServiceTest {
|
|||
private NamespaceMemberRepository namespaceMemberRepository;
|
||||
@Mock
|
||||
private NamespaceService namespaceService;
|
||||
@Mock
|
||||
private NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
|
||||
@InjectMocks
|
||||
private NamespaceMemberService namespaceMemberService;
|
||||
|
|
@ -29,7 +31,10 @@ class NamespaceMemberServiceTest {
|
|||
Long namespaceId = 1L;
|
||||
String userId = "user-2";
|
||||
NamespaceRole role = NamespaceRole.MEMBER;
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.empty());
|
||||
when(namespaceMemberRepository.save(any(NamespaceMember.class)))
|
||||
|
|
@ -43,12 +48,19 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Test
|
||||
void addMember_shouldThrowExceptionForOwnerRole() {
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(1L)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.addMember(1L, "user-2", NamespaceRole.OWNER, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addMember_shouldRequireAdminOrOwner() {
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(1L)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
doThrow(new DomainForbiddenException("error.namespace.admin.required")).when(namespaceService).assertAdminOrOwner(1L, "user-99");
|
||||
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
|
|
@ -59,6 +71,9 @@ class NamespaceMemberServiceTest {
|
|||
void addMember_shouldThrowExceptionWhenMemberExists() {
|
||||
Long namespaceId = 1L;
|
||||
String userId = "user-2";
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.of(new NamespaceMember()));
|
||||
|
||||
|
|
@ -66,11 +81,27 @@ class NamespaceMemberServiceTest {
|
|||
namespaceMemberService.addMember(namespaceId, userId, NamespaceRole.MEMBER, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addMember_shouldRejectFrozenNamespace() {
|
||||
Long namespaceId = 1L;
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
namespace.setStatus(NamespaceStatus.FROZEN);
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.addMember(namespaceId, "user-2", NamespaceRole.MEMBER, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeMember_shouldThrowExceptionForOwner() {
|
||||
Long namespaceId = 1L;
|
||||
String userId = "user-2";
|
||||
NamespaceMember ownerMember = new NamespaceMember(namespaceId, userId, NamespaceRole.OWNER);
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.of(ownerMember));
|
||||
|
||||
|
|
@ -80,6 +111,9 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Test
|
||||
void removeMember_shouldThrowExceptionWhenMemberNotFound() {
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(1L)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "user-2"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
|
|
@ -87,11 +121,27 @@ class NamespaceMemberServiceTest {
|
|||
namespaceMemberService.removeMember(1L, "user-2", "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMemberRole_shouldRejectArchivedNamespace() {
|
||||
Long namespaceId = 1L;
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
namespace.setStatus(NamespaceStatus.ARCHIVED);
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.updateMemberRole(namespaceId, "user-2", NamespaceRole.ADMIN, "user-99"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMemberRole_shouldUpdateRoleSuccessfully() {
|
||||
Long namespaceId = 1L;
|
||||
String userId = "user-2";
|
||||
NamespaceMember member = new NamespaceMember(namespaceId, userId, NamespaceRole.MEMBER);
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, userId))
|
||||
.thenReturn(Optional.of(member));
|
||||
when(namespaceMemberRepository.save(any(NamespaceMember.class))).thenReturn(member);
|
||||
|
|
@ -106,6 +156,9 @@ class NamespaceMemberServiceTest {
|
|||
void updateMemberRole_shouldThrowExceptionForOwnerRole() {
|
||||
Long namespaceId = 1L;
|
||||
String userId = "user-2";
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.updateMemberRole(namespaceId, userId, NamespaceRole.OWNER, "user-99"));
|
||||
|
|
@ -113,6 +166,9 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Test
|
||||
void updateMemberRole_shouldThrowExceptionWhenMemberNotFound() {
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(1L)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canManageMembers(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "user-2"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
|
|
@ -128,7 +184,10 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
NamespaceMember currentOwner = new NamespaceMember(namespaceId, currentOwnerId, NamespaceRole.OWNER);
|
||||
NamespaceMember newOwner = new NamespaceMember(namespaceId, newOwnerId, NamespaceRole.ADMIN);
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canTransferOwnership(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, currentOwnerId))
|
||||
.thenReturn(Optional.of(currentOwner));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, newOwnerId))
|
||||
|
|
@ -143,6 +202,9 @@ class NamespaceMemberServiceTest {
|
|||
|
||||
@Test
|
||||
void transferOwnership_shouldThrowExceptionWhenCurrentOwnerNotFound() {
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(1L)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canTransferOwnership(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(1L, "user-2"))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
|
|
@ -155,6 +217,9 @@ class NamespaceMemberServiceTest {
|
|||
Long namespaceId = 1L;
|
||||
String currentOwnerId = "user-2";
|
||||
NamespaceMember notOwner = new NamespaceMember(namespaceId, currentOwnerId, NamespaceRole.ADMIN);
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canTransferOwnership(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, currentOwnerId))
|
||||
.thenReturn(Optional.of(notOwner));
|
||||
|
||||
|
|
@ -168,7 +233,10 @@ class NamespaceMemberServiceTest {
|
|||
String currentOwnerId = "user-2";
|
||||
String newOwnerId = "user-3";
|
||||
NamespaceMember currentOwner = new NamespaceMember(namespaceId, currentOwnerId, NamespaceRole.OWNER);
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canTransferOwnership(namespace)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, currentOwnerId))
|
||||
.thenReturn(Optional.of(currentOwner));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, newOwnerId))
|
||||
|
|
@ -178,6 +246,18 @@ class NamespaceMemberServiceTest {
|
|||
namespaceMemberService.transferOwnership(namespaceId, currentOwnerId, newOwnerId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferOwnership_shouldRejectFrozenNamespace() {
|
||||
Long namespaceId = 1L;
|
||||
Namespace namespace = new Namespace("team-a", "Team A", "owner-1");
|
||||
namespace.setStatus(NamespaceStatus.FROZEN);
|
||||
when(namespaceService.getNamespace(namespaceId)).thenReturn(namespace);
|
||||
when(namespaceAccessPolicy.canTransferOwnership(namespace)).thenReturn(false);
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceMemberService.transferOwnership(namespaceId, "user-2", "user-3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMemberRole_shouldReturnRole() {
|
||||
Long namespaceId = 1L;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ class NamespaceServiceTest {
|
|||
@Mock
|
||||
private NamespaceMemberRepository namespaceMemberRepository;
|
||||
|
||||
@Mock
|
||||
private NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
|
||||
@InjectMocks
|
||||
private NamespaceService namespaceService;
|
||||
|
||||
|
|
@ -70,6 +73,8 @@ class NamespaceServiceTest {
|
|||
when(namespaceRepository.findById(namespaceId)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorUserId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(namespaceId, operatorUserId, NamespaceRole.OWNER)));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.canMutateSettings(namespace)).thenReturn(true);
|
||||
when(namespaceRepository.save(any(Namespace.class))).thenReturn(namespace);
|
||||
|
||||
Namespace result = namespaceService.updateNamespace(
|
||||
|
|
@ -105,6 +110,37 @@ class NamespaceServiceTest {
|
|||
namespaceService.updateNamespace(namespaceId, "Name", "Desc", null, operatorUserId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateNamespace_shouldRejectFrozenNamespace() {
|
||||
Long namespaceId = 1L;
|
||||
String operatorUserId = "user-1";
|
||||
Namespace namespace = new Namespace("slug", "Old Name", "user-1");
|
||||
namespace.setStatus(NamespaceStatus.FROZEN);
|
||||
when(namespaceRepository.findById(namespaceId)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorUserId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(namespaceId, operatorUserId, NamespaceRole.OWNER)));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.canMutateSettings(namespace)).thenReturn(false);
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceService.updateNamespace(namespaceId, "Name", "Desc", null, operatorUserId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateNamespace_shouldRejectGlobalNamespaceMutation() {
|
||||
Long namespaceId = 1L;
|
||||
String operatorUserId = "user-1";
|
||||
Namespace namespace = new Namespace("global", "Global", "system");
|
||||
namespace.setType(NamespaceType.GLOBAL);
|
||||
when(namespaceRepository.findById(namespaceId)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, operatorUserId))
|
||||
.thenReturn(Optional.of(new NamespaceMember(namespaceId, operatorUserId, NamespaceRole.OWNER)));
|
||||
when(namespaceAccessPolicy.isImmutable(namespace)).thenReturn(true);
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
namespaceService.updateNamespace(namespaceId, "Name", "Desc", null, operatorUserId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNamespaceBySlug_shouldReturnNamespace() {
|
||||
String slug = "test-slug";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue