feat: 完善 Skill 管理与发布治理
- 实现标准资源存储、能力绑定及双格式导入导出 - 接入分类、可见范围、审批发布与资源权限校验 - 补充并发、租户隔离、安全边界和迁移契约测试
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user