diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java index fa09efc2e..9851909cb 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/policy/RouteSecurityPolicyRegistryTest.java @@ -131,6 +131,11 @@ class RouteSecurityPolicyRegistryTest { var deniedAttach = registry.authorizeApiToken("PUT", path, Set.of("skill:read")); assertFalse(deniedAttach.allowed()); assertEquals("skill:publish", deniedAttach.requiredScope()); + var deniedDetach = registry.authorizeApiToken("DELETE", path, Set.of("skill:read")); + assertFalse(deniedDetach.allowed()); + assertEquals("skill:publish", deniedDetach.requiredScope()); + assertFalse(registry.authorizeApiToken("PUT", path, Set.of()).allowed()); + assertFalse(registry.authorizeApiToken("DELETE", path, Set.of()).allowed()); assertTrue(registry.authorizeApiToken("PUT", path, Set.of("skill:publish")).allowed()); assertTrue(registry.authorizeApiToken("DELETE", path, Set.of("skill:publish")).allowed()); } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/label/SkillLabelServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/label/SkillLabelServiceTest.java index ea3db73d9..e5c777de3 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/label/SkillLabelServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/label/SkillLabelServiceTest.java @@ -1,11 +1,27 @@ package com.iflytek.skillhub.domain.label; import com.iflytek.skillhub.domain.skill.SkillRepository; +import com.iflytek.skillhub.domain.skill.Skill; +import com.iflytek.skillhub.domain.skill.SkillVisibility; +import com.iflytek.skillhub.domain.namespace.NamespaceRole; +import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; class SkillLabelServiceTest { @@ -26,4 +42,82 @@ class SkillLabelServiceTest { assertEquals("skillhub.label.max-per-skill must be greater than 0", ex.getMessage()); } + + @ParameterizedTest(name = "{0} attach {4}: allowed={5}") + @MethodSource("skillLabelPermissions") + void attachUsesActualPermissionChecker(String role, String userId, + Map namespaceRoles, + Set platformRoles, LabelType type, + boolean allowed) throws Exception { + SkillLabelService service = serviceWithRealChecker(type); + if (!allowed) { + assertThrows(DomainForbiddenException.class, () -> service.attachLabel( + 10L, "automation", userId, namespaceRoles, platformRoles)); + verifyNoInteractions(skillLabelRepository); + return; + } + when(skillLabelRepository.findBySkillId(10L)).thenReturn(List.of()); + when(skillLabelRepository.findBySkillIdAndLabelId(10L, 20L)).thenReturn(Optional.empty()); + when(skillLabelRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + SkillLabel attached = service.attachLabel(10L, "automation", userId, namespaceRoles, platformRoles); + + assertEquals(10L, attached.getSkillId()); + assertEquals(20L, attached.getLabelId()); + assertEquals(userId, attached.getCreatedBy()); + verify(skillLabelRepository).save(attached); + } + + @ParameterizedTest(name = "{0} detach {4}: allowed={5}") + @MethodSource("skillLabelPermissions") + void detachUsesActualPermissionChecker(String role, String userId, + Map namespaceRoles, + Set platformRoles, LabelType type, + boolean allowed) throws Exception { + SkillLabelService service = serviceWithRealChecker(type); + if (!allowed) { + assertThrows(DomainForbiddenException.class, () -> service.detachLabel( + 10L, "automation", userId, namespaceRoles, platformRoles)); + verifyNoInteractions(skillLabelRepository); + return; + } + SkillLabel attached = new SkillLabel(10L, 20L, "original-author"); + when(skillLabelRepository.findBySkillIdAndLabelId(10L, 20L)).thenReturn(Optional.of(attached)); + + service.detachLabel(10L, "automation", userId, namespaceRoles, platformRoles); + + verify(skillLabelRepository).delete(attached); + } + + private SkillLabelService serviceWithRealChecker(LabelType type) throws Exception { + Skill skill = new Skill(1L, "demo", "author", SkillVisibility.PUBLIC); + LabelDefinition label = new LabelDefinition("automation", type, true, 0, "creator"); + setId(skill, 10L); + setId(label, 20L); + when(skillRepository.findById(10L)).thenReturn(Optional.of(skill)); + when(labelDefinitionRepository.findBySlugIgnoreCase("automation")).thenReturn(Optional.of(label)); + return new SkillLabelService(skillRepository, labelDefinitionRepository, skillLabelRepository, + new LabelPermissionChecker(), 10); + } + + private static Stream skillLabelPermissions() { + return Stream.of(LabelType.RECOMMENDED, LabelType.PRIVILEGED).flatMap(type -> Stream.of( + Arguments.of("skill owner", "author", Map.of(1L, NamespaceRole.MEMBER), Set.of(), type, + type == LabelType.RECOMMENDED), + Arguments.of("namespace admin", "admin", Map.of(1L, NamespaceRole.ADMIN), Set.of(), type, + type == LabelType.RECOMMENDED), + Arguments.of("namespace owner", "ns-owner", Map.of(1L, NamespaceRole.OWNER), Set.of(), type, + type == LabelType.RECOMMENDED), + Arguments.of("other namespace admin", "other-admin", Map.of(2L, NamespaceRole.ADMIN), Set.of(), type, false), + Arguments.of("ordinary member", "member", Map.of(1L, NamespaceRole.MEMBER), Set.of(), type, false), + Arguments.of("platform skill admin", "skill-admin", Map.of(), Set.of("SKILL_ADMIN"), type, false), + Arguments.of("super admin", "super-admin", Map.of(), Set.of("SUPER_ADMIN"), type, true))); + } + + private void setId(Object entity, Long id) throws Exception { + var field = entity.getClass().getDeclaredField("id"); + field.setAccessible(true); + field.set(entity, id); + } + }