|
|
@@ -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");
|
|
|
+ }
|
|
|
+}
|