using FsCheck; using FsCheck.Xunit; using Xunit; using XiangYi.Application.Interfaces; using XiangYi.Application.Services; namespace XiangYi.Application.Tests.Services; /// /// RealNameService属性测试 - 身份信息脱敏 /// public class IdentityMaskingPropertyTests { /// /// **Feature: backend-api, Property 19: 身份信息脱敏** /// **Validates: Requirements 8.3** /// /// *For any* 实名认证通过的用户, 存储的姓名和身份证号应进行脱敏处理 /// [Property(MaxTest = 100)] public Property RealName_ShouldBeMaskedCorrectly() { // 生成2-4个字符的中文姓名 var nameArb = Gen.Elements("张", "王", "李", "赵", "刘", "陈", "杨", "黄", "周", "吴") .SelectMany(surname => Gen.Elements("伟", "芳", "娜", "秀英", "敏", "静", "丽", "强", "磊", "军", "洋", "勇", "艳", "杰", "娟", "涛", "明", "超", "秀兰", "霞") .Select(given => surname + given)) .ToArbitrary(); return Prop.ForAll( nameArb, name => { var maskedName = RealNameService.MaskName(name); // 脱敏后的姓名应该保留第一个字 if (maskedName[0] != name[0]) return false; // 脱敏后的姓名长度应该与原始姓名相同 if (maskedName.Length != name.Length) return false; // 除第一个字外,其余应该是* for (int i = 1; i < maskedName.Length; i++) { if (maskedName[i] != '*') return false; } return true; }); } /// /// 身份证号脱敏 - 保留前3位和后4位 /// [Property(MaxTest = 100)] public Property IdCard_ShouldBeMaskedCorrectly() { // 生成18位身份证号 var idCardArb = Gen.Choose(100000, 999999) .SelectMany(prefix => Gen.Choose(19500101, 20051231) .SelectMany(date => Gen.Choose(1000, 9999) .Select(suffix => $"{prefix}{date}{suffix}"))) .Where(id => id.Length == 18) .ToArbitrary(); return Prop.ForAll( idCardArb, idCard => { var maskedIdCard = RealNameService.MaskIdCard(idCard); // 脱敏后的身份证号长度应该与原始相同 if (maskedIdCard.Length != idCard.Length) return false; // 前3位应该保留 if (maskedIdCard[..3] != idCard[..3]) return false; // 后4位应该保留 if (maskedIdCard[^4..] != idCard[^4..]) return false; // 中间应该是* for (int i = 3; i < maskedIdCard.Length - 4; i++) { if (maskedIdCard[i] != '*') return false; } return true; }); } /// /// 空姓名脱敏 - 返回空字符串 /// [Property(MaxTest = 100)] public Property EmptyName_ShouldReturnEmpty() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var maskedNull = RealNameService.MaskName(null!); var maskedEmpty = RealNameService.MaskName(""); return string.IsNullOrEmpty(maskedNull) && string.IsNullOrEmpty(maskedEmpty); }); } /// /// 单字姓名脱敏 - 保留原字 /// [Property(MaxTest = 100)] public Property SingleCharName_ShouldReturnSame() { var singleCharArb = Gen.Elements("张", "王", "李", "赵", "刘").ToArbitrary(); return Prop.ForAll( singleCharArb, name => { var maskedName = RealNameService.MaskName(name); return maskedName == name; }); } /// /// 空身份证号脱敏 - 返回空字符串 /// [Property(MaxTest = 100)] public Property EmptyIdCard_ShouldReturnEmpty() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var maskedNull = RealNameService.MaskIdCard(null!); var maskedEmpty = RealNameService.MaskIdCard(""); return string.IsNullOrEmpty(maskedNull) && string.IsNullOrEmpty(maskedEmpty); }); } /// /// 短身份证号脱敏 - 全部替换为* /// [Property(MaxTest = 100)] public Property ShortIdCard_ShouldBeAllMasked() { var shortIdCardArb = Gen.Choose(1, 7) .SelectMany(len => Gen.ArrayOf(len, Gen.Choose(0, 9)) .Select(arr => string.Join("", arr))) .ToArbitrary(); return Prop.ForAll( shortIdCardArb, idCard => { var maskedIdCard = RealNameService.MaskIdCard(idCard); // 短身份证号应该全部是* return maskedIdCard.All(c => c == '*') && maskedIdCard.Length == idCard.Length; }); } /// /// 验证脱敏函数一致性 /// [Property(MaxTest = 100)] public Property Masking_ShouldBeConsistent() { var nameArb = Gen.Elements("张伟", "王芳", "李娜", "赵敏", "刘强").ToArbitrary(); return Prop.ForAll( nameArb, name => { var idCard = "110101199001011234"; var maskedName = RealNameService.MaskName(name); var maskedIdCard = RealNameService.MaskIdCard(idCard); // 验证脱敏结果一致性 return RealNameService.ValidateMasking(name, maskedName, idCard, maskedIdCard); }); } } /// /// RealNameService属性测试 - 会员免费实名认证 /// public class MemberFreeRealNamePropertyTests { /// /// **Feature: backend-api, Property 20: 会员免费实名认证** /// **Validates: Requirements 8.4** /// /// *For any* 会员用户请求实名认证, 不应创建付费订单 /// [Property(MaxTest = 100)] public Property Member_ShouldGetFreeRealName() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var isMember = true; var isAlreadyRealName = false; var shouldBeFree = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); return shouldBeFree; }); } /// /// 非会员用户 - 需要付费 /// [Property(MaxTest = 100)] public Property NonMember_ShouldNotGetFreeRealName() { return Prop.ForAll( Arb.Default.PositiveInt(), _ => { var isMember = false; var isAlreadyRealName = false; var shouldBeFree = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); return !shouldBeFree; }); } /// /// 已实名用户 - 不需要再认证(无论是否会员) /// [Property(MaxTest = 100)] public Property AlreadyRealName_ShouldNotNeedAuth() { var isMemberArb = Gen.Elements(true, false).ToArbitrary(); return Prop.ForAll( isMemberArb, isMember => { var isAlreadyRealName = true; var shouldBeFree = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); // 已实名用户不应该返回免费(因为不需要再认证) return !shouldBeFree; }); } /// /// 会员状态与免费认证的关联性 /// [Property(MaxTest = 100)] public Property MemberStatus_ShouldDetermineFreeAuth() { var isMemberArb = Gen.Elements(true, false).ToArbitrary(); return Prop.ForAll( isMemberArb, isMember => { var isAlreadyRealName = false; var shouldBeFree = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); // 会员应该免费,非会员应该付费 return shouldBeFree == isMember; }); } /// /// 所有用户状态组合测试 /// [Property(MaxTest = 100)] public Property AllUserStates_ShouldHaveCorrectFreeStatus() { var isMemberArb = Gen.Elements(true, false).ToArbitrary(); return Prop.ForAll( isMemberArb, isMember => { var isAlreadyRealNameArb = Gen.Elements(true, false); foreach (var isAlreadyRealName in new[] { true, false }) { var shouldBeFree = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); // 已实名用户:不应该返回免费(不需要再认证) if (isAlreadyRealName && shouldBeFree) return false; // 未实名会员:应该免费 if (!isAlreadyRealName && isMember && !shouldBeFree) return false; // 未实名非会员:不应该免费 if (!isAlreadyRealName && !isMember && shouldBeFree) return false; } return true; }); } /// /// 免费认证逻辑的幂等性 /// [Property(MaxTest = 100)] public Property FreeAuthCheck_ShouldBeIdempotent() { var isMemberArb = Gen.Elements(true, false).ToArbitrary(); return Prop.ForAll( isMemberArb, isMember => { var isAlreadyRealName = false; var result1 = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); var result2 = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); var result3 = RealNameService.ShouldBeFreeForMember(isMember, isAlreadyRealName); return result1 == result2 && result2 == result3; }); } }