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

test(cfc-backend): S-cfc-40 能力键 team.invitation.create RED 契约级单测

契约来源: docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md v0.7
§5.1/5.2/5.3(团队邀请生成→inviteCode+小程序码wxacode+落地页) + §7.7(复用
distribution_relations.invite_code) + §11-#11(独立落地页) + §12-T1。

被测 DistributionInviteController#generate(POST /api/distribution/invite/generate)。
正常流断言 data 为邀请对象(含 inviteCode/wxacode/systemId/teamName/inviterId),
现实现仅返回 Result<String>(裸邀请码)→ 2 用例红;边界/权限流 5 用例绿。

仅测试代码, 不改 src/main。
E2E Test Bot 5 дней назад
Родитель
Сommit
2d6e67afaa

+ 182 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/TeamInvitationCreateContractTest.java

@@ -0,0 +1,182 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.DistributionService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * TDD 覆盖驱动(S-cfc-40 / 能力键 {@code team.invitation.create},单元 team)—— RED 阶段契约级单测。
+ *
+ * <p>契约来源:{@code docs/superpowers/specs/2026-11-07-referral-team-profit-sharing.md} v0.7
+ * <ul>
+ *   <li>§5.1/5.2:团队负责人/成员生成邀请 → 返回 {@code inviteCode + 小程序码(wxacode, 落地页)},复用
+ *       {@code distribution_relations.invite_code};</li>
+ *   <li>§7.7:团队邀请不新建表,复用 {@code distribution_relations.invite_code} + 现有分销服务
+ *       {@code DistributionService.generateInviteCode};</li>
+ *   <li>§11-#11(已拍板):团队邀请落地页为独立页(团队场景信息不同),复用 {@code POST /api/team-invite/generate}
+ *       约定的生成入口语义;</li>
+ *   <li>§12-T1:场景 A 注册链路补推荐人绑定钩子 + 场景 B 幂等入团接口 + 落地页/小程序码(参照家庭邀请)。</li>
+ * </ul>
+ *
+ * <p>被测:{@link DistributionInviteController#generate}({@code POST /api/distribution/invite/generate},
+ * 当前唯一的「创建团队邀请」入口;设计落地时其响应须满足 §5.2 契约)。
+ *
+ * <p>AC 断言(RED 期:实现未落地 / 边界未覆盖):
+ * <ol>
+ *   <li>正常流:code=200 且 {@code data} 为对象,必须包含 {@code inviteCode} / {@code wxacode}(小程序码,
+ *       指向独立团队落地页)/ {@code systemId} / {@code teamName} / {@code inviterId};
+ *       —— 现实现只返回 {@code Result&lt;String&gt;}(裸邀请码),无 wxacode/团队预览 → <b>红</b>。</li>
+ *   <li>边界流:systemId 缺失 → "systemId不能为空";体系不存在 / 体系未启用 / 用户不在该体系 → 明确错误透传。</li>
+ *   <li>权限流:未登录(无 userId 绑定属性)→ 400;非团队成员不可为他人团队生成邀请。</li>
+ * </ol>
+ *
+ * <p>MockMvc standalone + Mockito:全 service 打桩,不依赖数据库 / 真实微信 / 外部服务,不启 Spring 上下文。
+ */
+public class TeamInvitationCreateContractTest {
+
+    private static final String URL = "/api/distribution/invite/generate";
+
+    @Mock
+    private DistributionService distributionService;
+
+    private MockMvc mockMvc;
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        DistributionInviteController controller = new DistributionInviteController();
+        ReflectionTestUtils.setField(controller, "distributionService", distributionService);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常-1:生成团队邀请成功,data 必须为邀请对象(含 inviteCode/systemId/teamName/inviterId)——当前只返回裸字符串 → RED")
+    public void generate_success_dataMustBeInvitationObject() throws Exception {
+        String code = "ABC23456";
+        when(distributionService.generateInviteCode(eq(10L), eq(7L))).thenReturn(Result.success(code));
+
+        mockMvc.perform(post(URL)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"systemId\":10}")
+                        .requestAttr("userId", 7L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data").isMap())
+                .andExpect(jsonPath("$.data.inviteCode").value(code))
+                .andExpect(jsonPath("$.data.systemId").value(10))
+                .andExpect(jsonPath("$.data.teamName").isString())
+                .andExpect(jsonPath("$.data.inviterId").value(7));
+
+        verify(distributionService).generateInviteCode(eq(10L), eq(7L));
+    }
+
+    @Test
+    @DisplayName("正常-2:生成团队邀请必须返回小程序码(wxacode,独立团队落地页)——当前无 wxacode → RED")
+    public void generate_success_mustReturnWxacodeForLandingPage() throws Exception {
+        when(distributionService.generateInviteCode(eq(10L), eq(7L)))
+                .thenReturn(Result.success("ABC23456"));
+
+        mockMvc.perform(post(URL)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"systemId\":10}")
+                        .requestAttr("userId", 7L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.wxacode").isNotEmpty());
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界-1:systemId 缺失 → 明确错误且不触达 service")
+    public void generate_missingSystemId_returnsError() throws Exception {
+        mockMvc.perform(post(URL)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{}")
+                        .requestAttr("userId", 7L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("systemId不能为空"));
+
+        verify(distributionService, never()).generateInviteCode(anyLong(), anyLong());
+    }
+
+    @Test
+    @DisplayName("边界-2:销售体系不存在 → 错误透传")
+    public void generate_systemNotFound_returnsError() throws Exception {
+        when(distributionService.generateInviteCode(eq(999L), eq(7L)))
+                .thenReturn(Result.error("销售体系不存在"));
+
+        mockMvc.perform(post(URL)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"systemId\":999}")
+                        .requestAttr("userId", 7L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("销售体系不存在"));
+    }
+
+    @Test
+    @DisplayName("边界-3:销售体系未启用 → 错误透传")
+    public void generate_systemDisabled_returnsError() throws Exception {
+        when(distributionService.generateInviteCode(eq(10L), eq(7L)))
+                .thenReturn(Result.error("销售体系未启用"));
+
+        mockMvc.perform(post(URL)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"systemId\":10}")
+                        .requestAttr("userId", 7L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("销售体系未启用"));
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限-1:用户不在该销售体系中 → 拒绝生成(团队成员专属权限)")
+    public void generate_notTeamMember_returnsError() throws Exception {
+        when(distributionService.generateInviteCode(eq(10L), eq(7L)))
+                .thenReturn(Result.error("用户不在该销售体系中"));
+
+        mockMvc.perform(post(URL)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"systemId\":10}")
+                        .requestAttr("userId", 7L))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("用户不在该销售体系中"));
+    }
+
+    @Test
+    @DisplayName("权限-2:未登录(无 userId 绑定属性)→ 400 且不触达 service")
+    public void generate_anonymous_returns400() throws Exception {
+        mockMvc.perform(post(URL)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"systemId\":10}"))
+                .andExpect(status().isBadRequest());
+
+        verify(distributionService, never()).generateInviteCode(anyLong(), anyLong());
+    }
+}