xiangyixiangqin/server/tests/XiangYi.Application.Tests/Services/RealNameServicePropertyTests.cs
2026-01-02 18:00:49 +08:00

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;
});
}
}