|
|
@@ -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<String>}(裸邀请码),无 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());
|
|
|
+ }
|
|
|
+}
|