实现系统管理菜单树增强接口
This commit is contained in:
@@ -20,9 +20,15 @@ import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
|
||||
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
|
||||
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
|
||||
import cn.nianxx.thhotel.platform.navigation.common.enums.PlatformMenuStatus;
|
||||
import cn.nianxx.thhotel.platform.navigation.common.enums.PlatformMenuType;
|
||||
import cn.nianxx.thhotel.platform.navigation.domain.PlatformMenuEntity;
|
||||
import cn.nianxx.thhotel.platform.navigation.repository.PlatformNavigationRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -59,6 +65,8 @@ class AdminReadonlyControllerTest {
|
||||
@Autowired
|
||||
private PlatformHotelRepository hotelRepository;
|
||||
@Autowired
|
||||
private PlatformNavigationRepository navigationRepository;
|
||||
@Autowired
|
||||
private AuthPasswordService passwordService;
|
||||
|
||||
@BeforeEach
|
||||
@@ -440,6 +448,218 @@ class AdminReadonlyControllerTest {
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueryCompleteMenuTreeWithStableSortingAndWarnings() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
String suffix = String.valueOf(System.nanoTime());
|
||||
PlatformMenuEntity root = insertMenu("M006_TREE_ROOT_" + suffix, null, "树根", 500,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
PlatformMenuEntity beta = insertMenu("M006_TREE_BETA_" + suffix, root.getId(), "Beta 子菜单", 100,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
PlatformMenuEntity alpha = insertMenu("M006_TREE_ALPHA_" + suffix, root.getId(), "Alpha 子菜单", 100,
|
||||
false, PlatformMenuStatus.DISABLED.name());
|
||||
PlatformMenuEntity orphan = insertMenu("M006_TREE_ORPHAN_" + suffix, Long.MAX_VALUE - 10, "孤儿菜单", 700,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
|
||||
MvcResult result = mockMvc.perform(get("/api/admin/menus/tree")
|
||||
.header("Authorization", "Bearer " + token))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode json = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
JsonNode rootNode = requireMenuNode(json.path("items"), root.getMenuCode());
|
||||
Assertions.assertEquals(alpha.getMenuCode(), rootNode.path("children").get(0).path("menu_code").asText());
|
||||
Assertions.assertEquals(beta.getMenuCode(), rootNode.path("children").get(1).path("menu_code").asText());
|
||||
JsonNode alphaNode = requireMenuNode(json.path("items"), alpha.getMenuCode());
|
||||
Assertions.assertFalse(alphaNode.path("visible").asBoolean());
|
||||
Assertions.assertEquals(PlatformMenuStatus.DISABLED.name(), alphaNode.path("menu_status").asText());
|
||||
JsonNode orphanNode = requireMenuNode(json.path("items"), orphan.getMenuCode());
|
||||
Assertions.assertEquals(orphan.getId().toString(), orphanNode.path("id").asText());
|
||||
Assertions.assertTrue(json.path("warnings").size() >= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMenuTreeQueryWhenTokenMissingOrPermissionMissing() throws Exception {
|
||||
mockMvc.perform(get("/api/admin/menus/tree"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_AUTH_REQUIRED"));
|
||||
|
||||
String viewerToken = tokenFrom(login("m006-viewer", "Viewer@123456"));
|
||||
mockMvc.perform(get("/api/admin/menus/tree")
|
||||
.header("Authorization", "Bearer " + viewerToken))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_PERMISSION_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateMenuTreeOrderAndWriteAdminAudit() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
String suffix = String.valueOf(System.nanoTime());
|
||||
PlatformMenuEntity oldRoot = insertMenu("M006_ORDER_OLD_" + suffix, null, "旧父级", 610,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
PlatformMenuEntity newRoot = insertMenu("M006_ORDER_NEW_" + suffix, null, "新父级", 620,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
PlatformMenuEntity child = insertMenu("M006_ORDER_CHILD_" + suffix, oldRoot.getId(), "待移动菜单", 630,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
|
||||
MvcResult updateResult = mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"menu_id": "%s",
|
||||
"parent_id": "%s",
|
||||
"sort_order": 123
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(child.getId(), newRoot.getId())))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode json = objectMapper.readTree(updateResult.getResponse().getContentAsString());
|
||||
JsonNode newRootNode = requireMenuNode(json.path("items"), newRoot.getMenuCode());
|
||||
JsonNode movedChildNode = requireMenuNode(newRootNode.path("children"), child.getMenuCode());
|
||||
Assertions.assertEquals("123", movedChildNode.path("sort_order").asText());
|
||||
Assertions.assertEquals(newRoot.getId().toString(), movedChildNode.path("parent_id").asText());
|
||||
|
||||
mockMvc.perform(get("/api/admin/audits")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.param("target_type", "PLATFORM_MENU")
|
||||
.param("target_id", child.getId().toString())
|
||||
.param("action", "UPDATE_MENU_TREE_ORDER"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].before_snapshot_json").value(org.hamcrest.Matchers.containsString("parent_id")))
|
||||
.andExpect(jsonPath("$.items[0].before_snapshot_json").value(org.hamcrest.Matchers.containsString(oldRoot.getId().toString())))
|
||||
.andExpect(jsonPath("$.items[0].after_snapshot_json").value(org.hamcrest.Matchers.containsString("parent_id")))
|
||||
.andExpect(jsonPath("$.items[0].after_snapshot_json").value(org.hamcrest.Matchers.containsString(newRoot.getId().toString())))
|
||||
.andExpect(jsonPath("$.items[0].after_snapshot_json").value(org.hamcrest.Matchers.containsString("sort_order")))
|
||||
.andExpect(jsonPath("$.items[0].after_snapshot_json").value(org.hamcrest.Matchers.containsString("123")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGenerateStableSortOrderWhenTreeOrderSortOrderMissing() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
String suffix = String.valueOf(System.nanoTime());
|
||||
PlatformMenuEntity root = insertMenu("M006_SORT_ROOT_" + suffix, null, "排序父级", 640,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
PlatformMenuEntity first = insertMenu("M006_SORT_FIRST_" + suffix, null, "排序一", 10,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
PlatformMenuEntity second = insertMenu("M006_SORT_SECOND_" + suffix, null, "排序二", 20,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
|
||||
MvcResult updateResult = mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": [
|
||||
{ "menu_id": "%s", "parent_id": "%s" },
|
||||
{ "menu_id": "%s", "parent_id": "%s" }
|
||||
]
|
||||
}
|
||||
""".formatted(first.getId(), root.getId(), second.getId(), root.getId())))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode json = objectMapper.readTree(updateResult.getResponse().getContentAsString());
|
||||
JsonNode rootNode = requireMenuNode(json.path("items"), root.getMenuCode());
|
||||
Assertions.assertEquals(first.getMenuCode(), rootNode.path("children").get(0).path("menu_code").asText());
|
||||
Assertions.assertEquals(100, rootNode.path("children").get(0).path("sort_order").asInt());
|
||||
Assertions.assertEquals(second.getMenuCode(), rootNode.path("children").get(1).path("menu_code").asText());
|
||||
Assertions.assertEquals(200, rootNode.path("children").get(1).path("sort_order").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectInvalidMenuTreeOrderRequests() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
String suffix = String.valueOf(System.nanoTime());
|
||||
PlatformMenuEntity parent = insertMenu("M006_INVALID_PARENT_" + suffix, null, "父级", 650,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
PlatformMenuEntity child = insertMenu("M006_INVALID_CHILD_" + suffix, parent.getId(), "子级", 660,
|
||||
true, PlatformMenuStatus.ACTIVE.name());
|
||||
|
||||
mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": [
|
||||
{ "menu_id": "%s", "parent_id": "%s", "sort_order": 100 }
|
||||
]
|
||||
}
|
||||
""".formatted(parent.getId(), parent.getId())))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": [
|
||||
{ "menu_id": "%s", "parent_id": "9223372036854770000", "sort_order": 100 }
|
||||
]
|
||||
}
|
||||
""".formatted(parent.getId())))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": [
|
||||
{ "menu_id": "%s", "parent_id": "%s", "sort_order": 100 }
|
||||
]
|
||||
}
|
||||
""".formatted(parent.getId(), child.getId())))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_INVALID_REQUEST"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": [
|
||||
{ "menu_id": "9223372036854770000", "parent_id": null, "sort_order": 100 }
|
||||
]
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_TARGET_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectTreeOrderWhenTokenMissingOrPermissionMissing() throws Exception {
|
||||
mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": []
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_AUTH_REQUIRED"));
|
||||
|
||||
String viewerToken = tokenFrom(login("m006-viewer", "Viewer@123456"));
|
||||
|
||||
mockMvc.perform(put("/api/admin/menus/tree-order")
|
||||
.header("Authorization", "Bearer " + viewerToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"items": []
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("ADMIN_PERMISSION_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateDisabledHotelAndRejectBreakingSingleActiveHotelRule() throws Exception {
|
||||
String token = tokenFrom(login("m006-admin", "Admin@123456"));
|
||||
@@ -514,4 +734,52 @@ class AdminReadonlyControllerTest {
|
||||
accessRepository.insertPermission(permission);
|
||||
return permission;
|
||||
}
|
||||
|
||||
private PlatformMenuEntity insertMenu(
|
||||
String menuCode,
|
||||
Long parentId,
|
||||
String menuName,
|
||||
int sortOrder,
|
||||
boolean visible,
|
||||
String menuStatus) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
PlatformMenuEntity menu = new PlatformMenuEntity();
|
||||
menu.setParentId(parentId);
|
||||
menu.setMenuCode(menuCode);
|
||||
menu.setMenuName(menuName);
|
||||
menu.setMenuType(PlatformMenuType.PAGE.name());
|
||||
menu.setRoutePath("/system/menus");
|
||||
menu.setComponentKey("SystemMenus");
|
||||
menu.setIconKey("pi pi-sitemap");
|
||||
menu.setPermissionCode("SYSTEM_MENU_MANAGE");
|
||||
menu.setSortOrder(sortOrder);
|
||||
menu.setVisible(visible);
|
||||
menu.setMenuStatus(menuStatus);
|
||||
menu.setCreatedAt(now);
|
||||
menu.setUpdatedAt(now);
|
||||
navigationRepository.insertMenu(menu);
|
||||
return menu;
|
||||
}
|
||||
|
||||
private JsonNode requireMenuNode(JsonNode nodes, String menuCode) {
|
||||
JsonNode found = findMenuNode(nodes, menuCode);
|
||||
Assertions.assertTrue(Objects.nonNull(found), "未找到菜单节点:" + menuCode);
|
||||
return found;
|
||||
}
|
||||
|
||||
private JsonNode findMenuNode(JsonNode nodes, String menuCode) {
|
||||
if (nodes == null || !nodes.isArray()) {
|
||||
return null;
|
||||
}
|
||||
for (JsonNode node : nodes) {
|
||||
if (menuCode.equals(node.path("menu_code").asText())) {
|
||||
return node;
|
||||
}
|
||||
JsonNode child = findMenuNode(node.path("children"), menuCode);
|
||||
if (child != null) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user