From df5883ec04701f2557716931af2b7e58c0b7be94 Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <15071461069@163.com> Date: Mon, 16 Mar 2026 15:37:41 +0800 Subject: [PATCH] fix: stabilize skill promotion approval flow --- .../skillhub/service/MySkillAppService.java | 13 +++- .../service/MySkillAppServiceTest.java | 42 ++++++++++- .../review/PromotionRequestRepository.java | 1 + .../domain/review/PromotionService.java | 35 ++++++---- .../skill/service/SkillQueryService.java | 13 +++- .../domain/review/PromotionServiceTest.java | 43 ++++++++++-- .../skill/service/SkillQueryServiceTest.java | 70 ++++++++++++++++++- .../jpa/PromotionRequestJpaRepository.java | 4 +- 8 files changed, 198 insertions(+), 23 deletions(-) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/MySkillAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/MySkillAppService.java index 73c65ba0..ef64f336 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/MySkillAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/MySkillAppService.java @@ -3,6 +3,8 @@ package com.iflytek.skillhub.service; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceStatus; import com.iflytek.skillhub.domain.namespace.NamespaceType; +import com.iflytek.skillhub.domain.review.PromotionRequestRepository; +import com.iflytek.skillhub.domain.review.ReviewTaskStatus; import com.iflytek.skillhub.domain.skill.Skill; import com.iflytek.skillhub.domain.skill.SkillRepository; import com.iflytek.skillhub.domain.skill.SkillVersion; @@ -29,16 +31,19 @@ public class MySkillAppService { private final NamespaceRepository namespaceRepository; private final SkillVersionRepository skillVersionRepository; private final SkillStarRepository skillStarRepository; + private final PromotionRequestRepository promotionRequestRepository; public MySkillAppService( SkillRepository skillRepository, NamespaceRepository namespaceRepository, SkillVersionRepository skillVersionRepository, - SkillStarRepository skillStarRepository) { + SkillStarRepository skillStarRepository, + PromotionRequestRepository promotionRequestRepository) { this.skillRepository = skillRepository; this.namespaceRepository = namespaceRepository; this.skillVersionRepository = skillVersionRepository; this.skillStarRepository = skillStarRepository; + this.promotionRequestRepository = promotionRequestRepository; } public List listMySkills(String userId) { @@ -150,6 +155,12 @@ public class MySkillAppService { if (namespace.getStatus() != NamespaceStatus.ACTIVE || skill.getStatus() != com.iflytek.skillhub.domain.skill.SkillStatus.ACTIVE) { return false; } + if (promotionRequestRepository.findBySourceSkillIdAndStatus(skill.getId(), ReviewTaskStatus.PENDING).isPresent()) { + return false; + } + if (promotionRequestRepository.findBySourceSkillIdAndStatus(skill.getId(), ReviewTaskStatus.APPROVED).isPresent()) { + return false; + } return latestVersion != null && latestVersion.getStatus() == SkillVersionStatus.PUBLISHED; } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java index 352f26d1..6802de72 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/MySkillAppServiceTest.java @@ -2,6 +2,9 @@ package com.iflytek.skillhub.service; import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; +import com.iflytek.skillhub.domain.review.PromotionRequest; +import com.iflytek.skillhub.domain.review.PromotionRequestRepository; +import com.iflytek.skillhub.domain.review.ReviewTaskStatus; import com.iflytek.skillhub.domain.skill.Skill; import com.iflytek.skillhub.domain.skill.SkillRepository; import com.iflytek.skillhub.domain.skill.SkillVersion; @@ -43,11 +46,20 @@ class MySkillAppServiceTest { @Mock private SkillStarRepository skillStarRepository; + @Mock + private PromotionRequestRepository promotionRequestRepository; + private MySkillAppService service; @BeforeEach void setUp() { - service = new MySkillAppService(skillRepository, namespaceRepository, skillVersionRepository, skillStarRepository); + service = new MySkillAppService( + skillRepository, + namespaceRepository, + skillVersionRepository, + skillStarRepository, + promotionRequestRepository + ); } @Test @@ -134,6 +146,8 @@ class MySkillAppServiceTest { given(skillRepository.findByOwnerId("user-1")).willReturn(List.of(skill)); given(skillVersionRepository.findBySkillIdIn(List.of(2L))).willReturn(List.of(publishedVersion)); given(namespaceRepository.findByIdIn(List.of(101L))).willReturn(List.of(namespace)); + given(promotionRequestRepository.findBySourceSkillIdAndStatus(2L, ReviewTaskStatus.PENDING)).willReturn(Optional.empty()); + given(promotionRequestRepository.findBySourceSkillIdAndStatus(2L, ReviewTaskStatus.APPROVED)).willReturn(Optional.empty()); var skills = service.listMySkills("user-1"); @@ -142,4 +156,30 @@ class MySkillAppServiceTest { assertThat(skills.get(0).latestVersionStatus()).isEqualTo("PUBLISHED"); assertThat(skills.get(0).canSubmitPromotion()).isTrue(); } + + @Test + void listMySkills_hidesPromotionWhenPendingRequestExists() { + Skill skill = new Skill(101L, "team-skill", "user-1", SkillVisibility.PUBLIC); + skill.setDisplayName("Team Skill"); + ReflectionTestUtils.setField(skill, "id", 2L); + + SkillVersion publishedVersion = new SkillVersion(2L, "1.2.0", "user-1"); + publishedVersion.setStatus(SkillVersionStatus.PUBLISHED); + ReflectionTestUtils.setField(publishedVersion, "id", 22L); + ReflectionTestUtils.setField(publishedVersion, "createdAt", LocalDateTime.of(2026, 3, 15, 10, 30)); + + Namespace namespace = new Namespace("team-ai", "Team AI", "user-1"); + ReflectionTestUtils.setField(namespace, "id", 101L); + + given(skillRepository.findByOwnerId("user-1")).willReturn(List.of(skill)); + given(skillVersionRepository.findBySkillIdIn(List.of(2L))).willReturn(List.of(publishedVersion)); + given(namespaceRepository.findByIdIn(List.of(101L))).willReturn(List.of(namespace)); + given(promotionRequestRepository.findBySourceSkillIdAndStatus(2L, ReviewTaskStatus.PENDING)) + .willReturn(Optional.of(new PromotionRequest(2L, 22L, 999L, "user-1"))); + + var skills = service.listMySkills("user-1"); + + assertThat(skills).hasSize(1); + assertThat(skills.get(0).canSubmitPromotion()).isFalse(); + } } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java index 51ff4860..4275959b 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java @@ -8,6 +8,7 @@ public interface PromotionRequestRepository { PromotionRequest save(PromotionRequest request); Optional findById(Long id); Optional findBySourceVersionIdAndStatus(Long sourceVersionId, ReviewTaskStatus status); + Optional findBySourceSkillIdAndStatus(Long sourceSkillId, ReviewTaskStatus status); Page findByStatus(ReviewTaskStatus status, Pageable pageable); int updateStatusWithVersion(Long id, ReviewTaskStatus status, String reviewedBy, String reviewComment, Long targetSkillId, Integer expectedVersion); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java index 1809aa9e..733f3350 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionService.java @@ -85,10 +85,14 @@ public class PromotionService { throw new DomainBadRequestException("promotion.target_not_global", targetNamespaceId); } - promotionRequestRepository.findBySourceVersionIdAndStatus(sourceVersionId, ReviewTaskStatus.PENDING) + promotionRequestRepository.findBySourceSkillIdAndStatus(sourceSkillId, ReviewTaskStatus.PENDING) .ifPresent(existing -> { throw new DomainBadRequestException("promotion.duplicate_pending", sourceVersionId); }); + promotionRequestRepository.findBySourceSkillIdAndStatus(sourceSkillId, ReviewTaskStatus.APPROVED) + .ifPresent(existing -> { + throw new DomainBadRequestException("promotion.already_promoted", sourceSkillId); + }); PromotionRequest request = new PromotionRequest(sourceSkillId, sourceVersionId, targetNamespaceId, userId); return promotionRequestRepository.save(request); @@ -127,10 +131,14 @@ public class PromotionService { throw new DomainBadRequestException("promotion.target_not_global", targetNamespaceId); } - promotionRequestRepository.findBySourceVersionIdAndStatus(sourceVersionId, ReviewTaskStatus.PENDING) + promotionRequestRepository.findBySourceSkillIdAndStatus(sourceSkillId, ReviewTaskStatus.PENDING) .ifPresent(existing -> { throw new DomainBadRequestException("promotion.duplicate_pending", sourceVersionId); }); + promotionRequestRepository.findBySourceSkillIdAndStatus(sourceSkillId, ReviewTaskStatus.APPROVED) + .ifPresent(existing -> { + throw new DomainBadRequestException("promotion.already_promoted", sourceSkillId); + }); PromotionRequest request = new PromotionRequest(sourceSkillId, sourceVersionId, targetNamespaceId, userId); return promotionRequestRepository.save(request); @@ -156,14 +164,17 @@ public class PromotionService { throw new ConcurrentModificationException("Promotion request was modified concurrently"); } - Skill sourceSkill = skillRepository.findById(request.getSourceSkillId()) - .orElseThrow(() -> new DomainNotFoundException("skill.not_found", request.getSourceSkillId())); + PromotionRequest approvedRequest = promotionRequestRepository.findById(promotionId) + .orElseThrow(() -> new DomainNotFoundException("promotion.not_found", promotionId)); - SkillVersion sourceVersion = skillVersionRepository.findById(request.getSourceVersionId()) - .orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", request.getSourceVersionId())); + Skill sourceSkill = skillRepository.findById(approvedRequest.getSourceSkillId()) + .orElseThrow(() -> new DomainNotFoundException("skill.not_found", approvedRequest.getSourceSkillId())); + + SkillVersion sourceVersion = skillVersionRepository.findById(approvedRequest.getSourceVersionId()) + .orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", approvedRequest.getSourceVersionId())); // Create new skill in global namespace - Skill newSkill = new Skill(request.getTargetNamespaceId(), sourceSkill.getSlug(), + Skill newSkill = new Skill(approvedRequest.getTargetNamespaceId(), sourceSkill.getSlug(), sourceSkill.getOwnerId(), SkillVisibility.PUBLIC); newSkill.setDisplayName(sourceSkill.getDisplayName()); newSkill.setSummary(sourceSkill.getSummary()); @@ -189,7 +200,7 @@ public class PromotionService { skillRepository.save(newSkill); // Copy file records (reuse storageKey) - List sourceFiles = skillFileRepository.findByVersionId(request.getSourceVersionId()); + List sourceFiles = skillFileRepository.findByVersionId(approvedRequest.getSourceVersionId()); Long newVersionId = newVersion.getId(); List copiedFiles = sourceFiles.stream() .map(f -> new SkillFile(newVersionId, f.getFilePath(), f.getFileSize(), @@ -198,13 +209,13 @@ public class PromotionService { skillFileRepository.saveAll(copiedFiles); // Update promotion request with target skill id - request.setTargetSkillId(newSkill.getId()); - promotionRequestRepository.save(request); + approvedRequest.setTargetSkillId(newSkill.getId()); + PromotionRequest savedRequest = promotionRequestRepository.save(approvedRequest); eventPublisher.publishEvent(new SkillPublishedEvent( newSkill.getId(), newVersion.getId(), reviewerId)); governanceNotificationService.notifyUser( - request.getSubmittedBy(), + approvedRequest.getSubmittedBy(), "PROMOTION", "PROMOTION_REQUEST", promotionId, @@ -212,7 +223,7 @@ public class PromotionService { "{\"status\":\"APPROVED\"}" ); - return request; + return savedRequest; } @Transactional diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java index a1827346..32e84fa6 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java @@ -5,6 +5,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceStatus; import com.iflytek.skillhub.domain.namespace.NamespaceType; +import com.iflytek.skillhub.domain.review.PromotionRequestRepository; +import com.iflytek.skillhub.domain.review.ReviewTaskStatus; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; import com.iflytek.skillhub.domain.skill.*; @@ -37,6 +39,7 @@ public class SkillQueryService { private final SkillTagRepository skillTagRepository; private final ObjectStorageService objectStorageService; private final VisibilityChecker visibilityChecker; + private final PromotionRequestRepository promotionRequestRepository; public SkillQueryService( NamespaceRepository namespaceRepository, @@ -45,7 +48,8 @@ public class SkillQueryService { SkillFileRepository skillFileRepository, SkillTagRepository skillTagRepository, ObjectStorageService objectStorageService, - VisibilityChecker visibilityChecker) { + VisibilityChecker visibilityChecker, + PromotionRequestRepository promotionRequestRepository) { this.namespaceRepository = namespaceRepository; this.skillRepository = skillRepository; this.skillVersionRepository = skillVersionRepository; @@ -53,6 +57,7 @@ public class SkillQueryService { this.skillTagRepository = skillTagRepository; this.objectStorageService = objectStorageService; this.visibilityChecker = visibilityChecker; + this.promotionRequestRepository = promotionRequestRepository; } public record SkillDetailDTO( @@ -501,6 +506,12 @@ public class SkillQueryService { if (latestVersionEntity == null || latestVersionEntity.getStatus() != SkillVersionStatus.PUBLISHED) { return false; } + if (promotionRequestRepository.findBySourceSkillIdAndStatus(skill.getId(), ReviewTaskStatus.PENDING).isPresent()) { + return false; + } + if (promotionRequestRepository.findBySourceSkillIdAndStatus(skill.getId(), ReviewTaskStatus.APPROVED).isPresent()) { + return false; + } return canManageRestrictedSkill(skill, currentUserId, userNsRoles); } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java index 863b47fb..c5fd9e83 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/PromotionServiceTest.java @@ -133,7 +133,9 @@ class PromotionServiceTest { when(namespaceRepository.findById(sourceSkill.getNamespaceId())).thenReturn(Optional.of(createSourceNamespace())); when(permissionChecker.canSubmitPromotion(sourceSkill, USER_ID, Map.of())).thenReturn(true); when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.of(globalNs)); - when(promotionRequestRepository.findBySourceVersionIdAndStatus(SOURCE_VERSION_ID, ReviewTaskStatus.PENDING)) + when(promotionRequestRepository.findBySourceSkillIdAndStatus(SOURCE_SKILL_ID, ReviewTaskStatus.PENDING)) + .thenReturn(Optional.empty()); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(SOURCE_SKILL_ID, ReviewTaskStatus.APPROVED)) .thenReturn(Optional.empty()); when(promotionRequestRepository.save(any(PromotionRequest.class))) .thenAnswer(inv -> { @@ -230,13 +232,33 @@ class PromotionServiceTest { when(namespaceRepository.findById(sourceSkill.getNamespaceId())).thenReturn(Optional.of(createSourceNamespace())); when(permissionChecker.canSubmitPromotion(sourceSkill, USER_ID, Map.of())).thenReturn(true); when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.of(createGlobalNamespace())); - when(promotionRequestRepository.findBySourceVersionIdAndStatus(SOURCE_VERSION_ID, ReviewTaskStatus.PENDING)) + when(promotionRequestRepository.findBySourceSkillIdAndStatus(SOURCE_SKILL_ID, ReviewTaskStatus.PENDING)) .thenReturn(Optional.of(createPendingPromotion())); assertThrows(DomainBadRequestException.class, () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of())); } + @Test + void shouldThrowWhenSkillAlreadyPromoted() { + Skill sourceSkill = createSourceSkill(); + PromotionRequest approvedPromotion = createPendingPromotion(); + setField(approvedPromotion, "status", ReviewTaskStatus.APPROVED); + + when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(sourceSkill)); + when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(createPublishedVersion())); + when(namespaceRepository.findById(sourceSkill.getNamespaceId())).thenReturn(Optional.of(createSourceNamespace())); + when(permissionChecker.canSubmitPromotion(sourceSkill, USER_ID, Map.of())).thenReturn(true); + when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.of(createGlobalNamespace())); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(SOURCE_SKILL_ID, ReviewTaskStatus.PENDING)) + .thenReturn(Optional.empty()); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(SOURCE_SKILL_ID, ReviewTaskStatus.APPROVED)) + .thenReturn(Optional.of(approvedPromotion)); + + assertThrows(DomainBadRequestException.class, + () -> promotionService.submitPromotion(SOURCE_SKILL_ID, SOURCE_VERSION_ID, TARGET_NAMESPACE_ID, USER_ID, Map.of())); + } + @Test void shouldThrowWhenSubmitterIsNotOwnerOrNamespaceAdmin() { Skill sourceSkill = createSourceSkill(); @@ -277,7 +299,9 @@ class PromotionServiceTest { Map.of(sourceSkill.getNamespaceId(), com.iflytek.skillhub.domain.namespace.NamespaceRole.ADMIN))) .thenReturn(true); when(namespaceRepository.findById(TARGET_NAMESPACE_ID)).thenReturn(Optional.of(globalNs)); - when(promotionRequestRepository.findBySourceVersionIdAndStatus(SOURCE_VERSION_ID, ReviewTaskStatus.PENDING)) + when(promotionRequestRepository.findBySourceSkillIdAndStatus(SOURCE_SKILL_ID, ReviewTaskStatus.PENDING)) + .thenReturn(Optional.empty()); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(SOURCE_SKILL_ID, ReviewTaskStatus.APPROVED)) .thenReturn(Optional.empty()); when(promotionRequestRepository.save(any(PromotionRequest.class))) .thenAnswer(inv -> inv.getArgument(0)); @@ -362,11 +386,17 @@ class PromotionServiceTest { @Test void shouldApprovePromotionSuccessfully() { PromotionRequest pr = createPendingPromotion(); + PromotionRequest approvedPromotion = createPendingPromotion(); + setField(approvedPromotion, "status", ReviewTaskStatus.APPROVED); + setField(approvedPromotion, "version", 2); + setField(approvedPromotion, "reviewedBy", REVIEWER_ID); + setField(approvedPromotion, "reviewComment", "LGTM"); Skill sourceSkill = createSourceSkill(); SkillVersion sourceVersion = createPublishedVersion(); List sourceFiles = createSourceFiles(); - when(promotionRequestRepository.findById(PROMOTION_ID)).thenReturn(Optional.of(pr)); + when(promotionRequestRepository.findById(PROMOTION_ID)) + .thenReturn(Optional.of(pr), Optional.of(approvedPromotion)); when(permissionChecker.canReviewPromotion(pr, REVIEWER_ID, Set.of("SKILL_ADMIN"))).thenReturn(true); when(promotionRequestRepository.updateStatusWithVersion( PROMOTION_ID, ReviewTaskStatus.APPROVED, REVIEWER_ID, "LGTM", null, pr.getVersion())) @@ -385,6 +415,7 @@ class PromotionServiceTest { }); when(skillFileRepository.findByVersionId(SOURCE_VERSION_ID)).thenReturn(sourceFiles); when(skillFileRepository.saveAll(anyList())).thenAnswer(inv -> inv.getArgument(0)); + when(promotionRequestRepository.save(approvedPromotion)).thenReturn(approvedPromotion); PromotionRequest result = promotionService.approvePromotion( PROMOTION_ID, REVIEWER_ID, "LGTM", Set.of("SKILL_ADMIN")); @@ -431,8 +462,8 @@ class PromotionServiceTest { assertEquals(REVIEWER_ID, event.publisherId()); // Verify targetSkillId updated on promotion request - verify(promotionRequestRepository).save(pr); - assertEquals(NEW_SKILL_ID, pr.getTargetSkillId()); + verify(promotionRequestRepository).save(approvedPromotion); + assertEquals(NEW_SKILL_ID, approvedPromotion.getTargetSkillId()); } @Test diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java index 9ef0ec6c..79866b58 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java @@ -4,6 +4,8 @@ import com.iflytek.skillhub.domain.namespace.Namespace; import com.iflytek.skillhub.domain.namespace.NamespaceRepository; import com.iflytek.skillhub.domain.namespace.NamespaceRole; import com.iflytek.skillhub.domain.namespace.NamespaceStatus; +import com.iflytek.skillhub.domain.review.PromotionRequestRepository; +import com.iflytek.skillhub.domain.review.ReviewTaskStatus; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; import com.iflytek.skillhub.domain.skill.*; @@ -46,6 +48,8 @@ class SkillQueryServiceTest { private ObjectStorageService objectStorageService; @Mock private VisibilityChecker visibilityChecker; + @Mock + private PromotionRequestRepository promotionRequestRepository; private SkillQueryService service; @@ -58,7 +62,8 @@ class SkillQueryServiceTest { skillFileRepository, skillTagRepository, objectStorageService, - visibilityChecker + visibilityChecker, + promotionRequestRepository ); } @@ -485,6 +490,8 @@ class SkillQueryServiceTest { when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(published)); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(1L, ReviewTaskStatus.PENDING)).thenReturn(Optional.empty()); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(1L, ReviewTaskStatus.APPROVED)).thenReturn(Optional.empty()); SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles); @@ -492,6 +499,67 @@ class SkillQueryServiceTest { assertTrue(result.canSubmitPromotion()); } + @Test + void testGetSkillDetail_ShouldHidePromotionWhenPendingPromotionExists() throws Exception { + String namespaceSlug = "team-ns"; + String skillSlug = "team-skill"; + String userId = "owner-1"; + Map userNsRoles = Map.of(); + + Namespace namespace = new Namespace(namespaceSlug, "Team NS", userId); + setId(namespace, 1L); + Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC); + setId(skill, 1L); + skill.setStatus(SkillStatus.ACTIVE); + skill.setLatestVersionId(11L); + + SkillVersion published = new SkillVersion(1L, "1.0.0", userId); + setId(published, 11L); + published.setStatus(SkillVersionStatus.PUBLISHED); + + when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); + when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(published)); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(1L, ReviewTaskStatus.PENDING)) + .thenReturn(Optional.of(mock(com.iflytek.skillhub.domain.review.PromotionRequest.class))); + + SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles); + + assertFalse(result.canSubmitPromotion()); + } + + @Test + void testGetSkillDetail_ShouldHidePromotionWhenSkillAlreadyPromoted() throws Exception { + String namespaceSlug = "team-ns"; + String skillSlug = "team-skill"; + String userId = "owner-1"; + Map userNsRoles = Map.of(); + + Namespace namespace = new Namespace(namespaceSlug, "Team NS", userId); + setId(namespace, 1L); + Skill skill = new Skill(1L, skillSlug, userId, SkillVisibility.PUBLIC); + setId(skill, 1L); + skill.setStatus(SkillStatus.ACTIVE); + skill.setLatestVersionId(11L); + + SkillVersion published = new SkillVersion(1L, "1.0.0", userId); + setId(published, 11L); + published.setStatus(SkillVersionStatus.PUBLISHED); + + when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace)); + when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill)); + when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true); + when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(published)); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(1L, ReviewTaskStatus.PENDING)).thenReturn(Optional.empty()); + when(promotionRequestRepository.findBySourceSkillIdAndStatus(1L, ReviewTaskStatus.APPROVED)) + .thenReturn(Optional.of(mock(com.iflytek.skillhub.domain.review.PromotionRequest.class))); + + SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles); + + assertFalse(result.canSubmitPromotion()); + } + @Test void testGetSkillDetail_ShouldNotFlagLifecyclePermissionForRegularViewer() throws Exception { String namespaceSlug = "test-ns"; diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java index a7b91107..88cf99ea 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java @@ -18,9 +18,11 @@ public interface PromotionRequestJpaRepository extends JpaRepository findBySourceVersionIdAndStatus(Long sourceVersionId, ReviewTaskStatus status); + Optional findBySourceSkillIdAndStatus(Long sourceSkillId, ReviewTaskStatus status); + Page findByStatus(ReviewTaskStatus status, Pageable pageable); - @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" UPDATE PromotionRequest p SET p.status = :status,