fix: stabilize skill promotion approval flow

This commit is contained in:
yun-zhi-ztl 2026-03-16 15:37:41 +08:00 committed by Xudong Sun
parent 0b85bf1e89
commit df5883ec04
8 changed files with 198 additions and 23 deletions

View file

@ -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<SkillSummaryResponse> 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;
}

View file

@ -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();
}
}

View file

@ -8,6 +8,7 @@ public interface PromotionRequestRepository {
PromotionRequest save(PromotionRequest request);
Optional<PromotionRequest> findById(Long id);
Optional<PromotionRequest> findBySourceVersionIdAndStatus(Long sourceVersionId, ReviewTaskStatus status);
Optional<PromotionRequest> findBySourceSkillIdAndStatus(Long sourceSkillId, ReviewTaskStatus status);
Page<PromotionRequest> findByStatus(ReviewTaskStatus status, Pageable pageable);
int updateStatusWithVersion(Long id, ReviewTaskStatus status, String reviewedBy,
String reviewComment, Long targetSkillId, Integer expectedVersion);

View file

@ -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<SkillFile> sourceFiles = skillFileRepository.findByVersionId(request.getSourceVersionId());
List<SkillFile> sourceFiles = skillFileRepository.findByVersionId(approvedRequest.getSourceVersionId());
Long newVersionId = newVersion.getId();
List<SkillFile> 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

View file

@ -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);
}

View file

@ -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<SkillFile> 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

View file

@ -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<Long, NamespaceRole> 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<Long, NamespaceRole> 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";

View file

@ -18,9 +18,11 @@ public interface PromotionRequestJpaRepository extends JpaRepository<PromotionRe
Optional<PromotionRequest> findBySourceVersionIdAndStatus(Long sourceVersionId, ReviewTaskStatus status);
Optional<PromotionRequest> findBySourceSkillIdAndStatus(Long sourceSkillId, ReviewTaskStatus status);
Page<PromotionRequest> findByStatus(ReviewTaskStatus status, Pageable pageable);
@Modifying
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
UPDATE PromotionRequest p
SET p.status = :status,