340 lines
10 KiB
C#
340 lines
10 KiB
C#
using FsCheck;
|
|
using FsCheck.Xunit;
|
|
using Xunit;
|
|
using XiangYi.Application.Interfaces;
|
|
using XiangYi.Application.Services;
|
|
|
|
namespace XiangYi.Application.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// RealNameService属性测试 - 身份信息脱敏
|
|
/// </summary>
|
|
public class IdentityMaskingPropertyTests
|
|
{
|
|
/// <summary>
|
|
/// **Feature: backend-api, Property 19: 身份信息脱敏**
|
|
/// **Validates: Requirements 8.3**
|
|
///
|
|
/// *For any* 实名认证通过的用户, 存储的姓名和身份证号应进行脱敏处理
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 身份证号脱敏 - 保留前3位和后4位
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 空姓名脱敏 - 返回空字符串
|
|
/// </summary>
|
|
[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);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 单字姓名脱敏 - 保留原字
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 空身份证号脱敏 - 返回空字符串
|
|
/// </summary>
|
|
[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);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 短身份证号脱敏 - 全部替换为*
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证脱敏函数一致性
|
|
/// </summary>
|
|
[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);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// RealNameService属性测试 - 会员免费实名认证
|
|
/// </summary>
|
|
public class MemberFreeRealNamePropertyTests
|
|
{
|
|
/// <summary>
|
|
/// **Feature: backend-api, Property 20: 会员免费实名认证**
|
|
/// **Validates: Requirements 8.4**
|
|
///
|
|
/// *For any* 会员用户请求实名认证, 不应创建付费订单
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 非会员用户 - 需要付费
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 已实名用户 - 不需要再认证(无论是否会员)
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 会员状态与免费认证的关联性
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 所有用户状态组合测试
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 免费认证逻辑的幂等性
|
|
/// </summary>
|
|
[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;
|
|
});
|
|
}
|
|
}
|