mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-08 03:07:51 +00:00
feat(access): add SUPER_ADMIN platform role support
- Add platformRoles parameter to VisibilityChecker.canAccess() for platform-level access control - SUPER_ADMIN can access all skills regardless of visibility or publication status - Add archived namespace check to SkillQueryService.getSkillDetail() - Extract platformRoles from AuthContext in SkillController - Replace VisibilityChecker mock with real instance in SkillQueryServiceTest - Add 5 new tests for SUPER_ADMIN access scenarios - Add version-status-badge.tsx component for frontend status display Tests: 347 domain tests + 16 app tests passing
This commit is contained in:
parent
9a6f649e6f
commit
9801b549fc
9 changed files with 183 additions and 41 deletions
|
|
@ -70,10 +70,12 @@ public class SkillController extends BaseApiController {
|
|||
@PathVariable String namespace,
|
||||
@PathVariable String slug,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
@RequestAttribute(value = "platformRoles", required = false) java.util.Set<String> platformRoles) {
|
||||
|
||||
SkillQueryService.SkillDetailDTO detail = skillQueryService.getSkillDetail(
|
||||
namespace, slug, userId, userNsRoles != null ? userNsRoles : Map.of());
|
||||
namespace, slug, userId, userNsRoles != null ? userNsRoles : Map.of(),
|
||||
platformRoles != null ? platformRoles : java.util.Set.<String>of());
|
||||
|
||||
SkillDetailResponse response = new SkillDetailResponse(
|
||||
detail.id(),
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ public class AuthContextFilter extends OncePerRequestFilter {
|
|||
return;
|
||||
}
|
||||
request.setAttribute("userId", principal.userId());
|
||||
request.setAttribute("platformRoles", principal.platformRoles() != null ? principal.platformRoles() : java.util.Set.of());
|
||||
Map<Long, NamespaceRole> userNsRoles = namespaceMemberRepository.findByUserId(principal.userId()).stream()
|
||||
.collect(Collectors.toMap(
|
||||
NamespaceMember::getNamespaceId,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import java.util.Map;
|
|||
import java.util.TimeZone;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.anySet;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
|
@ -148,7 +149,8 @@ class SkillControllerTest {
|
|||
eq("team"),
|
||||
eq("demo"),
|
||||
eq((String) null),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
eq(Map.<Long, NamespaceRole>of()),
|
||||
org.mockito.ArgumentMatchers.<java.util.Set<String>>any()))
|
||||
.thenReturn(new SkillQueryService.SkillDetailDTO(
|
||||
1L,
|
||||
"demo",
|
||||
|
|
@ -195,7 +197,8 @@ class SkillControllerTest {
|
|||
eq("team"),
|
||||
eq("demo"),
|
||||
eq((String) null),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
eq(Map.<Long, NamespaceRole>of()),
|
||||
org.mockito.ArgumentMatchers.<java.util.Set<String>>any()))
|
||||
.thenThrow(new DomainForbiddenException("error.namespace.archived", "team"));
|
||||
|
||||
mockMvc.perform(get("/api/web/skills/team/demo"))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import static org.mockito.ArgumentMatchers.anyList;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.iflytek.skillhub.domain.skill.validation.PackageEntry;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
|
|
@ -69,7 +72,7 @@ class SkillPublishControllerTest {
|
|||
|
||||
given(skillPublishService.publishFromEntries(
|
||||
eq("global"),
|
||||
anyList(),
|
||||
ArgumentMatchers.<List<PackageEntry>>any(),
|
||||
eq("usr_1"),
|
||||
eq(SkillVisibility.PUBLIC),
|
||||
eq(Set.of("SUPER_ADMIN")),
|
||||
|
|
@ -120,7 +123,7 @@ class SkillPublishControllerTest {
|
|||
|
||||
given(skillPublishService.publishFromEntries(
|
||||
eq("global"),
|
||||
anyList(),
|
||||
ArgumentMatchers.<List<PackageEntry>>any(),
|
||||
eq("usr_1"),
|
||||
eq(SkillVisibility.PUBLIC),
|
||||
eq(Set.of("SUPER_ADMIN")),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.iflytek.skillhub.domain.skill;
|
|||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Evaluates whether a caller may read a skill based on publication state, visibility, ownership,
|
||||
|
|
@ -11,6 +12,13 @@ import java.util.Map;
|
|||
public class VisibilityChecker {
|
||||
|
||||
public boolean canAccess(Skill skill, String currentUserId, Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
return canAccess(skill, currentUserId, userNamespaceRoles, Set.of());
|
||||
}
|
||||
|
||||
public boolean canAccess(Skill skill, String currentUserId, Map<Long, NamespaceRole> userNamespaceRoles, Set<String> platformRoles) {
|
||||
if (isSuperAdmin(platformRoles)) {
|
||||
return true;
|
||||
}
|
||||
if (skill.isHidden()) {
|
||||
return isOwner(skill, currentUserId) || isAdminOrAbove(userNamespaceRoles.get(skill.getNamespaceId()));
|
||||
}
|
||||
|
|
@ -31,4 +39,8 @@ public class VisibilityChecker {
|
|||
private boolean isAdminOrAbove(NamespaceRole role) {
|
||||
return role == NamespaceRole.ADMIN || role == NamespaceRole.OWNER;
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(Set<String> platformRoles) {
|
||||
return platformRoles != null && platformRoles.contains("SUPER_ADMIN");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
|
@ -149,12 +150,28 @@ public class SkillQueryService {
|
|||
String skillSlug,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
return getSkillDetail(namespaceSlug, skillSlug, currentUserId, userNsRoles, Set.of());
|
||||
}
|
||||
|
||||
public SkillDetailDTO getSkillDetail(
|
||||
String namespaceSlug,
|
||||
String skillSlug,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles,
|
||||
Set<String> platformRoles) {
|
||||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Archived namespace: only members (or super admins) may view
|
||||
if (namespace.getStatus() == com.iflytek.skillhub.domain.namespace.NamespaceStatus.ARCHIVED
|
||||
&& !isNamespaceMember(namespace.getId(), currentUserId, userNsRoles)
|
||||
&& !isSuperAdmin(platformRoles)) {
|
||||
throw new DomainForbiddenException("error.namespace.archived", namespaceSlug);
|
||||
}
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles, platformRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +203,7 @@ public class SkillQueryService {
|
|||
skill.getNamespaceId(),
|
||||
skill.getCreatedAt(),
|
||||
skill.getUpdatedAt(),
|
||||
canManageRestrictedSkill(skill, currentUserId, userNsRoles),
|
||||
canManageRestrictedSkill(skill, currentUserId, userNsRoles, platformRoles),
|
||||
canSubmitPromotion(namespace, skill, publishedVersion, currentUserId, userNsRoles),
|
||||
headlineVersion == null || "PUBLISHED".equals(headlineVersion.status()),
|
||||
currentUserId == null || !Objects.equals(skill.getOwnerId(), currentUserId),
|
||||
|
|
@ -630,6 +647,13 @@ public class SkillQueryService {
|
|||
}
|
||||
|
||||
private boolean canManageRestrictedSkill(Skill skill, String currentUserId, Map<Long, NamespaceRole> userNsRoles) {
|
||||
return canManageRestrictedSkill(skill, currentUserId, userNsRoles, Set.of());
|
||||
}
|
||||
|
||||
private boolean canManageRestrictedSkill(Skill skill, String currentUserId, Map<Long, NamespaceRole> userNsRoles, Set<String> platformRoles) {
|
||||
if (platformRoles != null && platformRoles.contains("SUPER_ADMIN")) {
|
||||
return true;
|
||||
}
|
||||
if (currentUserId == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -671,6 +695,10 @@ public class SkillQueryService {
|
|||
return currentUserId != null && userNsRoles.containsKey(namespaceId);
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(Set<String> platformRoles) {
|
||||
return platformRoles != null && platformRoles.contains("SUPER_ADMIN");
|
||||
}
|
||||
|
||||
private String resolveOwnerPreviewReviewComment(SkillLifecycleProjectionService.VersionProjection ownerPreviewVersion) {
|
||||
if (ownerPreviewVersion == null || !"REJECTED".equals(ownerPreviewVersion.status())) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
|
@ -158,4 +159,34 @@ class VisibilityCheckerTest {
|
|||
boolean canAccess = checker.canAccess(hiddenPublicSkill, ADMIN_USER_ID, roles);
|
||||
assertTrue(canAccess);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuperAdminCanAccessPrivateSkill() {
|
||||
boolean canAccess = checker.canAccess(privateSkill, OTHER_USER_ID, Map.of(), Set.of("SUPER_ADMIN"));
|
||||
assertTrue(canAccess);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuperAdminCanAccessHiddenSkill() {
|
||||
boolean canAccess = checker.canAccess(hiddenPublicSkill, OTHER_USER_ID, Map.of(), Set.of("SUPER_ADMIN"));
|
||||
assertTrue(canAccess);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuperAdminCanAccessUnpublishedSkill() {
|
||||
boolean canAccess = checker.canAccess(unpublishedPublicSkill, OTHER_USER_ID, Map.of(), Set.of("SUPER_ADMIN"));
|
||||
assertTrue(canAccess);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNonSuperAdminPlatformRolesDoNotGrantAccess() {
|
||||
boolean canAccess = checker.canAccess(privateSkill, OTHER_USER_ID, Map.of(), Set.of("REVIEWER"));
|
||||
assertFalse(canAccess);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyPlatformRolesDoNotGrantAccess() {
|
||||
boolean canAccess = checker.canAccess(privateSkill, OTHER_USER_ID, Map.of(), Set.of());
|
||||
assertFalse(canAccess);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import java.lang.reflect.Field;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
|
@ -50,7 +51,6 @@ class SkillQueryServiceTest {
|
|||
private SkillTagRepository skillTagRepository;
|
||||
@Mock
|
||||
private ObjectStorageService objectStorageService;
|
||||
@Mock
|
||||
private VisibilityChecker visibilityChecker;
|
||||
@Mock
|
||||
private PromotionRequestRepository promotionRequestRepository;
|
||||
|
|
@ -65,6 +65,7 @@ class SkillQueryServiceTest {
|
|||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
visibilityChecker = new VisibilityChecker();
|
||||
skillSlugResolutionService = new SkillSlugResolutionService(skillRepository);
|
||||
skillLifecycleProjectionService = new SkillLifecycleProjectionService(skillVersionRepository);
|
||||
service = new SkillQueryService(
|
||||
|
|
@ -105,7 +106,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version));
|
||||
when(userAccountRepository.findById(userId)).thenReturn(Optional.of(new UserAccount(userId, "Alice", "alice@example.com", null)));
|
||||
|
||||
|
|
@ -148,7 +148,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(publishedSkill, ownSkill));
|
||||
when(visibilityChecker.canAccess(ownSkill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(22L)).thenReturn(Optional.of(ownVersion));
|
||||
|
||||
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles);
|
||||
|
|
@ -176,7 +175,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
|
|
@ -215,13 +213,11 @@ class SkillQueryServiceTest {
|
|||
setId(namespace, 1L);
|
||||
Skill skill1 = new Skill(1L, "skill1", userId, SkillVisibility.PUBLIC);
|
||||
setId(skill1, 1L);
|
||||
Skill skill2 = new Skill(1L, "skill2", userId, SkillVisibility.PRIVATE);
|
||||
Skill skill2 = new Skill(1L, "skill2", "user-200", SkillVisibility.PRIVATE);
|
||||
setId(skill2, 2L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndStatus(1L, SkillStatus.ACTIVE)).thenReturn(List.of(skill1, skill2));
|
||||
when(visibilityChecker.canAccess(skill1, userId, userNsRoles)).thenReturn(true);
|
||||
when(visibilityChecker.canAccess(skill2, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
// Act
|
||||
Page<Skill> result = service.listSkillsByNamespace(namespaceSlug, userId, userNsRoles, pageable);
|
||||
|
|
@ -267,8 +263,6 @@ class SkillQueryServiceTest {
|
|||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndStatus(1L, SkillStatus.ACTIVE))
|
||||
.thenReturn(List.of(ownUnpublishedSkill, othersUnpublishedSkill));
|
||||
when(visibilityChecker.canAccess(ownUnpublishedSkill, userId, userNsRoles)).thenReturn(true);
|
||||
when(visibilityChecker.canAccess(othersUnpublishedSkill, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
Page<Skill> result = service.listSkillsByNamespace(namespaceSlug, userId, userNsRoles, pageable);
|
||||
|
||||
|
|
@ -296,8 +290,6 @@ class SkillQueryServiceTest {
|
|||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndStatus(1L, SkillStatus.ACTIVE))
|
||||
.thenReturn(List.of(visibleSkill, hiddenSkill));
|
||||
when(visibilityChecker.canAccess(visibleSkill, userId, userNsRoles)).thenReturn(true);
|
||||
when(visibilityChecker.canAccess(hiddenSkill, userId, userNsRoles)).thenReturn(false);
|
||||
|
||||
Page<Skill> result = service.listSkillsByNamespace(namespaceSlug, userId, userNsRoles, pageable);
|
||||
|
||||
|
|
@ -325,7 +317,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
when(skillFileRepository.findByVersionId(1L)).thenReturn(List.of(file1));
|
||||
when(objectStorageService.exists("key1")).thenReturn(true);
|
||||
|
|
@ -358,7 +349,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, callerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
|
|
@ -385,7 +375,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
when(skillFileRepository.findByVersionId(1L)).thenReturn(List.of(file));
|
||||
when(objectStorageService.exists(file.getStorageKey())).thenReturn(true);
|
||||
|
|
@ -419,7 +408,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
when(skillFileRepository.findByVersionId(1L)).thenReturn(List.of(availableFile, missingFile));
|
||||
when(objectStorageService.exists("skills/1/1/SKILL.md")).thenReturn(true);
|
||||
|
|
@ -482,7 +470,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(skillVersion));
|
||||
|
||||
SkillQueryService.SkillVersionDetailDTO result = service.getVersionDetail(
|
||||
|
|
@ -516,7 +503,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(latestVersion));
|
||||
when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file));
|
||||
when(objectStorageService.exists("storage-key")).thenReturn(true);
|
||||
|
|
@ -555,7 +541,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(pending, published, rejected));
|
||||
|
||||
Page<SkillVersion> result = service.listVersions(namespaceSlug, skillSlug, ownerId, userNsRoles, PageRequest.of(0, 20));
|
||||
|
|
@ -589,7 +574,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, "user-100", userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of(version100, version110));
|
||||
when(skillVersionRepository.findById(10L)).thenReturn(Optional.of(version110));
|
||||
|
|
@ -631,7 +615,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, null, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(version));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(3L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(version));
|
||||
when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file));
|
||||
|
|
@ -664,7 +647,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
|
||||
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles);
|
||||
|
||||
|
|
@ -691,7 +673,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.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());
|
||||
|
|
@ -723,7 +704,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.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)));
|
||||
|
|
@ -753,7 +733,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.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))
|
||||
|
|
@ -784,7 +763,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(published));
|
||||
|
||||
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, userId, userNsRoles);
|
||||
|
|
@ -813,7 +791,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(12L)).thenReturn(Optional.of(pending));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED))
|
||||
.thenReturn(List.of());
|
||||
|
|
@ -853,7 +830,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(11L)).thenReturn(Optional.of(published));
|
||||
|
||||
SkillQueryService.SkillDetailDTO result = service.getSkillDetail(namespaceSlug, skillSlug, ownerId, userNsRoles);
|
||||
|
|
@ -889,7 +865,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findById(12L)).thenReturn(Optional.of(rejected));
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of());
|
||||
when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(rejected));
|
||||
|
|
@ -925,7 +900,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending));
|
||||
|
||||
SkillQueryService.SkillVersionDetailDTO result = service.getVersionDetail(
|
||||
|
|
@ -960,7 +934,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, ownerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending));
|
||||
when(skillFileRepository.findByVersionId(11L)).thenReturn(List.of(file));
|
||||
when(objectStorageService.exists("storage-key")).thenReturn(true);
|
||||
|
|
@ -992,7 +965,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, viewerId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, version)).thenReturn(Optional.of(pending));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () ->
|
||||
|
|
@ -1024,7 +996,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillId(1L)).thenReturn(List.of(rejected, draft, published));
|
||||
|
||||
Page<SkillVersion> result = service.listVersions(
|
||||
|
|
@ -1059,7 +1030,6 @@ class SkillQueryServiceTest {
|
|||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, userId, userNsRoles)).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndStatus(1L, SkillVersionStatus.PUBLISHED)).thenReturn(List.of(published));
|
||||
|
||||
Page<SkillVersion> result = service.listVersions(
|
||||
|
|
|
|||
92
web/src/features/skill/version-status-badge.tsx
Normal file
92
web/src/features/skill/version-status-badge.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/shared/lib/utils'
|
||||
|
||||
type VersionStatus =
|
||||
| 'DRAFT'
|
||||
| 'SCANNING'
|
||||
| 'SCAN_FAILED'
|
||||
| 'UPLOADED'
|
||||
| 'PENDING_REVIEW'
|
||||
| 'PUBLISHED'
|
||||
| 'REJECTED'
|
||||
| 'YANKED'
|
||||
|
||||
const statusStyles: Record<VersionStatus, string> = {
|
||||
PUBLISHED:
|
||||
'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
|
||||
UPLOADED:
|
||||
'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-400',
|
||||
PENDING_REVIEW:
|
||||
'border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400',
|
||||
REJECTED:
|
||||
'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-400',
|
||||
SCANNING:
|
||||
'border-purple-500/30 bg-purple-500/10 text-purple-700 dark:text-purple-400',
|
||||
SCAN_FAILED:
|
||||
'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-400',
|
||||
YANKED:
|
||||
'border-border/60 bg-secondary/40 text-muted-foreground',
|
||||
DRAFT:
|
||||
'border-border/60 bg-secondary/40 text-muted-foreground',
|
||||
}
|
||||
|
||||
const i18nKeys: Record<VersionStatus, string> = {
|
||||
DRAFT: 'skillDetail.versionStatusDraft',
|
||||
SCANNING: 'skillDetail.versionStatusScanning',
|
||||
SCAN_FAILED: 'skillDetail.versionStatusScanFailed',
|
||||
UPLOADED: 'skillDetail.versionStatusUploaded',
|
||||
PENDING_REVIEW: 'skillDetail.versionStatusPendingReview',
|
||||
PUBLISHED: 'skillDetail.versionStatusPublished',
|
||||
REJECTED: 'skillDetail.versionStatusRejected',
|
||||
YANKED: 'skillDetail.versionStatusYanked',
|
||||
}
|
||||
|
||||
/** Color-coded row styles (left-border + subtle background) for version cards. */
|
||||
export const versionRowStyles: Record<VersionStatus, string> = {
|
||||
UPLOADED:
|
||||
'border-l-[3px] !border-l-blue-500 bg-blue-500/[0.03]',
|
||||
PENDING_REVIEW:
|
||||
'border-l-[3px] !border-l-amber-500 bg-amber-500/[0.03]',
|
||||
REJECTED:
|
||||
'border-l-[3px] !border-l-red-500 bg-red-500/[0.04]',
|
||||
SCANNING:
|
||||
'border-l-[3px] !border-l-purple-500 bg-purple-500/[0.03]',
|
||||
SCAN_FAILED:
|
||||
'border-l-[3px] !border-l-red-500 bg-red-500/[0.04]',
|
||||
PUBLISHED: '',
|
||||
YANKED: '',
|
||||
DRAFT: '',
|
||||
}
|
||||
|
||||
export function getVersionRowStyle(status?: string): string {
|
||||
if (!status) return ''
|
||||
return versionRowStyles[status as VersionStatus] ?? ''
|
||||
}
|
||||
|
||||
export function VersionStatusBadge({
|
||||
status,
|
||||
className,
|
||||
}: {
|
||||
status?: string
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
if (!status) return null
|
||||
|
||||
const style = statusStyles[status as VersionStatus] ?? statusStyles.DRAFT
|
||||
const label = i18nKeys[status as VersionStatus]
|
||||
? t(i18nKeys[status as VersionStatus])
|
||||
: status
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium',
|
||||
style,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue