菜单修改优化

This commit is contained in:
supernijia
2026-07-28 13:40:19 +08:00
parent 4ddb8b47b0
commit 7c3c9e53a0
90 changed files with 9388 additions and 1545 deletions
@@ -0,0 +1,251 @@
package com.nanri.aiimage.modules.permission.controller;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.dto.ImageVideoDataPermissionUpdateRequest;
import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.permission.model.vo.ImageVideoDataPermissionUserVo;
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class PermissionMenuControllerTest {
@Test
void effectivePermissionEndpointAllowsAuthenticatedNormalUserToReadSelf() {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
AdminUserEntity currentUser = new AdminUserEntity();
currentUser.setId(9L);
currentUser.setRole("normal");
PermissionMenuItemVo item = new PermissionMenuItemVo();
item.setId(1L);
when(authSupport.requireUser(request)).thenReturn(currentUser);
when(service.getUserColumnPermissions(currentUser, 9L, "app")).thenReturn(List.of(item));
var response = controller.getUserColumnPermissions(request, 9L, "app");
assertThat(response.getData()).extracting(PermissionMenuItemVo::getId).containsExactly(1L);
verify(authSupport).requireUser(request);
verify(authSupport, never()).requireAdmin(request);
}
@Test
void internalProxyAllowsDatabaseBackedNormalUserToReadOwnEffectivePermissions() {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
ReflectionTestUtils.setField(controller, "internalToken", "shared-token");
AdminUserEntity currentUser = new AdminUserEntity();
currentUser.setId(9L);
currentUser.setRole("normal");
when(authSupport.requireUser(request)).thenThrow(new BusinessException(401, "未登录"));
when(request.getHeader("X-Internal-Token")).thenReturn("shared-token");
when(request.getParameter("operatorId")).thenReturn("9");
when(service.requireUserOperator(9L)).thenReturn(currentUser);
when(service.getUserColumnPermissions(currentUser, 9L, "app")).thenReturn(List.of());
controller.getUserColumnPermissions(request, 9L, "app");
verify(service).requireUserOperator(9L);
verify(service, never()).requireAdminOperator(anyLong());
verify(service).getUserColumnPermissions(currentUser, 9L, "app");
}
@Test
void directIdEndpointStillRequiresAdminForSelf() {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
BusinessException authFailure = new BusinessException(403, "需要管理员权限");
when(authSupport.requireAdmin(request)).thenThrow(authFailure);
assertThatThrownBy(() -> controller.getUserColumnIds(request, 9L, "app"))
.isSameAs(authFailure);
verify(authSupport, never()).requireUser(request);
verifyNoInteractions(service);
}
@Test
void directIdUpdateForwardsMenuTypeScope() {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
AdminUserEntity operator = new AdminUserEntity();
operator.setId(1L);
operator.setRole("super_admin");
UserColumnPermissionUpdateRequest body = new UserColumnPermissionUpdateRequest();
body.setColumnIds(List.of(12L));
when(authSupport.requireAdmin(request)).thenReturn(operator);
controller.updateUserColumnIds(request, 9L, "app", body);
verify(service).updateUserColumnPermissions(eq(operator), eq(9L), eq(body), eq("app"));
}
@Test
void internalProxyUsesSharedTokenAndDatabaseBackedOperator() {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
ReflectionTestUtils.setField(controller, "internalToken", "shared-token");
AdminUserEntity operator = new AdminUserEntity();
operator.setId(12L);
operator.setRole("admin");
when(authSupport.requireAdmin(request)).thenThrow(new BusinessException(401, "未登录"));
when(request.getHeader("X-Internal-Token")).thenReturn("shared-token");
when(request.getParameter("operatorId")).thenReturn("12");
when(service.requireAdminOperator(12L)).thenReturn(operator);
when(service.list("app")).thenReturn(List.of());
controller.listMenus(request, "app");
verify(service).requireAdminOperator(12L);
verify(service).list("app");
verify(request, never()).getParameter("superAdmin");
}
@Test
void internalProxyRejectsMismatchedSharedToken() {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
ReflectionTestUtils.setField(controller, "internalToken", "shared-token");
BusinessException authFailure = new BusinessException(401, "未登录");
when(authSupport.requireAdmin(request)).thenThrow(authFailure);
when(request.getHeader("X-Internal-Token")).thenReturn("wrong-token");
assertThatThrownBy(() -> controller.listMenus(request, "app"))
.isSameAs(authFailure);
verify(service, never()).requireAdminOperator(anyLong());
verify(service, never()).list("app");
}
@Test
void internalProxyUsesSharedTokenFileWhenEnvironmentTokenIsUnconfigured(@TempDir Path tempDir) throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
ReflectionTestUtils.setField(controller, "internalToken", "");
Path tokenFile = tempDir.resolve("internal-token");
Files.writeString(tokenFile, "file-token\n");
ReflectionTestUtils.setField(controller, "internalTokenFile", tokenFile.toString());
AdminUserEntity operator = new AdminUserEntity();
operator.setId(12L);
operator.setRole("admin");
when(authSupport.requireAdmin(request)).thenThrow(new BusinessException(401, "未登录"));
when(request.getHeader("X-Internal-Token")).thenReturn("file-token");
when(request.getParameter("operatorId")).thenReturn("12");
when(service.requireAdminOperator(12L)).thenReturn(operator);
when(service.list("admin")).thenReturn(List.of());
controller.listMenus(request, "admin");
verify(service).requireAdminOperator(12L);
verify(service).list("admin");
}
@Test
void internalProxyRejectsMissingSharedTokenFileWhenEnvironmentTokenIsUnconfigured(@TempDir Path tempDir) {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
ReflectionTestUtils.setField(controller, "internalToken", "");
ReflectionTestUtils.setField(controller, "internalTokenFile", tempDir.resolve("missing-token").toString());
BusinessException authFailure = new BusinessException(401, "未登录");
when(authSupport.requireAdmin(request)).thenThrow(authFailure);
when(request.getHeader("X-Internal-Token")).thenReturn("guessed-token");
when(request.getRemoteAddr()).thenReturn("127.0.0.1");
when(request.getParameter("operatorId")).thenReturn("12");
assertThatThrownBy(() -> controller.listMenus(request, "admin"))
.isSameAs(authFailure);
verify(service, never()).requireAdminOperator(anyLong());
verify(service, never()).list("admin");
}
@Test
void configuredInternalTokenTakesPrecedenceOverTokenFile(@TempDir Path tempDir) throws Exception {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
Path tokenFile = tempDir.resolve("internal-token");
Files.writeString(tokenFile, "file-token\n");
ReflectionTestUtils.setField(controller, "internalToken", "configured-token");
ReflectionTestUtils.setField(controller, "internalTokenFile", tokenFile.toString());
BusinessException authFailure = new BusinessException(401, "未登录");
when(authSupport.requireAdmin(request)).thenThrow(authFailure);
when(request.getHeader("X-Internal-Token")).thenReturn("file-token");
when(request.getParameter("operatorId")).thenReturn("12");
assertThatThrownBy(() -> controller.listMenus(request, "admin"))
.isSameAs(authFailure);
verify(service, never()).requireAdminOperator(anyLong());
verify(service, never()).list("admin");
}
@Test
void imageVideoPermissionEndpointsDelegateAuthenticatedOperator() {
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
PermissionMenuService service = mock(PermissionMenuService.class);
HttpServletRequest request = mock(HttpServletRequest.class);
PermissionMenuController controller = new PermissionMenuController(authSupport, service);
AdminUserEntity operator = new AdminUserEntity();
operator.setId(1L);
operator.setRole("super_admin");
ImageVideoDataPermissionUserVo permissionUser = new ImageVideoDataPermissionUserVo();
permissionUser.setId(20L);
ImageVideoDataPermissionUpdateRequest body = new ImageVideoDataPermissionUpdateRequest();
body.setUserIds(List.of(20L));
when(authSupport.requireAdmin(request)).thenReturn(operator);
when(service.listImageVideoDataPermissionUsers(operator)).thenReturn(List.of(permissionUser));
when(service.updateImageVideoDataPermissionUsers(operator, List.of(20L))).thenReturn(1);
var listResponse = controller.listImageVideoDataPermissionUsers(request);
var updateResponse = controller.updateImageVideoDataPermissionUsers(request, body);
assertThat(listResponse.getData()).containsExactly(permissionUser);
assertThat(updateResponse.getData()).isEqualTo(1);
verify(authSupport, times(2)).requireAdmin(request);
verify(service).listImageVideoDataPermissionUsers(operator);
verify(service).updateImageVideoDataPermissionUsers(operator, List.of(20L));
}
}
@@ -1,20 +1,33 @@
package com.nanri.aiimage.modules.permission.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper;
import com.nanri.aiimage.modules.permission.mapper.PermissionMenuMapper;
import com.nanri.aiimage.modules.permission.mapper.UserColumnPermissionMapper;
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuCreateRequest;
import com.nanri.aiimage.modules.permission.model.dto.PermissionMenuUpdateRequest;
import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.permission.model.entity.PermissionMenuEntity;
import com.nanri.aiimage.modules.permission.model.entity.UserColumnPermissionEntity;
import com.nanri.aiimage.modules.permission.model.vo.ImageVideoDataPermissionUserVo;
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
import com.nanri.aiimage.modules.permission.model.vo.UserColumnIdsVo;
import org.junit.jupiter.api.Test;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.mockito.ArgumentCaptor;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -61,10 +74,505 @@ class PermissionMenuServiceTest {
verify(permissionMapper, times(0)).insert(any(UserColumnPermissionEntity.class));
}
@Test
void expandsDirectParentGrantToDescendantsWithoutPersistingChildren() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(9L)).thenReturn(user(9L, "normal", 0));
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(9L, 1L)));
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(1L, null, "app", 10),
menu(2L, 1L, "app", 11),
menu(3L, 2L, "app", 12),
menu(4L, null, "app", 13)));
List<PermissionMenuItemVo> effective = service.getUserColumnPermissions(9L, "app");
assertThat(effective).extracting(PermissionMenuItemVo::getId)
.containsExactly(1L, 2L, 3L);
verify(permissionMapper, never()).insert(any(UserColumnPermissionEntity.class));
}
@Test
void directIdEndpointDoesNotReturnExpandedChildren() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(9L)).thenReturn(user(9L, "normal", 0));
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(9L, 1L)));
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(1L, null, "app", 10),
menu(2L, 1L, "app", 11)));
UserColumnIdsVo direct = service.getUserColumnIds(9L, "app");
assertThat(direct.getColumnIds()).containsExactly(1L);
}
@Test
void leafOnlyEffectivePermissionIncludesRootKeyWithoutGrantingAncestor() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(9L)).thenReturn(user(9L, "normal", 0));
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(9L, 2L)));
PermissionMenuEntity root = menu(1L, null, "app", 1);
root.setColumnKey("brand_front_tools");
PermissionMenuEntity leaf = menu(2L, 1L, "app", 2);
leaf.setColumnKey("brand");
when(menuMapper.selectList(any())).thenReturn(List.of(root, leaf));
List<PermissionMenuItemVo> effective = service.getUserColumnPermissions(9L, "app");
assertThat(effective).extracting(PermissionMenuItemVo::getId).containsExactly(2L);
assertThat(effective.getFirst().getRootColumnKey()).isEqualTo("brand_front_tools");
}
@Test
void menuCatalogIncludesRootKeyFromLoadedHierarchy() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
PermissionMenuEntity root = menu(1L, null, "app", 1);
root.setColumnKey("brand_front_tools");
PermissionMenuEntity leaf = menu(2L, 1L, "app", 2);
leaf.setColumnKey("brand");
when(menuMapper.selectList(any())).thenReturn(List.of(root, leaf));
List<PermissionMenuItemVo> result = service.list("app");
assertThat(result).extracting(PermissionMenuItemVo::getId).containsExactly(1L, 2L);
assertThat(result).extracting(PermissionMenuItemVo::getRootColumnKey)
.containsExactly("brand_front_tools", "brand_front_tools");
}
@Test
void superAdminDirectIdEndpointReturnsOnlyPersistedIds() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(1L)).thenReturn(user(1L, "super_admin", 1));
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(1L, 2L)));
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(1L, null, "app", 1),
menu(2L, 1L, "app", 2)));
UserColumnIdsVo direct = service.getUserColumnIds(1L, "app");
assertThat(direct.getColumnIds()).containsExactly(2L);
}
@Test
void superAdminEffectivePermissionsRemainVirtualFullCatalog() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(1L)).thenReturn(user(1L, "super_admin", 1));
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(1L, null, "app", 1),
menu(2L, 1L, "app", 2)));
List<PermissionMenuItemVo> effective = service.getUserColumnPermissions(1L, "app");
assertThat(effective).extracting(PermissionMenuItemVo::getId).containsExactly(1L, 2L);
verify(permissionMapper, never()).selectList(any());
}
@Test
void normalUserCanReadOwnEffectivePermissions() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity currentUser = user(9L, "normal", 0);
when(userMapper.selectById(9L)).thenReturn(currentUser);
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(9L, 1L)));
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(1L, null, "app", 10),
menu(2L, 1L, "app", 11)));
List<PermissionMenuItemVo> effective = service.getUserColumnPermissions(currentUser, 9L, "app");
assertThat(effective).extracting(PermissionMenuItemVo::getId)
.containsExactly(1L, 2L);
}
@Test
void normalUserCannotReadAnotherUsersEffectivePermissions() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity currentUser = user(9L, "normal", 0);
when(userMapper.selectById(10L)).thenReturn(user(10L, "normal", 0));
assertThatThrownBy(() -> service.getUserColumnPermissions(currentUser, 10L, "app"))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("管理员权限");
verify(permissionMapper, never()).selectList(any());
}
@Test
void rejectsParentFromAnotherMenuType() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
PermissionMenuEntity parent = menu(7L, null, "admin", 1);
when(menuMapper.selectById(7L)).thenReturn(parent);
PermissionMenuCreateRequest request = createRequest(7L, "app");
assertThatThrownBy(() -> service.create(request))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("同一类型");
verify(menuMapper, never()).insert(any(PermissionMenuEntity.class));
}
@Test
void rejectsParentCycleWhenUpdatingMenu() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
PermissionMenuEntity current = menu(1L, null, "app", 1);
PermissionMenuEntity parent = menu(2L, 3L, "app", 2);
PermissionMenuEntity ancestor = menu(3L, 1L, "app", 3);
when(menuMapper.selectById(1L)).thenReturn(current);
when(menuMapper.selectById(2L)).thenReturn(parent);
when(menuMapper.selectById(3L)).thenReturn(ancestor);
PermissionMenuUpdateRequest request = new PermissionMenuUpdateRequest();
request.setName("changed");
request.setColumnKey("changed");
request.setMenuType("app");
request.setRoutePath("changed");
request.setParentId(2L);
assertThatThrownBy(() -> service.update(1L, request))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("自己的子级");
verify(menuMapper, never()).updateById(any(PermissionMenuEntity.class));
}
@Test
void rejectsChangingMenuTypeWhileChildrenRemain() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(menuMapper.selectById(1L)).thenReturn(menu(1L, null, "app", 1));
when(menuMapper.selectCount(any())).thenReturn(1L);
PermissionMenuUpdateRequest request = new PermissionMenuUpdateRequest();
request.setName("changed");
request.setColumnKey("changed");
request.setMenuType("admin");
request.setRoutePath("changed");
assertThatThrownBy(() -> service.update(1L, request))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("子菜单");
verify(menuMapper, never()).updateById(any(PermissionMenuEntity.class));
}
@Test
void refusesDeletingParentMenuUntilChildrenAreRemoved() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(menuMapper.selectById(1L)).thenReturn(menu(1L, null, "app", 1));
when(menuMapper.selectCount(any())).thenReturn(1L);
assertThatThrownBy(() -> service.delete(1L))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("子菜单");
verify(menuMapper, never()).deleteById(any(Long.class));
verify(permissionMapper, never()).delete(any());
}
@Test
void ordinaryAdminCannotAssignMenuOutsideOwnEffectiveSet() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity operator = user(10L, "admin", 1);
AdminUserEntity target = user(20L, "normal", 0);
target.setCreatedById(10L);
when(userMapper.selectById(20L)).thenReturn(target);
when(userMapper.selectById(10L)).thenReturn(operator);
when(userMapper.selectOne(any())).thenReturn(null);
when(menuMapper.selectCount(any())).thenReturn(1L);
when(menuMapper.selectOne(any())).thenReturn(null);
when(menuMapper.selectList(any())).thenReturn(List.of(menu(1L, null, "app", 1)));
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(10L, 1L)));
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
request.setColumnIds(List.of(2L));
assertThatThrownBy(() -> service.updateUserColumnPermissions(operator, 20L, request))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("自己已有");
verify(permissionMapper, never()).delete(any());
}
@Test
void ordinaryAdminCanAssignDescendantGrantedThroughParent() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity operator = user(10L, "admin", 1);
AdminUserEntity target = user(20L, "normal", 0);
target.setCreatedById(10L);
when(userMapper.selectById(20L)).thenReturn(target);
when(userMapper.selectOne(any())).thenReturn(null);
when(menuMapper.selectCount(any())).thenReturn(1L);
when(menuMapper.selectOne(any())).thenReturn(null);
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(1L, null, "app", 1),
menu(2L, 1L, "app", 2)));
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(10L, 1L)));
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
request.setColumnIds(List.of(2L));
service.updateUserColumnPermissions(operator, 20L, request);
ArgumentCaptor<UserColumnPermissionEntity> captor = ArgumentCaptor.forClass(UserColumnPermissionEntity.class);
verify(permissionMapper).insert(captor.capture());
assertThat(captor.getValue().getColumnId()).isEqualTo(2L);
}
@Test
void ordinaryAdminReplacementPreservesDirectGrantsOutsideEffectiveScope() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity operator = user(10L, "admin", 1);
AdminUserEntity target = user(20L, "normal", 0);
target.setCreatedById(10L);
when(userMapper.selectById(20L)).thenReturn(target);
when(menuMapper.selectCount(any())).thenReturn(1L);
when(menuMapper.selectOne(any())).thenReturn(null);
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(1L, null, "app", 1),
menu(2L, null, "app", 2)));
when(permissionMapper.selectList(any()))
.thenReturn(List.of(grant(10L, 1L)), List.of(grant(20L, 2L)));
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
request.setColumnIds(List.of(1L));
service.updateUserColumnPermissions(operator, 20L, request);
ArgumentCaptor<UserColumnPermissionEntity> inserted =
ArgumentCaptor.forClass(UserColumnPermissionEntity.class);
verify(permissionMapper, times(2)).insert(inserted.capture());
assertThat(inserted.getAllValues())
.extracting(UserColumnPermissionEntity::getColumnId)
.containsExactly(1L, 2L);
}
@Test
void replacementStoresOnlyDirectRequestedParentId() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(9L)).thenReturn(user(9L, "normal", 0));
when(menuMapper.selectCount(any())).thenReturn(1L);
when(menuMapper.selectOne(any())).thenReturn(null);
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
request.setColumnIds(List.of(1L));
service.updateUserColumnPermissions(9L, request);
ArgumentCaptor<UserColumnPermissionEntity> captor = ArgumentCaptor.forClass(UserColumnPermissionEntity.class);
verify(permissionMapper).insert(captor.capture());
assertThat(captor.getValue().getColumnId()).isEqualTo(1L);
}
@Test
void appScopedReplacementDeletesOnlyAppDirectGrants() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity operator = user(1L, "super_admin", 1);
when(userMapper.selectById(9L)).thenReturn(user(9L, "normal", 0));
when(menuMapper.selectList(any())).thenReturn(List.of(
menu(11L, null, "app", 1),
menu(12L, 11L, "app", 2)));
when(menuMapper.selectOne(any())).thenReturn(null);
UserColumnPermissionUpdateRequest request = new UserColumnPermissionUpdateRequest();
request.setColumnIds(List.of(12L));
service.updateUserColumnPermissions(operator, 9L, request, PermissionMenuService.MENU_TYPE_APP);
TableInfoHelper.initTableInfo(
new MapperBuilderAssistant(new MybatisConfiguration(), ""),
UserColumnPermissionEntity.class);
ArgumentCaptor<LambdaUpdateWrapper<UserColumnPermissionEntity>> deleted =
ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
verify(permissionMapper).delete(deleted.capture());
assertThat(deleted.getValue().getSqlSegment()).contains("column_id", "IN");
assertThat(deleted.getValue().getParamNameValuePairs().values())
.contains(9L, 11L, 12L);
ArgumentCaptor<UserColumnPermissionEntity> inserted =
ArgumentCaptor.forClass(UserColumnPermissionEntity.class);
verify(permissionMapper).insert(inserted.capture());
assertThat(inserted.getValue().getColumnId()).isEqualTo(12L);
}
@Test
void explicitLowestIdAdminCannotManageImageVideoDataPermissions() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity explicitAdmin = user(1L, "admin", 1);
assertThatThrownBy(() -> service.listImageVideoDataPermissionUsers(explicitAdmin))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("超级管理员");
assertThatThrownBy(() -> service.updateImageVideoDataPermissionUsers(explicitAdmin, List.of(2L)))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("超级管理员");
verify(menuMapper, never()).selectOne(any());
verify(permissionMapper, never()).delete(any());
}
@Test
void explicitNormalRoleCannotActAsInternalAdminDespiteLegacyFlag() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
when(userMapper.selectById(1L)).thenReturn(user(1L, "normal", 1));
assertThatThrownBy(() -> service.requireAdminOperator(1L))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("管理员权限");
}
@Test
void superAdminCanListImageVideoDataPermissionUsers() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity operator = user(99L, "super_admin", 1);
AdminUserEntity explicitAdmin = user(1L, "admin", 1);
explicitAdmin.setUsername("admin");
AdminUserEntity normal = user(2L, "normal", 0);
normal.setUsername("normal");
when(menuMapper.selectOne(any())).thenReturn(imageVideoDataPermission());
when(permissionMapper.selectList(any())).thenReturn(List.of(grant(1L, 75L)));
when(userMapper.selectList(any())).thenReturn(List.of(operator, explicitAdmin, normal));
List<ImageVideoDataPermissionUserVo> result = service.listImageVideoDataPermissionUsers(operator);
assertThat(result).extracting(ImageVideoDataPermissionUserVo::getId).containsExactly(1L, 2L);
assertThat(result.get(0).isGranted()).isTrue();
assertThat(result.get(1).isGranted()).isFalse();
}
@Test
void imageVideoUpdateDeletesOnlyProtectedColumnGrants() {
PermissionMenuMapper menuMapper = mock(PermissionMenuMapper.class);
UserColumnPermissionMapper permissionMapper = mock(UserColumnPermissionMapper.class);
AdminUserMapper userMapper = mock(AdminUserMapper.class);
PermissionMenuService service = new PermissionMenuService(menuMapper, permissionMapper, userMapper);
AdminUserEntity operator = user(99L, "super_admin", 1);
AdminUserEntity explicitAdmin = user(1L, "admin", 1);
AdminUserEntity normal = user(2L, "normal", 0);
when(menuMapper.selectOne(any())).thenReturn(imageVideoDataPermission());
when(userMapper.selectList(any())).thenReturn(List.of(operator, explicitAdmin, normal));
int grantedCount = service.updateImageVideoDataPermissionUsers(operator, List.of(2L));
assertThat(grantedCount).isEqualTo(1);
verify(permissionMapper).deleteByMap(Map.of("column_id", 75L));
ArgumentCaptor<UserColumnPermissionEntity> inserted =
ArgumentCaptor.forClass(UserColumnPermissionEntity.class);
verify(permissionMapper).insert(inserted.capture());
assertThat(inserted.getValue().getUserId()).isEqualTo(2L);
assertThat(inserted.getValue().getColumnId()).isEqualTo(75L);
}
private PermissionMenuCreateRequest createRequest(Long parentId, String menuType) {
PermissionMenuCreateRequest request = new PermissionMenuCreateRequest();
request.setName("child");
request.setColumnKey("child");
request.setMenuType(menuType);
request.setRoutePath("child");
request.setParentId(parentId);
return request;
}
private PermissionMenuEntity menu(Long id, Long parentId, String menuType, int sortOrder) {
PermissionMenuEntity entity = new PermissionMenuEntity();
entity.setId(id);
entity.setParentId(parentId);
entity.setMenuType(menuType);
entity.setColumnKey("menu-" + id);
entity.setRoutePath("route-" + id);
entity.setSortOrder(sortOrder);
return entity;
}
private UserColumnPermissionEntity grant(Long userId, Long columnId) {
UserColumnPermissionEntity entity = new UserColumnPermissionEntity();
entity.setUserId(userId);
entity.setColumnId(columnId);
return entity;
}
private AdminUserEntity user(Long id, String role, int isAdmin) {
AdminUserEntity entity = new AdminUserEntity();
entity.setId(id);
entity.setRole(role);
entity.setIsAdmin(isAdmin);
return entity;
}
private PermissionMenuEntity imageVideoPermission() {
PermissionMenuEntity entity = new PermissionMenuEntity();
entity.setId(75L);
entity.setColumnKey("admin_image_video_tasks");
return entity;
}
private PermissionMenuEntity imageVideoDataPermission() {
PermissionMenuEntity entity = new PermissionMenuEntity();
entity.setId(75L);
entity.setColumnKey("admin_image_video_task_data");
return entity;
}
}