测试路由:增加动态菜单与前端钻取契约检查
This commit is contained in:
1 parent
ed08e1deb3
commit
ec3a7afe46
1 file changed
+188
+188
@@ -0,0 +1,188 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 业务内部动态路由契约、前端钻取与菜单完整性回归测试
|
||||
*/
|
||||
public class TrainRouteAndInteractionContractTest
|
||||
{
|
||||
private static final String PROJECT_ROOT = findProjectRoot();
|
||||
|
||||
private static String findProjectRoot()
|
||||
{
|
||||
File cur = new File(".").getAbsoluteFile();
|
||||
while (cur != null)
|
||||
{
|
||||
if (new File(cur, "build_ui").isDirectory() && new File(cur, "RuoYi-Vue").isDirectory())
|
||||
{
|
||||
return cur.getAbsolutePath();
|
||||
}
|
||||
cur = cur.getParentFile();
|
||||
}
|
||||
return new File(".").getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 验证管理驾驶舱业务待办提醒的目标路由契约:
|
||||
* 严禁出现 /train/report, /train/extra, /train/accommodation 等非动态路由硬编码;
|
||||
* 必须准确对齐 /analysis/attendance, /analysis/report, /equip/accommodation, /equip/extra.
|
||||
*/
|
||||
@Test
|
||||
public void testDashboardReminderRouteTargetContract() throws Exception
|
||||
{
|
||||
File dashboardVue = new File(PROJECT_ROOT, "build_ui/src/views/train/analysis/dashboard/index.vue");
|
||||
assertTrue("dashboard/index.vue 必须存在", dashboardVue.exists());
|
||||
|
||||
String content = new String(Files.readAllBytes(dashboardVue.toPath()), "UTF-8");
|
||||
|
||||
// 强校验:禁止出现旧错误硬编码
|
||||
assertFalse("严禁在驾驶舱待办中硬编码 /train/report", content.contains("'/train/report'"));
|
||||
assertFalse("严禁在驾驶舱待办中硬编码 /train/extra", content.contains("'/train/extra'"));
|
||||
assertFalse("严禁在驾驶舱待办中硬编码 /train/accommodation", content.contains("'/train/accommodation'"));
|
||||
|
||||
// 强校验:必须对齐真实动态路由
|
||||
assertTrue("必须包含考勤分析路由 /analysis/attendance", content.contains("'/analysis/attendance'"));
|
||||
assertTrue("待报到提醒必须指向真实路由 /analysis/report", content.contains("'/analysis/report'"));
|
||||
assertTrue("住宿预分配提醒必须指向真实业务路由 /equip/accommodation", content.contains("'/equip/accommodation'"));
|
||||
assertTrue("门禁授权异常提醒必须指向真实路由 /equip/extra", content.contains("'/equip/extra'"));
|
||||
|
||||
// 验证提醒配置化与角色感知
|
||||
assertTrue("必须包含 reminders 响应式配置数组", content.contains("reminders()"));
|
||||
assertTrue("必须包含 handleReminderClick 权限感知处理器", content.contains("handleReminderClick("));
|
||||
assertTrue("必须包含 non-clickable 权限样式支持", content.contains("non-clickable"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 验证所有有效业务菜单对应的前端 Vue 组件必须在 build_ui 中物理存在
|
||||
*/
|
||||
@Test
|
||||
public void testActiveTrainingMenuComponentsExistInBuildUi()
|
||||
{
|
||||
String[] coreViews = {
|
||||
"views/train/analysis/dashboard/index.vue",
|
||||
"views/train/analysis/attendance/index.vue",
|
||||
"views/train/portrait/index.vue",
|
||||
"views/train/report/index.vue",
|
||||
"views/train/accommodation/index.vue",
|
||||
"views/train/dormitory/index.vue",
|
||||
"views/train/extra/index.vue",
|
||||
"views/train/equipment/index.vue",
|
||||
"views/train/board/index.vue",
|
||||
"views/train/site/index.vue",
|
||||
"views/train/classroom/index.vue"
|
||||
};
|
||||
|
||||
File buildUiSrc = new File(PROJECT_ROOT, "build_ui/src");
|
||||
for (String viewPath : coreViews)
|
||||
{
|
||||
File f = new File(buildUiSrc, viewPath);
|
||||
assertTrue("核心组件物理文件必须存在: " + viewPath, f.exists());
|
||||
}
|
||||
|
||||
// 验证已停用的手动任务组件物理上确实不存在,防止误被作为有效组件解析
|
||||
File missingComponent = new File(buildUiSrc, "views/monitor/job/manual_index.vue");
|
||||
assertFalse("已停用菜单组件 manual_index.vue 物理上不应存在", missingComponent.exists());
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 验证 013 迁移脚本的内容契约:
|
||||
* 包含 2165(教学成果)停用、2059(手动任务)停用、2261/2262/2263(住宿管理)创建与后置断言。
|
||||
*/
|
||||
@Test
|
||||
public void testMigration013ScriptContract() throws Exception
|
||||
{
|
||||
File migration013 = new File(PROJECT_ROOT, "RuoYi-Vue/sql/system/kingbase/013_interaction_route_cleanup.sql");
|
||||
assertTrue("013 迁移脚本必须存在", migration013.exists());
|
||||
|
||||
String sql = new String(Files.readAllBytes(migration013.toPath()), "UTF-8");
|
||||
|
||||
assertTrue("013 必须显式停用教学成果空父菜单 (2165)", sql.contains("menu_id = 2165"));
|
||||
assertTrue("013 必须显式停用手动任务无效菜单 (2059)", sql.contains("menu_id = 2059"));
|
||||
assertTrue("013 必须注册住宿管理菜单 (2261)", sql.contains("2261"));
|
||||
assertTrue("013 必须注册住宿查询权限 (2262)", sql.contains("2262"));
|
||||
assertTrue("013 必须注册住宿办理权限 (2263)", sql.contains("2263"));
|
||||
assertTrue("013 必须包含事务边界 BEGIN 与 COMMIT", sql.contains("BEGIN;") && sql.contains("COMMIT;"));
|
||||
assertTrue("013 必须包含后置断言块 DO $$", sql.contains("DO $$"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. 验证整个 build_ui 中不存在非法的 /train/report, /train/extra, /train/accommodation 钻取路由
|
||||
*/
|
||||
@Test
|
||||
public void testNoInvalidInternalRouteRemainsInBuildUi() throws Exception
|
||||
{
|
||||
File buildUiSrc = new File(PROJECT_ROOT, "build_ui/src");
|
||||
assertTrue(buildUiSrc.exists());
|
||||
|
||||
File[] vueFiles = findFilesRecursively(buildUiSrc, ".vue");
|
||||
Pattern badRoutePattern = Pattern.compile("['\"]/train/(report|extra)['\"]");
|
||||
|
||||
for (File f : vueFiles)
|
||||
{
|
||||
String content = new String(Files.readAllBytes(f.toPath()), "UTF-8");
|
||||
Matcher m = badRoutePattern.matcher(content);
|
||||
assertFalse("文件 " + f.getName() + " 中不得残留 /train/report 或 /train/extra: ", m.find());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. 验证培训管理员 (train_admin) 角色权限安全防线:
|
||||
* 严禁被授予任何 remove 破坏性权限(包含课程删除、教师删除、学员删除、住宿删除)。
|
||||
*/
|
||||
@Test
|
||||
public void testTrainAdminDestructivePermissionSafetyBoundary() throws Exception
|
||||
{
|
||||
File migration013 = new File(PROJECT_ROOT, "RuoYi-Vue/sql/system/kingbase/013_interaction_route_cleanup.sql");
|
||||
String sql = new String(Files.readAllBytes(migration013.toPath()), "UTF-8");
|
||||
|
||||
// 将 013 脚本按后置断言 DO $$ 拆分为迁移执行块与安全守护断言块
|
||||
String[] parts = sql.split("DO \\$\\$");
|
||||
assertTrue("013 必须包含独立的后置断言守护块", parts.length >= 2);
|
||||
String migrationBody = parts[0];
|
||||
String assertionBody = parts[1];
|
||||
|
||||
// 强校验:在迁移执行块中,绝无任何创建或授权删除权限的语句
|
||||
assertFalse("013 迁移块严禁定义住宿删除权限", migrationBody.contains("train:accommodation:remove"));
|
||||
assertFalse("013 迁移块严禁定义学员删除权限", migrationBody.contains("train:student:remove"));
|
||||
assertFalse("013 迁移块严禁定义教师删除权限", migrationBody.contains("train:teacher:remove"));
|
||||
assertFalse("013 迁移块严禁定义课程删除权限", migrationBody.contains("train:course:remove"));
|
||||
|
||||
// 强校验:在安全守护断言块中,必须严格校验培训管理员绝无此类破坏性权限
|
||||
assertTrue("后置断言必须核查培训管理员无课程删除权限", assertionBody.contains("train:course:remove"));
|
||||
assertTrue("后置断言必须核查培训管理员无教师删除权限", assertionBody.contains("train:teacher:remove"));
|
||||
assertTrue("后置断言必须核查培训管理员无学员删除权限", assertionBody.contains("train:student:remove"));
|
||||
assertTrue("后置断言必须核查培训管理员无住宿删除权限", assertionBody.contains("train:accommodation:remove"));
|
||||
assertTrue("后置断言必须抛出安全违背异常", assertionBody.contains("Security violation: train_admin role must not have remove permissions"));
|
||||
}
|
||||
|
||||
private static File[] findFilesRecursively(File dir, String suffix)
|
||||
{
|
||||
java.util.List<File> list = new java.util.ArrayList<>();
|
||||
scanDir(dir, suffix, list);
|
||||
return list.toArray(new File[0]);
|
||||
}
|
||||
|
||||
private static void scanDir(File dir, String suffix, java.util.List<File> list)
|
||||
{
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return;
|
||||
for (File f : files)
|
||||
{
|
||||
if (f.isDirectory())
|
||||
{
|
||||
scanDir(f, suffix, list);
|
||||
}
|
||||
else if (f.getName().endsWith(suffix))
|
||||
{
|
||||
list.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user