소스 검색

merge(task/S-cfc-36): RED 契约级单测并入主链

Agnes Agent 5 일 전
부모
커밋
880683d753
1개의 변경된 파일과 245개의 추가작업 그리고 0개의 파일을 삭제
  1. 245 0
      cfc-backend/src/test/java/com/etotem/cfc/service/FamilyJoinRequestServiceJoinValidationGateTest.java

+ 245 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FamilyJoinRequestServiceJoinValidationGateTest.java

@@ -0,0 +1,245 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.FamilyJoinRequest;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyJoinRequestMapper;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.sql.Date;
+import java.time.LocalDate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级单测:家庭加入准入门禁(能力键 family.invite.joinValidationGate)
+ * 规格:docs/superpowers/specs/2026-11-07-family-join-required-info.md §3 AC-R2 / §5 WI-2
+ * 设计锚点:FamilyJoinRequestService.createJoinRequest + User 实体
+ *
+ * 契约断言(WI-2 验收条件):
+ * - AC2.1/AC2.2/AC2.3:realName 空 / birthday 为 null / gender 非 male|female → 拒绝加入,
+ *   抛「请先完善真实姓名、出生日期与性别后再申请加入家庭」,且不在 family_join_requests 产生任何记录。
+ * - AC3.1:birthday 为未来日期 → 抛「出生日期不能晚于今天」。
+ * - AC3.2:birthday 早于 1900-01-01 → 抛「出生日期不合法」。
+ * - AC3.3:realName 超 20 字 → 拒绝。
+ * - AC3.4:gender 大小写混用(如 Male)/其他值 → 拒绝。
+ * - AC2.4 / AC-R4:三项齐全且合法 → 正常创建请求(进入待审批),既有逻辑不受影响。
+ *
+ * TDD-RED:当前 createJoinRequest 未实现门禁,缺失/非法字段用例将失败(红)。
+ */
+class FamilyJoinRequestServiceJoinValidationGateTest {
+
+    private static final String GEN_MSG = "请先完善真实姓名、出生日期与性别后再申请加入家庭";
+    private static final String FUTURE_MSG = "出生日期不能晚于今天";
+    private static final String EARLY_MSG = "出生日期不合法";
+    private static final long FAMILY_ID = 100L;
+    private static final long USER_ID = 3L;
+    private static final String INVITE_CODE = "100"; // 数字 familyId 路径
+
+    @Mock
+    private FamilyJoinRequestMapper familyJoinRequestMapper;
+
+    @Mock
+    private FamilyMapper familyMapper;
+
+    @Mock
+    private UserMapper userMapper;
+
+    @InjectMocks
+    private FamilyJoinRequestService service;
+
+    private Family family;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        family = new Family();
+        family.setId(FAMILY_ID);
+        family.setName("测试家庭");
+        when(familyMapper.selectById(FAMILY_ID)).thenReturn(family);
+    }
+
+    // ==================== AC2.1:realName 空 → 拒绝且无 DB 记录 ====================
+
+    @Test
+    void createRequest_realNameNull_rejectsAndNoInsert() {
+        User u = user(USER_ID, null, null, "male", validBirthday());
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    @Test
+    void createRequest_realNameBlank_rejectsAndNoInsert() {
+        User u = user(USER_ID, null, "   ", "male", validBirthday());
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC3.3:realName 超 20 字 → 拒绝 ====================
+
+    @Test
+    void createRequest_realNameTooLong_rejectsAndNoInsert() {
+        String longName = "这是一个由二十一个汉字组成的非常长的真实姓名字符串";
+        assertTrue(longName.length() > 20, "前置条件:姓名必须超过 20 字");
+        User u = user(USER_ID, null, longName, "male", validBirthday());
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC2.2:birthday 为 null → 拒绝且无 DB 记录 ====================
+
+    @Test
+    void createRequest_birthdayNull_rejectsAndNoInsert() {
+        User u = user(USER_ID, null, "张三", "male", null);
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC2.3:gender 非 male/female → 拒绝且无 DB 记录 ====================
+
+    @Test
+    void createRequest_genderNull_rejectsAndNoInsert() {
+        User u = user(USER_ID, null, "张三", null, validBirthday());
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    @Test
+    void createRequest_genderBlank_rejectsAndNoInsert() {
+        User u = user(USER_ID, null, "张三", "", validBirthday());
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC3.4:gender 大小写混用 / 其他值 → 拒绝 ====================
+
+    @Test
+    void createRequest_genderCaseMixed_rejectsAndNoInsert() {
+        User u = user(USER_ID, null, "张三", "Male", validBirthday());
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    @Test
+    void createRequest_genderOtherValue_rejectsAndNoInsert() {
+        User u = user(USER_ID, null, "张三", "other", validBirthday());
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(GEN_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC3.1:birthday 未来日期 → 拒绝,提示「出生日期不能晚于今天」 ====================
+
+    @Test
+    void createRequest_birthdayFuture_rejectsWithSpecificMessage() {
+        User u = user(USER_ID, null, "张三", "male", Date.valueOf("2099-01-01"));
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(FUTURE_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC3.2:birthday 早于 1900-01-01 → 拒绝,提示「出生日期不合法」 ====================
+
+    @Test
+    void createRequest_birthdayTooEarly_rejectsWithSpecificMessage() {
+        User u = user(USER_ID, null, "张三", "male", Date.valueOf("1800-01-01"));
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, INVITE_CODE, null));
+        assertEquals(EARLY_MSG, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC-R3 边界:birthday = 今天 → 合法(不晚于今天) ====================
+
+    @Test
+    void createRequest_birthdayToday_createsRequest() {
+        User u = user(USER_ID, null, "张三", "male", Date.valueOf(LocalDate.now()));
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        FamilyJoinRequest created = service.createJoinRequest(USER_ID, INVITE_CODE, null);
+        assertNotNull(created);
+        assertEquals("pending", created.getStatus());
+        assertEquals(USER_ID, created.getRequesterUserId());
+        verify(familyJoinRequestMapper).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== AC2.4 / AC-R4:三项齐全且合法 → 正常创建请求 ====================
+
+    @Test
+    void createRequest_allValid_createsRequest() {
+        User u = user(USER_ID, null, "张三", "male", Date.valueOf("2000-01-01"));
+        when(userMapper.selectById(USER_ID)).thenReturn(u);
+
+        FamilyJoinRequest created = service.createJoinRequest(USER_ID, INVITE_CODE, null);
+        assertNotNull(created);
+        assertEquals("pending", created.getStatus());
+        assertEquals(FAMILY_ID, created.getTargetFamilyId());
+        assertEquals(USER_ID, created.getRequesterUserId());
+        verify(familyJoinRequestMapper).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== 辅助构造 ====================
+
+    private User user(Long id, Long familyId, String realName, String gender, Date birthday) {
+        User u = new User();
+        u.setId(id);
+        u.setFamilyId(familyId);
+        u.setRealName(realName);
+        u.setGender(gender);
+        u.setBirthday(birthday);
+        return u;
+    }
+
+    private Date validBirthday() {
+        return Date.valueOf("2000-01-01");
+    }
+}