|
@@ -0,0 +1,293 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.entity.Family;
|
|
|
|
|
+import com.etotem.cfc.entity.FamilyJoinRequest;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyInvitationMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyJoinRequestMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.service.FamilyJoinRequestService;
|
|
|
|
|
+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.text.ParseException;
|
|
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
|
|
+import java.util.Calendar;
|
|
|
|
|
+import java.util.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.ArgumentMatchers.anyLong;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 契约测试:能力键 {@code family.invite.joinValidationRules}(规格 2026-11-07-family-join-required-info.md WI-3)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>契约锚点</b></p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>需求:§3 AC-R3 / §5 WI-3(AC3.1~AC3.5)</li>
|
|
|
|
|
+ * <li>设计:UserService.java:492-505(gender/birthday 解析口径:gender 严格 male/female、birthday 为 yyyy-MM-dd 合法日期)</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>被校验的准入规则(validateJoinRequiredInfo 前置门禁)</b></p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>真实姓名:trim 后非空,长度 1–20 字;null/空/>20 字 → 抛「请先完善真实姓名、出生日期与性别后再申请加入家庭」</li>
|
|
|
|
|
+ * <li>出生日期:非空、合法日期;晚于今天 → 抛「出生日期不能晚于今天」;早于 1900-01-01 → 抛「出生日期不合法」(不设年龄上限)</li>
|
|
|
|
|
+ * <li>性别:仅限 {@code male}/{@code female}(大小写敏感);null/空/其他/混用 → 抛「请先完善真实姓名、出生日期与性别后再申请加入家庭」</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>测试范围</b>:{@link FamilyJoinRequestService#createJoinRequest(Long, String, String)} 门禁行为。
|
|
|
|
|
+ * 仅用 Mockito(无 Spring / 无 DB / 无真实微信),断言不依赖真实外部服务。
|
|
|
|
|
+ * 字符串可解析性("yyyy-MM-dd" 无法解析串)由上游 UserService 解析口径(设计锚点)保障,
|
|
|
|
|
+ * 门禁层只做 Date 存在性 + 范围判定,故本类以 Date 值驱动。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>RED 预期</b>:门禁块未落地(FamilyJoinRequestService.createJoinRequest 中尚无
|
|
|
|
|
+ * {@code validateJoinRequiredInfo} 调用),缺失/非法项用例在实现补齐前应失败(红)。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+class FamilyJoinValidationRulesContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String MSG_MISSING_REQUIRED = "请先完善真实姓名、出生日期与性别后再申请加入家庭";
|
|
|
|
|
+ private static final String MSG_BIRTHDAY_FUTURE = "出生日期不能晚于今天";
|
|
|
|
|
+ private static final String MSG_BIRTHDAY_ILLEGAL = "出生日期不合法";
|
|
|
|
|
+
|
|
|
|
|
+ private static final Long TARGET_FAMILY_ID = 100L;
|
|
|
|
|
+ private static final Long REQUESTER_USER_ID = 200L;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private FamilyMapper familyMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private FamilyJoinRequestMapper familyJoinRequestMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Mock
|
|
|
|
|
+ private FamilyInvitationMapper familyInvitationMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @InjectMocks
|
|
|
|
|
+ private FamilyJoinRequestService service;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() {
|
|
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
|
|
+ // 数字 inviteCode → familyMapper.selectById 直查家庭;确保家庭存在
|
|
|
|
|
+ Family family = new Family();
|
|
|
|
|
+ family.setId(TARGET_FAMILY_ID);
|
|
|
|
|
+ family.setName("契约测试家庭");
|
|
|
|
|
+ when(familyMapper.selectById(TARGET_FAMILY_ID)).thenReturn(family);
|
|
|
|
|
+ // 无既有待审批请求(门禁通过后走创建分支)
|
|
|
|
|
+ when(familyJoinRequestMapper.selectOne(any())).thenReturn(null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== AC2.1 / AC-R3:真实姓名 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 真实姓名null_拒绝并提示完善信息_不写库() {
|
|
|
|
|
+ User u = user(null, date("1990-05-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_MISSING_REQUIRED, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 真实姓名仅空白_拒绝并提示完善信息() {
|
|
|
|
|
+ User u = user(" ", date("1990-05-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_MISSING_REQUIRED, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 真实姓名超过20字_拒绝并提示完善信息() {
|
|
|
|
|
+ User u = user(repeat("张", 21), date("1990-05-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_MISSING_REQUIRED, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== AC2.2 / AC-R3:出生日期(非空) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 出生日期null_拒绝并提示完善信息_不写库() {
|
|
|
|
|
+ User u = user("张三", null, "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_MISSING_REQUIRED, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== AC2.3 / AC-R3:性别(严格二值) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 性别null_拒绝并提示完善信息() {
|
|
|
|
|
+ User u = user("张三", date("1990-05-01"), null);
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_MISSING_REQUIRED, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 性别大小写混用_拒绝并提示完善信息() {
|
|
|
|
|
+ User u = user("张三", date("1990-05-01"), "Male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_MISSING_REQUIRED, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 性别其他值_拒绝并提示完善信息() {
|
|
|
|
|
+ User u = user("张三", date("1990-05-01"), "unknown");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_MISSING_REQUIRED, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== AC3.1 / AC3.2:出生日期范围 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 出生日期为未来日期_拒绝并提示不能晚于今天_不写库() {
|
|
|
|
|
+ User u = user("张三", date("2099-01-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_BIRTHDAY_FUTURE, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 出生日期早于1900年_拒绝并提示不合法_不写库() {
|
|
|
|
|
+ User u = user("张三", date("1800-01-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.createJoinRequest(REQUESTER_USER_ID, "100", null));
|
|
|
|
|
+ assertEquals(MSG_BIRTHDAY_ILLEGAL, ex.getMessage());
|
|
|
|
|
+ verify(familyJoinRequestMapper, never()).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== AC-R4 / AC2.4:三项齐全合法 → 正常创建 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 三项齐全合法_正常创建请求_待审批() {
|
|
|
|
|
+ User u = user("张三", date("1990-05-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ FamilyJoinRequest created = service.createJoinRequest(REQUESTER_USER_ID, "100", null);
|
|
|
|
|
+
|
|
|
|
|
+ assertNotNull(created);
|
|
|
|
|
+ assertEquals("pending", created.getStatus());
|
|
|
|
|
+ assertEquals(REQUESTER_USER_ID, created.getRequesterUserId());
|
|
|
|
|
+ assertEquals(TARGET_FAMILY_ID, created.getTargetFamilyId());
|
|
|
|
|
+ verify(familyJoinRequestMapper).insert(any(FamilyJoinRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 边界_真实姓名恰好20字_通过() {
|
|
|
|
|
+ User u = user(repeat("张", 20), date("1990-05-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ FamilyJoinRequest created = service.createJoinRequest(REQUESTER_USER_ID, "100", null);
|
|
|
|
|
+ assertEquals("pending", created.getStatus());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 边界_真实姓名恰好1字_通过() {
|
|
|
|
|
+ User u = user("张", date("1990-05-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ FamilyJoinRequest created = service.createJoinRequest(REQUESTER_USER_ID, "100", null);
|
|
|
|
|
+ assertEquals("pending", created.getStatus());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 边界_出生日期等于1900年1月1日_通过() {
|
|
|
|
|
+ User u = user("张三", date("1900-01-01"), "male");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ FamilyJoinRequest created = service.createJoinRequest(REQUESTER_USER_ID, "100", null);
|
|
|
|
|
+ assertEquals("pending", created.getStatus());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 边界_出生日期等于今天_通过() {
|
|
|
|
|
+ User u = user("张三", today(), "female");
|
|
|
|
|
+ stubCurrentUser(u);
|
|
|
|
|
+
|
|
|
|
|
+ FamilyJoinRequest created = service.createJoinRequest(REQUESTER_USER_ID, "100", null);
|
|
|
|
|
+ assertEquals("pending", created.getStatus());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 辅助构造 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private User user(String realName, Date birthday, String gender) {
|
|
|
|
|
+ User u = new User();
|
|
|
|
|
+ u.setId(REQUESTER_USER_ID);
|
|
|
|
|
+ u.setRealName(realName);
|
|
|
|
|
+ u.setBirthday(birthday);
|
|
|
|
|
+ u.setGender(gender);
|
|
|
|
|
+ // 不预置 familyId → 走「加入新家庭」创建分支(非已属成员分支)
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void stubCurrentUser(User u) {
|
|
|
|
|
+ when(userMapper.selectById(anyLong())).thenReturn(u);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date date(String yyyyMMdd) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
|
|
+ sdf.setLenient(false);
|
|
|
|
|
+ return sdf.parse(yyyyMMdd);
|
|
|
|
|
+ } catch (ParseException e) {
|
|
|
|
|
+ throw new IllegalArgumentException("非法测试日期: " + yyyyMMdd, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date today() {
|
|
|
|
|
+ Calendar c = Calendar.getInstance();
|
|
|
|
|
+ c.set(Calendar.HOUR_OF_DAY, 0);
|
|
|
|
|
+ c.set(Calendar.MINUTE, 0);
|
|
|
|
|
+ c.set(Calendar.SECOND, 0);
|
|
|
|
|
+ c.set(Calendar.MILLISECOND, 0);
|
|
|
|
|
+ return c.getTime();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Java 8 兼容:拼接 n 次字符串(String.repeat 为 Java 11+ API) */
|
|
|
|
|
+ private String repeat(String s, int n) {
|
|
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
|
|
+ sb.append(s);
|
|
|
|
|
+ }
|
|
|
|
|
+ return sb.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|