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