mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-15 04:18:20 +00:00
fix(authoring): prevent namespace deletion while drafts remain
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
642e780f1e
commit
d8dbd4f0c9
8 changed files with 60 additions and 4 deletions
|
|
@ -93,7 +93,7 @@ error.namespace.global.members.platformAdmin.required=Only platform user adminis
|
|||
error.namespace.admin.required=Namespace owner or admin role required
|
||||
error.namespace.owner.required=Namespace owner role required
|
||||
error.namespace.create.platformAdminRequired=Only SKILL_ADMIN or SUPER_ADMIN can create namespaces
|
||||
error.namespace.delete.hasDependencies=Namespace cannot be deleted while it still contains skills or governance records
|
||||
error.namespace.delete.hasDependencies=Namespace cannot be deleted while it still contains skills, drafts or governance records
|
||||
error.namespace.member.owner.assignDirect=Cannot assign OWNER role directly
|
||||
error.namespace.member.alreadyExists=User is already a namespace member
|
||||
error.namespace.member.notFound=Member not found
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ error.namespace.global.members.platformAdmin.required=Только админи
|
|||
error.namespace.admin.required=Требуется роль владельца или администратора пространства имён
|
||||
error.namespace.owner.required=Требуется роль владельца пространства имён
|
||||
error.namespace.create.platformAdminRequired=Создавать пространства имён могут только SKILL_ADMIN или SUPER_ADMIN
|
||||
error.namespace.delete.hasDependencies=Пространство имён нельзя удалить, пока в нём есть скиллы или записи управления
|
||||
error.namespace.delete.hasDependencies=Пространство имён нельзя удалить, пока в нём есть скиллы, черновики или записи управления
|
||||
error.namespace.member.owner.assignDirect=Нельзя назначить роль OWNER напрямую
|
||||
error.namespace.member.alreadyExists=Пользователь уже является участником пространства имён
|
||||
error.namespace.member.notFound=Участник не найден
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ error.namespace.global.members.platformAdmin.required=只有平台用户管理
|
|||
error.namespace.admin.required=需要命名空间管理员或所有者权限
|
||||
error.namespace.owner.required=需要命名空间所有者权限
|
||||
error.namespace.create.platformAdminRequired=只有 SKILL_ADMIN 或 SUPER_ADMIN 可以创建命名空间
|
||||
error.namespace.delete.hasDependencies=命名空间下仍有技能或治理记录,暂时不能删除
|
||||
error.namespace.delete.hasDependencies=命名空间下仍有技能、草稿或治理记录,暂时不能删除
|
||||
error.namespace.member.owner.assignDirect=不能直接分配 OWNER 角色
|
||||
error.namespace.member.alreadyExists=用户已经是该命名空间成员
|
||||
error.namespace.member.notFound=未找到命名空间成员
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceMember;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
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.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.service.authoring.ValidationRunOrchestrator;
|
||||
|
|
@ -93,6 +95,7 @@ class AuthoringDraftFlowIntegrationTest {
|
|||
@Autowired private ValidationRunService runService;
|
||||
@Autowired private ValidationRunOrchestrator orchestrator;
|
||||
@Autowired private NamespaceRepository namespaceRepository;
|
||||
@Autowired private NamespaceService namespaceService;
|
||||
@Autowired private NamespaceMemberRepository namespaceMemberRepository;
|
||||
@Autowired private UserAccountRepository userAccountRepository;
|
||||
|
||||
|
|
@ -377,6 +380,26 @@ class AuthoringDraftFlowIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void namespaceWithOnlyDraftRequiresDraftRemovalBeforeDeletion() {
|
||||
Namespace namespace = namespaceService.createNamespace("draft-delete-team", "Draft Delete Team", null, USER);
|
||||
SkillDraft draft = draftService.createDraft(namespace.getSlug(), USER, "namespace-delete-draft", null, null);
|
||||
|
||||
assertThat(namespaceService.canDelete(namespace, NamespaceRole.OWNER)).isFalse();
|
||||
DomainBadRequestException conflict = assertThrows(DomainBadRequestException.class,
|
||||
() -> namespaceService.deleteNamespace(namespace.getId(), USER));
|
||||
assertThat(conflict.messageCode()).isEqualTo("error.namespace.delete.hasDependencies");
|
||||
assertThat(namespaceRepository.findById(namespace.getId())).isPresent();
|
||||
assertThat(draftService.readFile(draft.getId(), USER, "SKILL.md", null).asText())
|
||||
.contains("name: namespace-delete-draft");
|
||||
|
||||
draftService.deleteDraft(draft.getId(), USER, null);
|
||||
assertThat(namespaceService.canDelete(namespace, NamespaceRole.OWNER)).isTrue();
|
||||
namespaceService.deleteNamespace(namespace.getId(), USER);
|
||||
assertThat(namespaceRepository.findById(namespace.getId())).isEmpty();
|
||||
assertThat(namespaceMemberRepository.findByNamespaceIdAndUserId(namespace.getId(), USER)).isEmpty();
|
||||
}
|
||||
|
||||
private <T> List<T> concurrently(int workers, Callable<T> action) throws Exception {
|
||||
CountDownLatch ready = new CountDownLatch(workers);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ public interface SkillDraftRepository {
|
|||
|
||||
List<SkillDraft> findByNamespaceId(Long namespaceId);
|
||||
|
||||
boolean existsByNamespaceId(Long namespaceId);
|
||||
|
||||
/** Case-insensitive uniqueness of (owner, name) used at creation time. */
|
||||
Optional<SkillDraft> findByOwnerIdAndNameIgnoreCase(String ownerId, String name);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.authoring.SkillDraftRepository;
|
||||
import com.iflytek.skillhub.domain.review.PromotionRequestRepository;
|
||||
import com.iflytek.skillhub.domain.review.ReviewTaskRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
|
|
@ -20,6 +21,7 @@ public class NamespaceService {
|
|||
private final NamespaceMemberRepository namespaceMemberRepository;
|
||||
private final NamespaceAccessPolicy namespaceAccessPolicy;
|
||||
private final SkillRepository skillRepository;
|
||||
private final SkillDraftRepository skillDraftRepository;
|
||||
private final ReviewTaskRepository reviewTaskRepository;
|
||||
private final PromotionRequestRepository promotionRequestRepository;
|
||||
|
||||
|
|
@ -27,12 +29,14 @@ public class NamespaceService {
|
|||
NamespaceMemberRepository namespaceMemberRepository,
|
||||
NamespaceAccessPolicy namespaceAccessPolicy,
|
||||
SkillRepository skillRepository,
|
||||
SkillDraftRepository skillDraftRepository,
|
||||
ReviewTaskRepository reviewTaskRepository,
|
||||
PromotionRequestRepository promotionRequestRepository) {
|
||||
this.namespaceRepository = namespaceRepository;
|
||||
this.namespaceMemberRepository = namespaceMemberRepository;
|
||||
this.namespaceAccessPolicy = namespaceAccessPolicy;
|
||||
this.skillRepository = skillRepository;
|
||||
this.skillDraftRepository = skillDraftRepository;
|
||||
this.reviewTaskRepository = reviewTaskRepository;
|
||||
this.promotionRequestRepository = promotionRequestRepository;
|
||||
}
|
||||
|
|
@ -181,7 +185,8 @@ public class NamespaceService {
|
|||
private boolean hasDependentData(Long namespaceId) {
|
||||
return skillRepository.existsByNamespaceId(namespaceId)
|
||||
|| reviewTaskRepository.existsByNamespaceId(namespaceId)
|
||||
|| promotionRequestRepository.existsByTargetNamespaceId(namespaceId);
|
||||
|| promotionRequestRepository.existsByTargetNamespaceId(namespaceId)
|
||||
|| skillDraftRepository.existsByNamespaceId(namespaceId);
|
||||
}
|
||||
|
||||
private void assertWritable(Namespace namespace) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.iflytek.skillhub.domain.namespace;
|
||||
|
||||
import com.iflytek.skillhub.domain.authoring.SkillDraftRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.review.PromotionRequestRepository;
|
||||
|
|
@ -33,6 +34,9 @@ class NamespaceServiceTest {
|
|||
@Mock
|
||||
private SkillRepository skillRepository;
|
||||
|
||||
@Mock
|
||||
private SkillDraftRepository skillDraftRepository;
|
||||
|
||||
@Mock
|
||||
private ReviewTaskRepository reviewTaskRepository;
|
||||
|
||||
|
|
@ -288,6 +292,26 @@ class NamespaceServiceTest {
|
|||
assertFalse(namespaceService.canDelete(namespace, NamespaceRole.OWNER));
|
||||
}
|
||||
|
||||
@Test
|
||||
void draftDependencyPreventsDeleteAndDisablesDeleteCapability() {
|
||||
Long namespaceId = 1L;
|
||||
String owner = "owner-1";
|
||||
Namespace namespace = new Namespace("draft-team", "Draft Team", owner);
|
||||
setField(namespace, "id", namespaceId);
|
||||
when(namespaceRepository.findById(namespaceId)).thenReturn(Optional.of(namespace));
|
||||
when(namespaceAccessPolicy.canDelete(namespace, NamespaceRole.OWNER)).thenReturn(true);
|
||||
when(namespaceMemberRepository.findByNamespaceIdAndUserId(namespaceId, owner))
|
||||
.thenReturn(Optional.of(new NamespaceMember(namespaceId, owner, NamespaceRole.OWNER)));
|
||||
when(skillDraftRepository.existsByNamespaceId(namespaceId)).thenReturn(true);
|
||||
|
||||
assertFalse(namespaceService.canDelete(namespace, NamespaceRole.OWNER));
|
||||
DomainBadRequestException exception = assertThrows(DomainBadRequestException.class,
|
||||
() -> namespaceService.deleteNamespace(namespaceId, owner));
|
||||
assertEquals("error.namespace.delete.hasDependencies", exception.messageCode());
|
||||
verify(namespaceMemberRepository, never()).deleteByNamespaceId(namespaceId);
|
||||
verify(namespaceRepository, never()).delete(any());
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public interface SkillDraftJpaRepository extends JpaRepository<SkillDraft, Long>
|
|||
|
||||
List<SkillDraft> findByNamespaceId(Long namespaceId);
|
||||
|
||||
boolean existsByNamespaceId(Long namespaceId);
|
||||
|
||||
Optional<SkillDraft> findByOwnerIdAndNameIgnoreCase(String ownerId, String name);
|
||||
|
||||
boolean existsByIdAndOwnerId(Long id, String ownerId);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue