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

test(S-cfc-8): dimension.registry.mapLegacyCode 契约级单测(RED)

能力键:dimension.registry.mapLegacyCode(S-cfc-8,RED 阶段)
溯源:PRD §4 P0-3 编码映射层 + 设计 §4/§5.1 GET /api/dimension-registry/map?legacy= -> Result<String>

新增 DimensionRegistryMapContractTest(MockMvc 全栈契约测试,不假定实现类包路径):
- map_healthSevenLegacyCodes_resolveToBodySubCode:健康 7 维 legacy -> body.* 统一 code
- map_energyRootCodes_passthroughAsIs:能量 5 根同码原样返回
- map_unifiedSubCode_passthroughAsIs:统一子 code 幂等原样返回
- map_unknownLegacy_returnsNullData:未知 legacy -> data=null(200)
- map_emptyLegacy_returnsNullData:空串 legacy -> data=null(200)
- map_missingLegacy_returnsBadRequest:缺参 -> 400
- map_withoutAuthToken_isReachable:无角色限定(parent/child/teacher/admin 共用),匿名可达

运行结果(RED,cfclub 未合入实现):Tests run: 7, Failures: 7(均 404,接口未实现)
暴露缺陷:无(纯 RED,未发现实现缺陷;实现仅在 feature/unified-dimension-p0)
E2E Test Bot 1 неделя назад
Родитель
Сommit
73654bcf8f

+ 114 - 0
cfc-backend/src/test/java/com/etotem/cfc/integration/controller/DimensionRegistryMapContractTest.java

@@ -0,0 +1,114 @@
+package com.etotem.cfc.integration.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.hamcrest.Matchers.nullValue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * 契约级单测:dimension.registry.mapLegacyCode
+ *
+ * <p>溯源·需求:统一维度体系 PRD §4 P0-3「编码映射层」(4 套历史编码 → 统一 code,双向可回查)。
+ * 溯源·设计:统一维度体系设计文档 §4 映射层设计 + §5.1 GET /api/dimension-registry/map?legacy= → Result&lt;String&gt;。
+ *
+ * <p>契约要点(以设计文档为准):
+ * <ul>
+ *   <li>GET /api/dimension-registry/map?legacy=growth → Result.success("body.growth")(旧健康 7 维 → body.*)</li>
+ *   <li>入参本身已是统一 code(如 body)→ 原样返回</li>
+ *   <li>无法解析的 legacy → Result.success(null)(data 为 null,code=200,不抛异常)</li>
+ *   <li>空/缺失 legacy 参数 → 服务层返回 null(空串入参)或 Spring 400(缺失入参)</li>
+ *   <li>权限:该接口为注册中心只读取数入口,设计上供小程序(parent/child)与管理端(admin/teacher)共用,
+ *       无角色限定(角色/权限控制在此键「不适用」),故无 token 匿名请求也应能到达控制器(200)。</li>
+ * </ul>
+ *
+ * <p>本测试不假定实现类包路径(只经 HTTP 契约驱动),RED 阶段(cfclub 未合入实现)预期整体失败/404,
+ * 合入 feature/unified-dimension-p0 后应转绿。测试经 MockMvc 全栈调用,不依赖真实外部服务(无微信/第三方)。
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+class DimensionRegistryMapContractTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    private static final String MAP_PATH = "/api/dimension-registry/map";
+
+    /** 正常路径:健康 7 维 legacy code → body.* 统一 code(PRD P0-3 / 设计 §4 示例映射)。 */
+    @Test
+    void map_healthSevenLegacyCodes_resolveToBodySubCode() throws Exception {
+        assertMap("growth", "body.growth");
+        assertMap("sleep", "body.sleep");
+        assertMap("vision", "body.vision");
+        assertMap("immunity", "body.immunity");
+        assertMap("nutrition", "body.nutrition");
+        assertMap("gut", "body.gut");
+        assertMap("exercise", "body.exercise");
+    }
+
+    /** 正常路径:能量五维 root code(body/mind/wisdom/action/wealth)与统一 code 同码,原样返回。 */
+    @Test
+    void map_energyRootCodes_passthroughAsIs() throws Exception {
+        assertMap("body", "body");
+        assertMap("mind", "mind");
+        assertMap("wisdom", "wisdom");
+        assertMap("action", "action");
+        assertMap("wealth", "wealth");
+    }
+
+    /** 关键边界:已入参为统一子 code(body.growth),应原样返回(幂等)。 */
+    @Test
+    void map_unifiedSubCode_passthroughAsIs() throws Exception {
+        assertMap("body.growth", "body.growth");
+    }
+
+    /** 关键边界:未知 legacy code → Result.success(null),data=null 且 code=200(不抛异常)。 */
+    @Test
+    void map_unknownLegacy_returnsNullData() throws Exception {
+        mockMvc.perform(get(MAP_PATH).param("legacy", "unknown_legacy_xyz"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").value(nullValue()));
+    }
+
+    /** 关键边界:空串 legacy → 服务层视为不可解析,返回 data=null(而非 500)。 */
+    @Test
+    void map_emptyLegacy_returnsNullData() throws Exception {
+        mockMvc.perform(get(MAP_PATH).param("legacy", ""))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").value(nullValue()));
+    }
+
+    /** 关键边界:缺失 legacy 参数 → Spring 参数绑定失败,返回 400。 */
+    @Test
+    void map_missingLegacy_returnsBadRequest() throws Exception {
+        mockMvc.perform(get(MAP_PATH))
+                .andExpect(status().isBadRequest());
+    }
+
+    /**
+     * 权限/角色:map 为注册中心只读取数入口,无角色限定(设计上 parent/child/teacher/admin 共用)。
+     * 匿名(无 Authorization token)也应能到达控制器得到 200,证明接口未被 admin-only 门禁挡住。
+     * (角色/权限控制在此能力键不适用——接口本身不做 role 区分,故以「匿名可达」作为权限契约断言。)
+     */
+    @Test
+    void map_withoutAuthToken_isReachable() throws Exception {
+        mockMvc.perform(get(MAP_PATH).param("legacy", "growth"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").value("body.growth"));
+    }
+
+    private void assertMap(String legacy, String expectedUnified) throws Exception {
+        mockMvc.perform(get(MAP_PATH).param("legacy", legacy))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").value(expectedUnified));
+    }
+}