426 lines
15 KiB
C#
426 lines
15 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Xunit;
|
||
using XiangYi.Application.Services;
|
||
using XiangYi.Core.Enums;
|
||
|
||
namespace XiangYi.Application.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// ProfileService属性测试
|
||
/// </summary>
|
||
public class ProfileServicePropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 2: 昵称生成规则正确性**
|
||
/// **Validates: Requirements 2.1**
|
||
///
|
||
/// *For any* 用户资料提交, 系统生成的昵称应符合规则:
|
||
/// 父亲->"X家长(父亲)",母亲->"X家长(母亲)",
|
||
/// 本人男->"X先生(本人)",本人女->"X女士(本人)"
|
||
/// </summary>
|
||
[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
|
||
};
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 昵称生成 - 空姓氏应使用默认值"某"
|
||
/// </summary>
|
||
[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("某");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 昵称生成 - 父亲关系应包含"父亲"
|
||
/// </summary>
|
||
[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("家长");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 昵称生成 - 母亲关系应包含"母亲"
|
||
/// </summary>
|
||
[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("家长");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 昵称生成 - 本人男性应包含"先生"
|
||
/// </summary>
|
||
[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("本人");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 昵称生成 - 本人女性应包含"女士"
|
||
/// </summary>
|
||
[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("本人");
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 照片数量限制属性测试
|
||
/// </summary>
|
||
public class PhotoLimitPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 3: 照片数量限制**
|
||
/// **Validates: Requirements 2.2**
|
||
///
|
||
/// *For any* 用户上传照片操作, 当照片总数超过5张时应拒绝上传
|
||
///
|
||
/// 此测试验证:CanUploadPhotos方法正确判断是否可以上传照片
|
||
/// </summary>
|
||
[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;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片数量限制 - 边界测试:恰好5张时不能再上传
|
||
/// </summary>
|
||
[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;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片数量限制 - 边界测试:4张时只能上传1张
|
||
/// </summary>
|
||
[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);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片数量限制 - 0张时可以上传最多5张
|
||
/// </summary>
|
||
[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);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片数量限制 - 最大照片数量常量应为5
|
||
/// </summary>
|
||
[Fact]
|
||
public void MaxPhotoCount_ShouldBe5()
|
||
{
|
||
Assert.Equal(5, ProfileService.MaxPhotoCount);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 静态方法:验证是否可以上传照片
|
||
/// </summary>
|
||
private static bool CanUploadPhotosStatic(int currentCount, int uploadCount, int maxCount)
|
||
{
|
||
return currentCount + uploadCount <= maxCount;
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 照片隐私控制属性测试
|
||
/// </summary>
|
||
public class PhotoPrivacyPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 4: 照片隐私控制**
|
||
/// **Validates: Requirements 2.5**
|
||
///
|
||
/// *For any* 设置照片不公开的用户, 在推荐列表中返回时照片字段应为空或模糊处理
|
||
///
|
||
/// 此测试验证:FilterPhotosForDisplay方法正确处理照片隐私
|
||
/// </summary>
|
||
[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;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片隐私 - 公开照片应该正常显示
|
||
/// </summary>
|
||
[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);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片隐私 - 会员查看不公开照片时应该显示(交换照片后)
|
||
/// 注:此测试验证会员特权逻辑,实际实现中会员可能需要通过交换请求才能查看
|
||
/// </summary>
|
||
[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;
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片隐私 - 空照片列表应该返回空
|
||
/// </summary>
|
||
[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<string>();
|
||
|
||
// Act
|
||
var filteredPhotos = ProfileService.FilterPhotosForDisplay(photos, isPhotoPublic: isPublic, isViewerMember: isMember);
|
||
|
||
// Assert - 空列表应该返回空
|
||
return filteredPhotos.Count == 0;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片隐私 - 隐私设置不应影响照片数量(只影响是否显示)
|
||
/// </summary>
|
||
[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<string>(originalPhotos);
|
||
|
||
// Act - 调用过滤方法
|
||
_ = ProfileService.FilterPhotosForDisplay(originalPhotos, isPhotoPublic: false, isViewerMember: false);
|
||
|
||
// Assert - 原始列表不应被修改
|
||
return originalPhotos.SequenceEqual(photosCopy);
|
||
});
|
||
}
|
||
}
|