using FsCheck;
using FsCheck.Xunit;
using Xunit;
using XiangYi.Application.Services;
using XiangYi.Core.Enums;
namespace XiangYi.Application.Tests.Services;
///
/// ProfileService属性测试
///
public class ProfileServicePropertyTests
{
///
/// **Feature: backend-api, Property 2: 昵称生成规则正确性**
/// **Validates: Requirements 2.1**
///
/// *For any* 用户资料提交, 系统生成的昵称应符合规则:
/// 父亲->"X家长(父亲)",母亲->"X家长(母亲)",
/// 本人男->"X先生(本人)",本人女->"X女士(本人)"
///
[Property(MaxTest = 100)]
public Property Nickname_ShouldFollowGenerationRules()
{
// 生成有效的姓氏(非空非纯空白字符串)
var surnameArb = Arb.Default.NonEmptyString()
.Filter(s => !string.IsNullOrWhiteSpace(s.Get))
.Generator
.Select(s => s.Get.Trim().Substring(0, Math.Min(s.Get.Trim().Length, 2)));
// 生成有效的关系类型(1父亲, 2母亲, 3本人)
var relationshipArb = Gen.Elements(1, 2, 3);
// 生成有效的性别(1男, 2女)
var genderArb = Gen.Elements(1, 2);
return Prop.ForAll(
surnameArb.ToArbitrary(),
relationshipArb.ToArbitrary(),
genderArb.ToArbitrary(),
(surname, relationship, gender) =>
{
// Act
var nickname = ProfileService.GenerateNicknameStatic(relationship, surname, gender);
// Assert
return relationship switch
{
(int)Relationship.Father => nickname == $"{surname}家长(父亲)",
(int)Relationship.Mother => nickname == $"{surname}家长(母亲)",
(int)Relationship.Self when gender == (int)Gender.Male => nickname == $"{surname}先生(本人)",
(int)Relationship.Self when gender == (int)Gender.Female => nickname == $"{surname}女士(本人)",
_ => false
};
});
}
///
/// 昵称生成 - 空姓氏应使用默认值"某"
///
[Property(MaxTest = 100)]
public Property Nickname_EmptySurname_ShouldUseDefault()
{
var relationshipArb = Gen.Elements(1, 2, 3);
var genderArb = Gen.Elements(1, 2);
var emptySurnameArb = Gen.Elements("", " ", " ", null!);
return Prop.ForAll(
emptySurnameArb.ToArbitrary(),
relationshipArb.ToArbitrary(),
genderArb.ToArbitrary(),
(surname, relationship, gender) =>
{
// Act
var nickname = ProfileService.GenerateNicknameStatic(relationship, surname, gender);
// Assert - 应该使用默认姓氏"某"
return nickname.StartsWith("某");
});
}
///
/// 昵称生成 - 父亲关系应包含"父亲"
///
[Property(MaxTest = 100)]
public Property Nickname_Father_ShouldContainFatherSuffix()
{
var surnameArb = Gen.Elements("李", "王", "张", "刘", "陈");
var genderArb = Gen.Elements(1, 2);
return Prop.ForAll(
surnameArb.ToArbitrary(),
genderArb.ToArbitrary(),
(surname, gender) =>
{
var nickname = ProfileService.GenerateNicknameStatic((int)Relationship.Father, surname, gender);
return nickname.Contains("父亲") && nickname.Contains("家长");
});
}
///
/// 昵称生成 - 母亲关系应包含"母亲"
///
[Property(MaxTest = 100)]
public Property Nickname_Mother_ShouldContainMotherSuffix()
{
var surnameArb = Gen.Elements("李", "王", "张", "刘", "陈");
var genderArb = Gen.Elements(1, 2);
return Prop.ForAll(
surnameArb.ToArbitrary(),
genderArb.ToArbitrary(),
(surname, gender) =>
{
var nickname = ProfileService.GenerateNicknameStatic((int)Relationship.Mother, surname, gender);
return nickname.Contains("母亲") && nickname.Contains("家长");
});
}
///
/// 昵称生成 - 本人男性应包含"先生"
///
[Property(MaxTest = 100)]
public Property Nickname_SelfMale_ShouldContainMrSuffix()
{
var surnameArb = Gen.Elements("李", "王", "张", "刘", "陈");
return Prop.ForAll(
surnameArb.ToArbitrary(),
surname =>
{
var nickname = ProfileService.GenerateNicknameStatic((int)Relationship.Self, surname, (int)Gender.Male);
return nickname.Contains("先生") && nickname.Contains("本人");
});
}
///
/// 昵称生成 - 本人女性应包含"女士"
///
[Property(MaxTest = 100)]
public Property Nickname_SelfFemale_ShouldContainMsSuffix()
{
var surnameArb = Gen.Elements("李", "王", "张", "刘", "陈");
return Prop.ForAll(
surnameArb.ToArbitrary(),
surname =>
{
var nickname = ProfileService.GenerateNicknameStatic((int)Relationship.Self, surname, (int)Gender.Female);
return nickname.Contains("女士") && nickname.Contains("本人");
});
}
}
///
/// 照片数量限制属性测试
///
public class PhotoLimitPropertyTests
{
///
/// **Feature: backend-api, Property 3: 照片数量限制**
/// **Validates: Requirements 2.2**
///
/// *For any* 用户上传照片操作, 当照片总数超过5张时应拒绝上传
///
/// 此测试验证:CanUploadPhotos方法正确判断是否可以上传照片
///
[Property(MaxTest = 100)]
public Property PhotoUpload_ShouldRejectWhenExceedsLimit()
{
// 生成当前照片数量(0-10)
var currentCountArb = Gen.Choose(0, 10);
// 生成要上传的照片数量(1-10)
var uploadCountArb = Gen.Choose(1, 10);
return Prop.ForAll(
currentCountArb.ToArbitrary(),
uploadCountArb.ToArbitrary(),
(currentCount, uploadCount) =>
{
// Act - 使用静态方法验证逻辑
var canUpload = CanUploadPhotosStatic(currentCount, uploadCount, ProfileService.MaxPhotoCount);
// Assert
// 如果当前数量 + 上传数量 <= 5,应该允许上传
// 如果当前数量 + 上传数量 > 5,应该拒绝上传
var expectedResult = currentCount + uploadCount <= ProfileService.MaxPhotoCount;
return canUpload == expectedResult;
});
}
///
/// 照片数量限制 - 边界测试:恰好5张时不能再上传
///
[Property(MaxTest = 100)]
public Property PhotoUpload_AtMaxLimit_ShouldRejectAnyUpload()
{
var uploadCountArb = Gen.Choose(1, 10);
return Prop.ForAll(
uploadCountArb.ToArbitrary(),
uploadCount =>
{
// 当前已有5张照片
var currentCount = ProfileService.MaxPhotoCount;
var canUpload = CanUploadPhotosStatic(currentCount, uploadCount, ProfileService.MaxPhotoCount);
// 应该拒绝任何上传
return !canUpload;
});
}
///
/// 照片数量限制 - 边界测试:4张时只能上传1张
///
[Property(MaxTest = 100)]
public Property PhotoUpload_OneBelowLimit_ShouldAllowOnlyOne()
{
var uploadCountArb = Gen.Choose(1, 10);
return Prop.ForAll(
uploadCountArb.ToArbitrary(),
uploadCount =>
{
// 当前已有4张照片
var currentCount = ProfileService.MaxPhotoCount - 1;
var canUpload = CanUploadPhotosStatic(currentCount, uploadCount, ProfileService.MaxPhotoCount);
// 只有上传1张时才允许
return canUpload == (uploadCount == 1);
});
}
///
/// 照片数量限制 - 0张时可以上传最多5张
///
[Property(MaxTest = 100)]
public Property PhotoUpload_EmptyPhotos_ShouldAllowUpToMax()
{
var uploadCountArb = Gen.Choose(1, 10);
return Prop.ForAll(
uploadCountArb.ToArbitrary(),
uploadCount =>
{
// 当前没有照片
var currentCount = 0;
var canUpload = CanUploadPhotosStatic(currentCount, uploadCount, ProfileService.MaxPhotoCount);
// 上传数量 <= 5 时允许
return canUpload == (uploadCount <= ProfileService.MaxPhotoCount);
});
}
///
/// 照片数量限制 - 最大照片数量常量应为5
///
[Fact]
public void MaxPhotoCount_ShouldBe5()
{
Assert.Equal(5, ProfileService.MaxPhotoCount);
}
///
/// 静态方法:验证是否可以上传照片
///
private static bool CanUploadPhotosStatic(int currentCount, int uploadCount, int maxCount)
{
return currentCount + uploadCount <= maxCount;
}
}
///
/// 照片隐私控制属性测试
///
public class PhotoPrivacyPropertyTests
{
///
/// **Feature: backend-api, Property 4: 照片隐私控制**
/// **Validates: Requirements 2.5**
///
/// *For any* 设置照片不公开的用户, 在推荐列表中返回时照片字段应为空或模糊处理
///
/// 此测试验证:FilterPhotosForDisplay方法正确处理照片隐私
///
[Property(MaxTest = 100)]
public Property PhotoPrivacy_WhenNotPublic_ShouldHidePhotos()
{
// 生成照片URL列表(1-5张)
var photoCountArb = Gen.Choose(1, 5);
return Prop.ForAll(
photoCountArb.ToArbitrary(),
count =>
{
// Arrange
var photos = Enumerable.Range(1, count)
.Select(i => $"https://example.com/photo{i}.jpg")
.ToList();
// Act - 照片不公开时
var filteredPhotos = ProfileService.FilterPhotosForDisplay(photos, isPhotoPublic: false, isViewerMember: false);
// Assert - 照片应该被隐藏(返回空列表)
return filteredPhotos.Count == 0;
});
}
///
/// 照片隐私 - 公开照片应该正常显示
///
[Property(MaxTest = 100)]
public Property PhotoPrivacy_WhenPublic_ShouldShowPhotos()
{
var photoCountArb = Gen.Choose(1, 5);
return Prop.ForAll(
photoCountArb.ToArbitrary(),
count =>
{
// Arrange
var photos = Enumerable.Range(1, count)
.Select(i => $"https://example.com/photo{i}.jpg")
.ToList();
// Act - 照片公开时
var filteredPhotos = ProfileService.FilterPhotosForDisplay(photos, isPhotoPublic: true, isViewerMember: false);
// Assert - 照片应该正常显示
return filteredPhotos.Count == photos.Count &&
filteredPhotos.SequenceEqual(photos);
});
}
///
/// 照片隐私 - 会员查看不公开照片时应该显示(交换照片后)
/// 注:此测试验证会员特权逻辑,实际实现中会员可能需要通过交换请求才能查看
///
[Property(MaxTest = 100)]
public Property PhotoPrivacy_MemberViewing_ShouldRespectPrivacySetting()
{
var photoCountArb = Gen.Choose(1, 5);
var isPublicArb = Gen.Elements(true, false);
return Prop.ForAll(
photoCountArb.ToArbitrary(),
isPublicArb.ToArbitrary(),
(count, isPublic) =>
{
// Arrange
var photos = Enumerable.Range(1, count)
.Select(i => $"https://example.com/photo{i}.jpg")
.ToList();
// Act - 会员查看时
var filteredPhotos = ProfileService.FilterPhotosForDisplay(photos, isPhotoPublic: isPublic, isViewerMember: true);
// Assert
// 如果照片公开,会员可以看到
// 如果照片不公开,会员也看不到(需要通过交换请求)
if (isPublic)
{
return filteredPhotos.Count == photos.Count;
}
else
{
// 不公开时,即使是会员也看不到(需要交换)
return filteredPhotos.Count == 0;
}
});
}
///
/// 照片隐私 - 空照片列表应该返回空
///
[Property(MaxTest = 100)]
public Property PhotoPrivacy_EmptyPhotos_ShouldReturnEmpty()
{
var isPublicArb = Gen.Elements(true, false);
var isMemberArb = Gen.Elements(true, false);
return Prop.ForAll(
isPublicArb.ToArbitrary(),
isMemberArb.ToArbitrary(),
(isPublic, isMember) =>
{
// Arrange
var photos = new List();
// Act
var filteredPhotos = ProfileService.FilterPhotosForDisplay(photos, isPhotoPublic: isPublic, isViewerMember: isMember);
// Assert - 空列表应该返回空
return filteredPhotos.Count == 0;
});
}
///
/// 照片隐私 - 隐私设置不应影响照片数量(只影响是否显示)
///
[Property(MaxTest = 100)]
public Property PhotoPrivacy_PrivacySetting_ShouldNotAffectOriginalList()
{
var photoCountArb = Gen.Choose(1, 5);
return Prop.ForAll(
photoCountArb.ToArbitrary(),
count =>
{
// Arrange
var originalPhotos = Enumerable.Range(1, count)
.Select(i => $"https://example.com/photo{i}.jpg")
.ToList();
var photosCopy = new List(originalPhotos);
// Act - 调用过滤方法
_ = ProfileService.FilterPhotosForDisplay(originalPhotos, isPhotoPublic: false, isViewerMember: false);
// Assert - 原始列表不应被修改
return originalPhotos.SequenceEqual(photosCopy);
});
}
}