|
|
@@ -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;
|
|
|
+ }
|
|
|
+}
|