|
@@ -0,0 +1,223 @@
|
|
|
|
|
+package com.etotem.cfc.controller.family;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.dto.AddFamilyMemberDTO;
|
|
|
|
|
+import com.etotem.cfc.dto.FamilyMemberVO;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.service.FamilyMemberService;
|
|
|
|
|
+import com.etotem.cfc.service.UnlockGateService;
|
|
|
|
|
+import com.etotem.cfc.service.UserService;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
|
|
+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.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
|
|
+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-1 / 能力键 family.familyMembers.addMember):
|
|
|
|
|
+ * POST /api/family/member/add 添加家庭成员 的契约级单测。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 契约(对应 API_REFERENCE.md §4.2 与 FamilyMembersController.addMember):
|
|
|
|
|
+ * - userId == null → Result code=500 message=「请先登录」,不触达 Service
|
|
|
|
|
+ * - 正常路径:Service 返回 VO → Result code=200 data=VO;若当前用户有家庭(userMapper.selectById 返回非空且 familyId 非空)
|
|
|
|
|
+ * 则额外触发 unlockGateService.refreshFamilyGates(familyId, userId)(创建家人成功后的通关解锁刷新)
|
|
|
|
|
+ * - 正常路径但用户查询为空 / 用户无家庭:仍 code=200,但不触发 refreshFamilyGates
|
|
|
|
|
+ * - Service 抛 IllegalArgumentException → code=500 message=异常信息
|
|
|
|
|
+ * - Service 抛 RuntimeException → code=500 message=异常信息
|
|
|
|
|
+ *
|
|
|
|
|
+ * 角色/权限口径:addMember 处理器无 @RequestAttribute("role"/"roles"),授权交由 Service 层
|
|
|
|
|
+ * (familyId=登录用户所属家庭)与 JwtInterceptor(匿名注入 userId=-1 被 401 拦截)承担;
|
|
|
|
|
+ * 本控制器唯一鉴权边界是 userId 空守卫,故本测试覆盖该守卫而非角色分支。
|
|
|
|
|
+ * 测试不依赖真实微信/外部服务:FamilyMemberService/UserMapper/UnlockGateService 全 mock。
|
|
|
|
|
+ */
|
|
|
|
|
+public class FamilyMembersControllerAddMemberTest {
|
|
|
|
|
+
|
|
|
|
|
+ @Mock private FamilyMemberService familyMemberService;
|
|
|
|
|
+ @Mock private UserService userService;
|
|
|
|
|
+ @Mock private UnlockGateService unlockGateService;
|
|
|
|
|
+ @Mock private UserMapper userMapper;
|
|
|
|
|
+
|
|
|
|
|
+ private MockMvc mockMvc;
|
|
|
|
|
+ private FamilyMembersController controller;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ public void setUp() {
|
|
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
|
|
+ controller = new FamilyMembersController();
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "familyMemberService", familyMemberService);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "unlockGateService", unlockGateService);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "userService", userService);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "userMapper", userMapper);
|
|
|
|
|
+ mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private User user(Long id, Long familyId) {
|
|
|
|
|
+ User u = new User();
|
|
|
|
|
+ u.setId(id);
|
|
|
|
|
+ u.setFamilyId(familyId);
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static final String VALID_BODY = "{"
|
|
|
|
|
+ + "\"nickname\":\"小明\","
|
|
|
|
|
+ + "\"generationLevel\":\"child\","
|
|
|
|
|
+ + "\"relativeMemberId\":5,"
|
|
|
|
|
+ + "\"peerType\":null,"
|
|
|
|
|
+ + "\"phone\":\"13800001111\","
|
|
|
|
|
+ + "\"gender\":\"male\","
|
|
|
|
|
+ + "\"birthday\":\"2018-01-01\","
|
|
|
|
|
+ + "\"birthWeight\":3200,"
|
|
|
|
|
+ + "\"isAdmin\":0"
|
|
|
|
|
+ + "}";
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void addMember_success_userHasFamily_triggersGateRefresh() throws Exception {
|
|
|
|
|
+ Long userId = 1L;
|
|
|
|
|
+ Long familyId = 7L;
|
|
|
|
|
+ FamilyMemberVO vo = new FamilyMemberVO();
|
|
|
|
|
+ vo.setId(100L);
|
|
|
|
|
+ when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class))).thenReturn(vo);
|
|
|
|
|
+ when(userMapper.selectById(userId)).thenReturn(user(userId, familyId));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/family/member/add")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(VALID_BODY)
|
|
|
|
|
+ .requestAttr("userId", userId))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.id").value(100));
|
|
|
|
|
+
|
|
|
|
|
+ verify(unlockGateService).refreshFamilyGates(familyId, userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void addMember_success_userNotFound_doesNotRefresh() throws Exception {
|
|
|
|
|
+ Long userId = 1L;
|
|
|
|
|
+ FamilyMemberVO vo = new FamilyMemberVO();
|
|
|
|
|
+ vo.setId(101L);
|
|
|
|
|
+ when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class))).thenReturn(vo);
|
|
|
|
|
+ when(userMapper.selectById(userId)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/family/member/add")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(VALID_BODY)
|
|
|
|
|
+ .requestAttr("userId", userId))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.id").value(101));
|
|
|
|
|
+
|
|
|
|
|
+ verify(unlockGateService, never()).refreshFamilyGates(anyLong(), anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void addMember_success_userWithoutFamily_doesNotRefresh() throws Exception {
|
|
|
|
|
+ Long userId = 1L;
|
|
|
|
|
+ FamilyMemberVO vo = new FamilyMemberVO();
|
|
|
|
|
+ vo.setId(102L);
|
|
|
|
|
+ when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class))).thenReturn(vo);
|
|
|
|
|
+ when(userMapper.selectById(userId)).thenReturn(user(userId, null));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/family/member/add")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(VALID_BODY)
|
|
|
|
|
+ .requestAttr("userId", userId))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.id").value(102));
|
|
|
|
|
+
|
|
|
|
|
+ verify(unlockGateService, never()).refreshFamilyGates(anyLong(), anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void addMember_illegalArgument_returnsErrorMessage() throws Exception {
|
|
|
|
|
+ Long userId = 1L;
|
|
|
|
|
+ when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class)))
|
|
|
|
|
+ .thenThrow(new IllegalArgumentException("成员昵称不能为空"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/family/member/add")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(VALID_BODY)
|
|
|
|
|
+ .requestAttr("userId", userId))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(500))
|
|
|
|
|
+ .andExpect(jsonPath("$.message").value("成员昵称不能为空"));
|
|
|
|
|
+
|
|
|
|
|
+ verify(unlockGateService, never()).refreshFamilyGates(anyLong(), anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void addMember_runtimeException_returnsErrorMessage() throws Exception {
|
|
|
|
|
+ Long userId = 1L;
|
|
|
|
|
+ when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class)))
|
|
|
|
|
+ .thenThrow(new RuntimeException("用户未加入家庭,请先创建或加入家庭"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/family/member/add")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(VALID_BODY)
|
|
|
|
|
+ .requestAttr("userId", userId))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(500))
|
|
|
|
|
+ .andExpect(jsonPath("$.message").value("用户未加入家庭,请先创建或加入家庭"));
|
|
|
|
|
+
|
|
|
|
|
+ verify(unlockGateService, never()).refreshFamilyGates(anyLong(), anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void addMember_dtoDeserializedAndPassedToService() throws Exception {
|
|
|
|
|
+ Long userId = 1L;
|
|
|
|
|
+ FamilyMemberVO vo = new FamilyMemberVO();
|
|
|
|
|
+ vo.setId(103L);
|
|
|
|
|
+ when(familyMemberService.addMember(eq(userId), any(AddFamilyMemberDTO.class))).thenReturn(vo);
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/family/member/add")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content(VALID_BODY)
|
|
|
|
|
+ .requestAttr("userId", userId))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200));
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<AddFamilyMemberDTO> captor = ArgumentCaptor.forClass(AddFamilyMemberDTO.class);
|
|
|
|
|
+ verify(familyMemberService).addMember(eq(userId), captor.capture());
|
|
|
|
|
+ AddFamilyMemberDTO dto = captor.getValue();
|
|
|
|
|
+ assertNotNull(dto);
|
|
|
|
|
+ assertEquals("小明", dto.getNickname());
|
|
|
|
|
+ assertEquals("child", dto.getGenerationLevel());
|
|
|
|
|
+ assertEquals(Long.valueOf(5L), dto.getRelativeMemberId());
|
|
|
|
|
+ assertEquals("13800001111", dto.getPhone());
|
|
|
|
|
+ assertEquals("male", dto.getGender());
|
|
|
|
|
+ assertEquals("2018-01-01", dto.getBirthday());
|
|
|
|
|
+ assertEquals(Integer.valueOf(3200), dto.getBirthWeight());
|
|
|
|
|
+ assertEquals(Integer.valueOf(0), dto.getIsAdmin());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * userId 空守卫:MockMvc 的 @RequestAttribute 缺省 required=true 无法注入 null,
|
|
|
|
|
+ * 故按 FamilyControllerTest 先例直接调用控制器方法验证该守卫分支。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Test
|
|
|
|
|
+ public void addMember_nullUserId_returnsLoginRequired() {
|
|
|
|
|
+ AddFamilyMemberDTO dto = new AddFamilyMemberDTO();
|
|
|
|
|
+ dto.setNickname("小明");
|
|
|
|
|
+ dto.setGenerationLevel("child");
|
|
|
|
|
+
|
|
|
|
|
+ com.etotem.cfc.common.Result<FamilyMemberVO> result = controller.addMember(dto, null);
|
|
|
|
|
+ assertEquals(Integer.valueOf(500), result.getCode());
|
|
|
|
|
+ assertEquals("请先登录", result.getMessage());
|
|
|
|
|
+
|
|
|
|
|
+ verify(familyMemberService, never()).addMember(anyLong(), any(AddFamilyMemberDTO.class));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|