Просмотр исходного кода

Merge task/S-cfc-209: 契约级单测 admin.system.monitor TDD-RED 5红 (S-cfc-209)

E2E Test Bot 2 часов назад
Родитель
Сommit
81d170e9e3
1 измененных файлов с 230 добавлено и 0 удалено
  1. 230 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/AdminSystemMonitorContractTest.java

+ 230 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/AdminSystemMonitorContractTest.java

@@ -0,0 +1,230 @@
+package com.etotem.cfc.unit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * 契约级单测:能力键 {@code admin.system.monitor}(S-cfc-209,TDD-RED 阶段)。
+ *
+ * <p>契约溯源:
+ * <ul>
+ *   <li>需求:{@code tests/user-scenarios/应用场景总览.md}
+ *       SC-A-14(系统监控/告警:目标保障可用;流程「看监控 → 告警处理」;边界「误报确认消除」)、
+ *       SC-A-20(限流/熔断配置:目标接口防护;流程「配限流规则 → 生效」;边界「误杀白名单」)。</li>
+ *   <li>用例:{@code tests/test-cases/admin.md} TC-A-21(网关/限流:配限流规则 → 生效)、
+ *       TC-A-21-E(限流规则误杀白名单 IP 不被限流;规则配置非法(负阈值)400)、
+ *       TC-A-N04(限流熔断:触发限流/熔断返回 429/友好降级)。</li>
+ *   <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13(/api/admin/*);
+ *       {@code docs/superpowers/specs/2026-11-07-admin-ops-contract.md}
+ *       §1.14(系统监控/告警:仅 /api/system/health 状态探活,<b>无告警规则/告警记录/监控大盘端点</b>)、
+ *       §1.20(限流/熔断:<b>无 HTTP 配置端点</b>,阈值硬编码于 RateLimitInterceptor,<b>不可通过管理端配置</b>)。</li>
+ * </ul></p>
+ *
+ * <p><b>载体核对(src/main 实测,防假 RED)</b>:
+ * <ul>
+ *   <li>{@code AdminSystemController}(/api/system/health)——已实现状态探活(JVM uptime/内存/CPU),
+ *       但<b>无告警管理端点、无限流/熔断配置端点</b>(仅 health + 两个私有格式化方法)。</li>
+ *   <li>{@code RateLimitInterceptor}——{@code RATE_LIMIT_RULES} 为 {@code static final} Map,
+ *       阈值以字面量硬编码在静态块(10次/秒、1次/60秒、默认50次/秒),<b>无可配置来源、无更新方法</b>。</li>
+ *   <li>{@code AdminBasicQuotaController}(/api/admin/basic-quota/reset)——配额刷新载体(应急),
+ *       {@code BasicQuotaService.refreshQuotas} 仅兜底创建当前周期记录,<b>不重置用量</b>。</li>
+ *   <li>{@code SysMenuController}(/api/admin/menu)——权限治理载体(tree/role-menus/…),已存在。</li>
+ * </ul></p>
+ *
+ * <p><b>契约缺失(本批次 RED 断言)</b>:
+ * <ol>
+ *   <li><b>告警管理端点缺失</b>(SC-A-14):{@code AdminSystemController} 应提供告警查询/处理端点
+ *       (如 alerts / alert-list / monitor-overview),当前仅有 health 探活,无告警载体。</li>
+ *   <li><b>误报确认消除缺失</b>(SC-A-14 边界):{@code AdminSystemController} 应提供告警确认/消除端点
+ *       (如 alert-ack / alert-dismiss),当前不存在。</li>
+ *   <li><b>限流/熔断配置端点缺失</b>(SC-A-20):{@code AdminSystemController} 应提供限流规则/熔断配置端点
+ *       (如 rate-limit/config、breaker/config),当前不存在,运营无法「配限流规则 → 生效」。</li>
+ *   <li><b>限流阈值硬编码</b>(SC-A-20 边界):{@code RateLimitInterceptor} 的限流规则应可配置
+ *       (非 {@code static final} 字面量,或暴露规则更新入口),当前静态硬编码,运营不可调。</li>
+ * </ol></p>
+ *
+ * <p><b>测试风格</b>:纯反射 + JUnit 5(无 Spring 上下文 / 无 DB / 无外部服务),
+ * 与同批已合入主链的 {@code AdminActivityConfigContractTest} / {@code TeamProfitHookContractTest}
+ * / {@code FamilyJoinValidationRulesContractTest} 一致。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code AdminSystemController}(或新建 admin 监控 Controller)增加
+ * 告警查询/处理端点与限流/熔断配置端点(经 {@code SysMenuController} 权限治理挂权限),并将
+ * {@code RateLimitInterceptor} 阈值改为可配置来源(SysConfig / @Value),本测试即转绿。</p>
+ */
+class AdminSystemMonitorContractTest {
+
+    /** 系统监控/告警与限流配置的载体(API_REFERENCE §4.13 /api/system) */
+    private static final String SYSTEM_CONTROLLER = "com.etotem.cfc.controller.admin.AdminSystemController";
+
+    /** 限流拦截器(SC-A-20 载体,阈值硬编码现状) */
+    private static final String RATE_LIMIT_INTERCEPTOR = "com.etotem.cfc.config.RateLimitInterceptor";
+
+    /** 权限治理参考载体(设计溯源 /api/admin/menu) */
+    private static final String MENU_CONTROLLER = "com.etotem.cfc.controller.admin.SysMenuController";
+
+    // ==================== 基线(已实现锚点,非契约缺失) ====================
+
+    @Test
+    @DisplayName("基线-1:健康探活端点已实现(/api/system/health)——锚点,非本批次缺口")
+    void baseline_healthProbeExists() {
+        Class<?> clazz = loadClass(SYSTEM_CONTROLLER);
+        assertNotNull(clazz,
+                "[admin.system.monitor] AdminSystemController 类不存在:设计溯源 §4.13 要求 /api/system 载体。");
+        assertTrue(hasMethod(clazz, "health"),
+                "[admin.system.monitor] AdminSystemController.health() 应存在(SC-A-14 看监控/探活已落地)。");
+    }
+
+    // ==================== 契约1:告警管理端点缺失(SC-A-14) ====================
+
+    @Test
+    @DisplayName("契约-Alert-1:AdminSystemController 应提供告警查询/监控大盘端点(SC-A-14)")
+    void alertManagement_endpointsExist() {
+        Class<?> clazz = loadClass(SYSTEM_CONTROLLER);
+        if (clazz == null) {
+            fail("[admin.system.monitor] AdminSystemController 类不存在,无法验证告警管理端点(SC-A-14)。");
+        }
+        boolean hasAlert = hasMethodContaining(clazz, "alert")
+                || hasMethodContaining(clazz, "monitor")
+                || hasMethodContaining(clazz, "dashboard")
+                || hasMethodContaining(clazz, "overview");
+        assertTrue(hasAlert,
+                "[admin.system.monitor] AdminSystemController 应提供告警查询/监控大盘端点(如 alerts / "
+                        + "monitor-overview):SC-A-14 流程「看监控 → 告警处理」,当前仅 /api/system/health "
+                        + "状态探活,无告警管理载体(admin-ops-contract §1.14 记为「端点载体不完整」)。");
+    }
+
+    @Test
+    @DisplayName("契约-Alert-2:AdminSystemController 应提供告警确认/消除端点(SC-A-14 边界 误报确认消除)")
+    void alertAck_endpointExists() {
+        Class<?> clazz = loadClass(SYSTEM_CONTROLLER);
+        if (clazz == null) {
+            fail("[admin.system.monitor] AdminSystemController 类不存在,无法验证告警确认端点(SC-A-14)。");
+        }
+        boolean hasAck = hasMethodContaining(clazz, "ack")
+                || hasMethodContaining(clazz, "dismiss")
+                || hasMethodContaining(clazz, "confirm")
+                || hasMethodContaining(clazz, "resolve");
+        assertTrue(hasAck,
+                "[admin.system.monitor] AdminSystemController 应提供告警确认/消除端点(如 alert-ack / "
+                        + "alert-dismiss):SC-A-14 边界「误报确认消除」,当前无任何告警处理载体。");
+    }
+
+    // ==================== 契约2:限流/熔断配置端点缺失(SC-A-20) ====================
+
+    @Test
+    @DisplayName("契约-RateLimit-1:AdminSystemController 应提供限流/熔断配置端点(SC-A-20)")
+    void rateLimitConfig_endpointExists() {
+        Class<?> clazz = loadClass(SYSTEM_CONTROLLER);
+        if (clazz == null) {
+            fail("[admin.system.monitor] AdminSystemController 类不存在,无法验证限流配置端点(SC-A-20)。");
+        }
+        boolean hasConfig = hasMethodContaining(clazz, "ratelimit")
+                || hasMethodContaining(clazz, "rateLimit")
+                || hasMethodContaining(clazz, "breaker")
+                || hasMethodContaining(clazz, "circuit")
+                || hasMethodContaining(clazz, "limiter");
+        assertTrue(hasConfig,
+                "[admin.system.monitor] AdminSystemController 应提供限流规则/熔断配置端点(如 rate-limit/config、"
+                        + "breaker/config):SC-A-20 流程「配限流规则 → 生效」,当前无 HTTP 配置端点,"
+                        + "运营无法调整阈值(admin-ops-contract §1.20 记为「无端点载体」)。");
+    }
+
+    @Test
+    @DisplayName("契约-RateLimit-2:限流/熔断配置端点应支持非法阈值守卫(SC-A-20 边界 负阈值 400)")
+    void rateLimitConfig_rejectsNegativeThreshold() {
+        Class<?> clazz = loadClass(SYSTEM_CONTROLLER);
+        if (clazz == null) {
+            fail("[admin.system.monitor] AdminSystemController 类不存在,无法验证限流阈值守卫(SC-A-20)。");
+        }
+        // 契约:配置端点存在且对负阈值(非法规则)返回业务错误(400),避免误杀白名单(TC-A-21-E)。
+        // 当前无配置端点 → 此契约无法成立 → 红。
+        boolean hasConfig = hasMethodContaining(clazz, "ratelimit")
+                || hasMethodContaining(clazz, "rateLimit")
+                || hasMethodContaining(clazz, "breaker")
+                || hasMethodContaining(clazz, "circuit");
+        assertTrue(hasConfig,
+                "[admin.system.monitor] 限流配置端点应存在并对负阈值返回 400(TC-A-21-E 非法阈值):"
+                        + "当前无配置端点,规则合法性守卫无从谈起。");
+    }
+
+    // ==================== 契约3:限流阈值硬编码(SC-A-20 边界) ====================
+
+    @Test
+    @DisplayName("契约-RateLimit-3:RateLimitInterceptor 限流规则应可配置(非静态硬编码)")
+    void rateLimitRules_configurable() {
+        Class<?> clazz = loadClass(RATE_LIMIT_INTERCEPTOR);
+        if (clazz == null) {
+            fail("[admin.system.monitor] RateLimitInterceptor 类不存在,无法验证限流阈值可配置性(SC-A-20)。");
+        }
+        boolean rulesFieldConfigurable = false;
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals("RATE_LIMIT_RULES")) {
+                // 契约:限流规则应来自可配置来源(SysConfig / @Value / 更新入口),
+                // 而非 static final 字面量——运营侧「配限流规则 → 生效」的前提。
+                int mods = f.getModifiers();
+                boolean hardcoded = Modifier.isStatic(mods) && Modifier.isFinal(mods);
+                rulesFieldConfigurable = !hardcoded;
+                break;
+            }
+        }
+        boolean hasUpdateEntry = hasMethodContaining(clazz, "updateRule")
+                || hasMethodContaining(clazz, "setRules")
+                || hasMethodContaining(clazz, "refreshRules")
+                || hasMethodContaining(clazz, "reload");
+        assertTrue(rulesFieldConfigurable || hasUpdateEntry,
+                "[admin.system.monitor] RateLimitInterceptor 限流规则应可配置(非 static final 硬编码,"
+                        + "或暴露规则更新入口):SC-A-20 边界「误杀白名单」要求运营可调阈值,"
+                        + "当前 RATE_LIMIT_RULES 以字面量硬编码于静态块(10次/秒、1次/60秒、默认50次/秒),"
+                        + "无可配置来源(admin-ops-contract §1.20)。");
+    }
+
+    // ==================== 权限流(设计溯源参考) ====================
+
+    @Test
+    @DisplayName("权限流:告警/限流配置端点应挂 SysMenuController 权限治理(/api/admin/menu)")
+    void permissionGovernance_menuControllerExists() {
+        Class<?> menu = loadClass(MENU_CONTROLLER);
+        assertNotNull(menu,
+                "[admin.system.monitor] SysMenuController 不存在:设计溯源要求 /api/admin/menu 权限治理载体。");
+        boolean hasTree = hasMethod(menu, "tree");
+        assertTrue(hasTree,
+                "[admin.system.monitor] SysMenuController 应提供 tree 端点承载权限治理(/api/admin/menu),"
+                        + "告警/限流配置端点挂对应权限位后运营方可访问。");
+    }
+
+    // ==================== 辅助 ====================
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    private boolean hasMethod(Class<?> clazz, String name) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasMethodContaining(Class<?> clazz, String fragment) {
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().toLowerCase().contains(fragment.toLowerCase())) {
+                return true;
+            }
+        }
+        return false;
+    }
+}