Browse Source

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

Agnes Agent 5 days ago
parent
commit
7d6e3f232e

+ 173 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/FamilyJoinRequestServiceJoinRequiredInfoContractTest.java

@@ -0,0 +1,173 @@
+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 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.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 契约级测试:家庭加入准入校验门禁(TDD-RED,S-cfc-39 / family.invite.joinRequiredInfoTest)
+ *
+ * 需求锚点:docs/superpowers/specs/2026-11-07-family-join-required-info.md
+ *   - §3 AC-R3 校验规则 / §5 WI-2(后端门禁)/ WI-3(规则细化)
+ * 设计锚点:FamilyJoinRequestService.createJoinRequest 校验块 validateJoinRequiredInfo(currentUser)
+ *   - 真实姓名:trim 后非空,长度 1–20 字
+ *   - 出生日期:非空、不晚于今天、不早于 1900-01-01(不设年龄上限,2026-11-07 主理人确认)
+ *   - 性别:仅限 male / female 二值(大小写敏感)
+ *
+ * 三流覆盖:
+ *   - 正常流:三项齐全且合法 → 请求正常创建,insert 调用一次
+ *   - 缺失流:realName/birthday/gender 任一缺失 → 抛「请先完善真实姓名、出生日期与性别后再申请加入家庭」,不落库
+ *   - 非法流:生日未来/极早、姓名超长、性别大小写混用 → 拒绝,不落库
+ */
+class FamilyJoinRequestServiceJoinRequiredInfoContractTest {
+
+    private static final String MSG_INCOMPLETE = "请先完善真实姓名、出生日期与性别后再申请加入家庭";
+    private static final String MSG_FUTURE_BIRTHDAY = "出生日期不能晚于今天";
+    private static final String MSG_ILLEGAL_BIRTHDAY = "出生日期不合法";
+    private static final Long USER_ID = 1L;
+    private static final Long FAMILY_ID = 100L;
+
+    @Mock
+    private FamilyJoinRequestMapper familyJoinRequestMapper;
+
+    @Mock
+    private FamilyMapper familyMapper;
+
+    @Mock
+    private UserMapper userMapper;
+
+    @InjectMocks
+    private FamilyJoinRequestService service;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        Family family = new Family();
+        family.setId(FAMILY_ID);
+        when(familyMapper.selectById(FAMILY_ID)).thenReturn(family);
+        // 默认无待审批请求(避免「您已有待审批的加入请求」先于门禁抛错干扰断言)
+        when(familyJoinRequestMapper.selectOne(any())).thenReturn(null);
+    }
+
+    // ==================== 正常流(AC2.4 / AC5.3) ====================
+
+    @Test
+    void createJoinRequest_withCompleteValidInfo_createsPendingRequest() {
+        givenCurrentUser(validUser());
+
+        FamilyJoinRequest result = service.createJoinRequest(USER_ID, String.valueOf(FAMILY_ID), null);
+
+        assertNotNull(result);
+        assertEquals("pending", result.getStatus());
+        verify(familyJoinRequestMapper).insert(any(FamilyJoinRequest.class));
+    }
+
+    // ==================== 缺失流(AC2.1 / AC2.2 / AC2.3,WI-2 门禁) ====================
+
+    @Test
+    void createJoinRequest_withNullRealName_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setRealName(null);
+        assertRejected(u, MSG_INCOMPLETE);
+    }
+
+    @Test
+    void createJoinRequest_withBlankRealName_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setRealName("   ");
+        assertRejected(u, MSG_INCOMPLETE);
+    }
+
+    @Test
+    void createJoinRequest_withNullBirthday_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setBirthday(null);
+        assertRejected(u, MSG_INCOMPLETE);
+    }
+
+    @Test
+    void createJoinRequest_withNullGender_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setGender(null);
+        assertRejected(u, MSG_INCOMPLETE);
+    }
+
+    @Test
+    void createJoinRequest_withEmptyGender_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setGender("");
+        assertRejected(u, MSG_INCOMPLETE);
+    }
+
+    // ==================== 非法流(AC3.1 / AC3.2 / AC3.3 / AC3.4,WI-3 规则细化) ====================
+
+    @Test
+    void createJoinRequest_withRealNameOver20Chars_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setRealName("这是一个超过二十个字符的超长真实姓名测试用");
+        assertRejected(u, MSG_INCOMPLETE);
+    }
+
+    @Test
+    void createJoinRequest_withMixedCaseGender_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setGender("Male");
+        assertRejected(u, MSG_INCOMPLETE);
+    }
+
+    @Test
+    void createJoinRequest_withFutureBirthday_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setBirthday(Date.valueOf("2099-01-01"));
+        assertRejected(u, MSG_FUTURE_BIRTHDAY);
+    }
+
+    @Test
+    void createJoinRequest_withTooEarlyBirthday_rejectedAndNoInsert() {
+        User u = validUser();
+        u.setBirthday(Date.valueOf("1800-01-01"));
+        assertRejected(u, MSG_ILLEGAL_BIRTHDAY);
+    }
+
+    // ==================== 辅助构造 ====================
+
+    private User validUser() {
+        User u = new User();
+        u.setId(USER_ID);
+        u.setNickname("zhangsan");
+        u.setRealName("张三");
+        u.setBirthday(Date.valueOf("2000-01-01"));
+        u.setGender("male");
+        return u;
+    }
+
+    private void givenCurrentUser(User user) {
+        when(userMapper.selectById(USER_ID)).thenReturn(user);
+    }
+
+    private void assertRejected(User user, String expectedMessage) {
+        givenCurrentUser(user);
+        RuntimeException ex = assertThrows(RuntimeException.class,
+                () -> service.createJoinRequest(USER_ID, String.valueOf(FAMILY_ID), null));
+        assertEquals(expectedMessage, ex.getMessage());
+        verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
+    }
+}