Explorar el Código

test(cockpit): 覆盖 Redis 故障错误分类

Kazerin hace 3 semanas
padre
commit
73a8ae37b5

+ 1 - 1
docs/cockpit/README.md

@@ -200,7 +200,7 @@ mvn -pl zksy-admin -am test -Dtest='Cockpit*Test'
 - 本轮真实登录态补充验收:16 个端点非法参数样例全部返回 400 `INVALID_REQUEST`;以真实 `deviceCode` `860347065467627` 依次请求 archive、metrics、events、work-orders、detail-aggregate、freshness,均返回 200、`readOnly=true` 并回显同一 canonical `deviceCode`。
 - 第 4 轮 `detail-aggregate` 的 MockMvc 故障注入证明:单条时间线失败保留其余成功时间线和档案/指标;返回其他工单的顶层或内层日志编码会被拒绝,聚合仍以 200 `PARTIAL` 返回未受影响分区。这是契约测试证据,未在真实服务上制造业务故障。
 - 第 4 轮真实 HTTP 复核(重启加载 `f33b9bf`):首屏无 token 返回 200 并签发 `snapshotAt=+08:00`、`total=15`;使用该 token 请求第 2 页返回 200、同 `snapshotAt`、同 `total`;第 2 页缺 token 和使用旧未签发 token 均返回 400(契约类别由 MockMvc 固化为 `SNAPSHOT_EXPIRED`)。
-- 第 6 轮重启加载合并后代码的真实切片核对:overview/infrastructure、overview/monitoring、overview/warning-settings、infrastructure/base-archives、operating-information、emergency-support、monitoring/overview、monitoring/situation 均返回 200/readOnly/CURRENT;`monitoring/situation` 返回 6 个异常设备;`events/history-analysis` 返回 169 行,`events/handling-efficiency` 返回 2 条事件,`events/multidimensional-analysis` 返回 7 个维度。`monitoring/distribution` 返回 200/readOnly/`freshness=EMPTY` 且点位和未关联设备均为 0,原因仍是无可信坐标校核,不是 fixture 空壳。
+- 第 7 轮补充 Redis GET/SET 运行时异常测试:适配器将缓存连接故障包装为 `CockpitReadException`,控制器契约映射为 `500 SERVER_ERROR`,不误报为 `SNAPSHOT_EXPIRED`;定向与全量回归均通过。
 - **验收边界**:上述 200/readOnly 证明当前本机实例的认证与响应契约,不等于 #63 全部切片的代表性真实数据、空/脏数据及故障场景均已验收。静态装配盘点显示 23 个 `Empty*Adapter` 均有对应的 `@Primary Business*Adapter`(另有 1 个共享事件事实适配器);`Empty*Adapter` 作为 fallback 保留,不能仅凭类仍存在判断运行时未接线。目标部署实例仍需验收,issue 保持开启。
 - WebSocket `/ws/deviceStatus` 真实浏览器验收通过:发送 `cockpitSubscribe(scope=all)` 后收到 `cockpitSubscribed`,响应带 `serverUpdatedAt`;所有抽查时间戳为 `+08:00`。
 - 目标部署地址 `192.168.110.30:8301` TCP 不可达,因此没有把它当作本轮 HTTP 证据;生产部署实例、性能压测、银河麒麟与 Docker 实机验证仍未执行。

+ 32 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitMapSnapshotAdapterTest.java

@@ -7,6 +7,7 @@ import com.zksy.base.service.EquipmentBaseService;
 import com.zksy.base.service.EquipmentTypeService;
 import com.zksy.base.service.MonitorPointService;
 import com.zksy.cockpit.foundation.CockpitCoordinateValidator;
+import com.zksy.cockpit.foundation.CockpitReadException;
 import com.zksy.cockpit.foundation.CockpitSnapshotExpiredException;
 import com.zksy.cockpit.map.BusinessCockpitMapSnapshotAdapter;
 import com.zksy.cockpit.map.CockpitMapSnapshot;
@@ -416,6 +417,37 @@ class BusinessCockpitMapSnapshotAdapterTest {
         verifyNoInteractions(monitorPointService, equipmentBaseService, equipmentTypeService);
     }
 
+    @Test
+    void redisGetFailureIsClassifiedAsReadFailure() {
+        RedisTemplate<Object, Object> sharedRedis = mock(RedisTemplate.class);
+        ValueOperations<Object, Object> values = mock(ValueOperations.class);
+        when(sharedRedis.opsForValue()).thenReturn(values);
+        when(values.get(any())).thenThrow(new IllegalStateException("redis unavailable"));
+        BusinessCockpitMapSnapshotAdapter sharedAdapter = new BusinessCockpitMapSnapshotAdapter(
+                monitorPointService, equipmentBaseService, equipmentTypeService, deviceFactResolver,
+                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC), sharedRedis);
+
+        assertThrows(CockpitReadException.class, () -> sharedAdapter.load(null));
+    }
+
+    @Test
+    void redisSetFailureIsClassifiedAsReadFailure() {
+        RedisTemplate<Object, Object> sharedRedis = mock(RedisTemplate.class);
+        ValueOperations<Object, Object> values = mock(ValueOperations.class);
+        when(sharedRedis.opsForValue()).thenReturn(values);
+        when(values.get(any())).thenReturn(null);
+        when(values.setIfAbsent(any(), any(), anyLong(), eq(java.util.concurrent.TimeUnit.SECONDS)))
+                .thenThrow(new IllegalStateException("redis unavailable"));
+        when(monitorPointService.list()).thenReturn(Collections.emptyList());
+        when(equipmentBaseService.list()).thenReturn(Collections.emptyList());
+        when(equipmentTypeService.list()).thenReturn(Collections.emptyList());
+        BusinessCockpitMapSnapshotAdapter sharedAdapter = new BusinessCockpitMapSnapshotAdapter(
+                monitorPointService, equipmentBaseService, equipmentTypeService, deviceFactResolver,
+                Clock.fixed(Instant.parse("2026-09-02T02:00:00Z"), ZoneOffset.UTC), sharedRedis);
+
+        assertThrows(CockpitReadException.class, () -> sharedAdapter.load(null));
+    }
+
     @Test
     void expiredSnapshotAtIsRejectedInsteadOfLoadingCurrentRelations() {
         when(monitorPointService.list()).thenReturn(Collections.emptyList());