feat: 完善 Skill 管理与发布治理

- 实现标准资源存储、能力绑定及双格式导入导出

- 接入分类、可见范围、审批发布与资源权限校验

- 补充并发、租户隔离、安全边界和迁移契约测试
This commit is contained in:
2026-07-27 18:54:20 +08:00
parent aedefe6b5e
commit 2a9e882ac6
165 changed files with 23737 additions and 1088 deletions

View File

@@ -0,0 +1,202 @@
package tech.easyflow.system.service.impl;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.permission.resource.VisibilityResource;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysDeptService;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.Set;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* {@link ResourceAccessServiceImpl} 资源动作与可见范围回归测试。
*/
public class ResourceAccessServiceImplTest {
private CategoryPermissionService categoryPermissionService;
private SysDeptService sysDeptService;
private ResourceAccessServiceImpl service;
/**
* 初始化被测服务及权限依赖。
*
* @throws Exception 反射注入失败时抛出
*/
@Before
public void setUp() throws Exception {
categoryPermissionService = Mockito.mock(CategoryPermissionService.class);
sysDeptService = Mockito.mock(SysDeptService.class);
service = new ResourceAccessServiceImpl();
inject(service, "categoryPermissionService", categoryPermissionService);
inject(service, "sysDeptService", sysDeptService);
}
/**
* 验证创建者始终可以管理自己的 Skill。
*/
@Test
public void ownerShouldManageOwnResource() {
LoginAccount account = account(7, 70);
VisibilityResource resource = resource(7, 99, 700, VisibilityScope.PRIVATE);
assertTrue(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.MANAGE));
}
/**
* 验证非创建者即使能查看分类,也不能获得 MANAGE。
*/
@Test
public void nonOwnerShouldNotManageResource() {
LoginAccount account = account(8, 80);
VisibilityResource resource = resource(7, 80, 700, VisibilityScope.PUBLIC);
Mockito.when(categoryPermissionService.canAccessCategory(
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
.thenReturn(true);
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.MANAGE));
}
/**
* 验证 PUBLIC 仍必须先通过分类范围,避免公开标记绕过分类授权。
*/
@Test
public void publicResourceShouldStillRequireCategoryAccess() {
LoginAccount account = account(8, 80);
VisibilityResource resource = resource(7, 90, 700, VisibilityScope.PUBLIC);
Mockito.when(categoryPermissionService.canAccessCategory(
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
.thenReturn(false);
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
}
/**
* 验证部门可见资源使用现有部门树访问判定。
*/
@Test
public void departmentResourceShouldUseDepartmentAccess() {
LoginAccount account = account(8, 80);
VisibilityResource resource = resource(7, 90, 700, VisibilityScope.DEPT);
Mockito.when(categoryPermissionService.canAccessCategory(
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
.thenReturn(true);
Mockito.when(sysDeptService.canUserAccessDeptScopedResource(BigInteger.valueOf(80), BigInteger.valueOf(90)))
.thenReturn(true);
assertTrue(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
}
/**
* 验证 PRIVATE 对分类内其他用户仍不可见。
*/
@Test
public void privateResourceShouldStayPrivateWithinCategory() {
LoginAccount account = account(8, 80);
VisibilityResource resource = resource(7, 80, 700, VisibilityScope.PRIVATE);
Mockito.when(categoryPermissionService.canAccessCategory(
account, CategoryResourceType.SKILL.getCode(), BigInteger.valueOf(7), BigInteger.valueOf(700)))
.thenReturn(true);
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
}
/**
* 验证 Skill 分类 ALL 范围可以读取其他创建者的未分类私有草稿。
*/
@Test
public void allCategoryScopeShouldReadUnclassifiedPrivateSkill() {
LoginAccount account = account(8, 80);
VisibilityResource resource = new TestVisibilityResource(
BigInteger.ONE, BigInteger.valueOf(7), BigInteger.valueOf(90), null,
VisibilityScope.PRIVATE.name());
Mockito.when(categoryPermissionService.getAccess(CategoryResourceType.SKILL.getCode(), account))
.thenReturn(new RoleCategoryAccessSnapshot(
CategoryResourceType.SKILL.getCode(), account.getId(), false, true, Set.of()));
assertTrue(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.MANAGE));
}
/**
* 验证资源动作不能跨越租户边界,即使资源是公开状态。
*/
@Test
public void resourceShouldNeverCrossTenantBoundary() {
LoginAccount account = account(8, 80);
VisibilityResource resource = resource(7, 80, 700, VisibilityScope.PUBLIC, BigInteger.TWO);
assertFalse(service.canAccess(account, CategoryResourceType.SKILL, resource, ResourceAction.READ));
Mockito.verifyNoInteractions(categoryPermissionService, sysDeptService);
}
private LoginAccount account(long id, long deptId) {
LoginAccount account = new LoginAccount();
account.setId(BigInteger.valueOf(id));
account.setDeptId(BigInteger.valueOf(deptId));
account.setTenantId(BigInteger.ONE);
return account;
}
private VisibilityResource resource(long createdBy, long deptId, long categoryId, VisibilityScope scope) {
return resource(createdBy, deptId, categoryId, scope, BigInteger.ONE);
}
private VisibilityResource resource(long createdBy, long deptId, long categoryId,
VisibilityScope scope, BigInteger tenantId) {
return new TestVisibilityResource(
tenantId,
BigInteger.valueOf(createdBy),
BigInteger.valueOf(deptId),
BigInteger.valueOf(categoryId),
scope.name());
}
private void inject(Object target, String fieldName, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
private record TestVisibilityResource(BigInteger tenantId,
BigInteger createdBy,
BigInteger deptId,
BigInteger categoryId,
String visibilityScope) implements VisibilityResource {
@Override
public BigInteger getTenantId() {
return tenantId;
}
@Override
public BigInteger getCreatedBy() {
return createdBy;
}
@Override
public BigInteger getDeptId() {
return deptId;
}
@Override
public BigInteger getCategoryId() {
return categoryId;
}
@Override
public String getVisibilityScope() {
return visibilityScope;
}
}
}