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

540 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using FsCheck;
using FsCheck.Xunit;
using Xunit;
using XiangYi.Application.DTOs.Requests;
using XiangYi.Application.Services;
using XiangYi.Core.Entities.Biz;
using XiangYi.Core.Enums;
namespace XiangYi.Application.Tests.Services;
/// <summary>
/// SearchService属性测试 - 搜索条件过滤正确性
/// </summary>
public class SearchCriteriaFilterPropertyTests
{
/// <summary>
/// **Feature: backend-api, Property 12: 搜索条件过滤正确性**
/// **Validates: Requirements 5.1**
///
/// *For any* 搜索请求, 返回的用户列表中每个用户都应满足所有搜索条件
/// </summary>
[Property(MaxTest = 100)]
public Property SearchCriteria_AllMatchedUsersShouldSatisfyAllConditions()
{
// 生成有效的用户资料参数
var birthYearArb = Gen.Choose(1970, 2005);
var heightArb = Gen.Choose(150, 200);
return Prop.ForAll(
birthYearArb.ToArbitrary(),
heightArb.ToArbitrary(),
(birthYear, height) =>
{
// 创建用户资料
var profile = CreateBasicProfile(birthYear, height);
// 创建空搜索条件
var request = new SearchRequest();
// Act
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// Assert - 空条件应匹配所有用户
return matches;
});
}
/// <summary>
/// 年龄过滤应正确工作
/// </summary>
[Property(MaxTest = 100)]
public Property AgeFilter_ShouldWorkCorrectly()
{
var birthYearArb = Gen.Choose(1970, 2005);
var ageMinArb = Gen.Choose(18, 40);
var ageMaxArb = Gen.Choose(25, 60);
return Prop.ForAll(
birthYearArb.ToArbitrary(),
ageMinArb.ToArbitrary(),
ageMaxArb.ToArbitrary(),
(birthYear, ageMin, ageMax) =>
{
// 确保ageMin <= ageMax
var actualMin = Math.Min(ageMin, ageMax);
var actualMax = Math.Max(ageMin, ageMax);
var profile = CreateBasicProfile(birthYear);
var request = new SearchRequest { AgeMin = actualMin, AgeMax = actualMax };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
var age = DateTime.Now.Year - birthYear;
// 验证:匹配当且仅当年龄在范围内
var expectedMatch = age >= actualMin && age <= actualMax;
return matches == expectedMatch;
});
}
/// <summary>
/// 身高过滤应正确工作
/// </summary>
[Property(MaxTest = 100)]
public Property HeightFilter_ShouldWorkCorrectly()
{
var heightArb = Gen.Choose(150, 200);
var heightMinArb = Gen.Choose(155, 175);
var heightMaxArb = Gen.Choose(170, 195);
return Prop.ForAll(
heightArb.ToArbitrary(),
heightMinArb.ToArbitrary(),
heightMaxArb.ToArbitrary(),
(height, heightMin, heightMax) =>
{
// 确保heightMin <= heightMax
var actualMin = Math.Min(heightMin, heightMax);
var actualMax = Math.Max(heightMin, heightMax);
var profile = CreateBasicProfile(1990);
profile.Height = height;
var request = new SearchRequest { HeightMin = actualMin, HeightMax = actualMax };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 验证:匹配当且仅当身高在范围内
var expectedMatch = height >= actualMin && height <= actualMax;
return matches == expectedMatch;
});
}
/// <summary>
/// 月收入过滤应正确工作
/// </summary>
[Property(MaxTest = 100)]
public Property MonthlyIncomeFilter_ShouldWorkCorrectly()
{
var incomeArb = Gen.Choose(1, 10);
var incomeMinArb = Gen.Choose(1, 5);
var incomeMaxArb = Gen.Choose(5, 10);
return Prop.ForAll(
incomeArb.ToArbitrary(),
incomeMinArb.ToArbitrary(),
incomeMaxArb.ToArbitrary(),
(income, incomeMin, incomeMax) =>
{
// 确保incomeMin <= incomeMax
var actualMin = Math.Min(incomeMin, incomeMax);
var actualMax = Math.Max(incomeMin, incomeMax);
var profile = CreateBasicProfile(1990);
profile.MonthlyIncome = income;
var request = new SearchRequest { MonthlyIncomeMin = actualMin, MonthlyIncomeMax = actualMax };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 验证:匹配当且仅当收入在范围内
var expectedMatch = income >= actualMin && income <= actualMax;
return matches == expectedMatch;
});
}
/// <summary>
/// 工作城市过滤应正确工作
/// </summary>
[Property(MaxTest = 100)]
public Property WorkCityFilter_ShouldWorkCorrectly()
{
var cities = new[] { "北京", "上海", "广州", "深圳", "杭州" };
var cityArb = Gen.Elements(cities);
return Prop.ForAll(
cityArb.ToArbitrary(),
cityArb.ToArbitrary(),
(profileCity, searchCity) =>
{
var profile = CreateBasicProfile(1990);
profile.WorkCity = profileCity;
var request = new SearchRequest { WorkCity = searchCity };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 验证:匹配当且仅当城市相同(忽略大小写)
var expectedMatch = string.Equals(profileCity, searchCity, StringComparison.OrdinalIgnoreCase);
return matches == expectedMatch;
});
}
/// <summary>
/// 房产情况过滤应正确工作OR逻辑
/// </summary>
[Property(MaxTest = 100)]
public Property HouseStatusFilter_ShouldWorkWithOrLogic()
{
var houseStatusArb = Gen.Choose(1, 6);
var houseStatus1Arb = Gen.Choose(1, 3);
var houseStatus2Arb = Gen.Choose(4, 6);
return Prop.ForAll(
houseStatusArb.ToArbitrary(),
houseStatus1Arb.ToArbitrary(),
houseStatus2Arb.ToArbitrary(),
(profileHouseStatus, status1, status2) =>
{
var searchHouseStatuses = new List<int> { status1, status2 };
var profile = CreateBasicProfile(1990);
profile.HouseStatus = profileHouseStatus;
var request = new SearchRequest { HouseStatus = searchHouseStatuses };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 验证匹配当且仅当资料的房产状态在搜索列表中OR逻辑
var expectedMatch = searchHouseStatuses.Contains(profileHouseStatus);
return matches == expectedMatch;
});
}
/// <summary>
/// 车辆情况过滤应正确工作OR逻辑
/// </summary>
[Property(MaxTest = 100)]
public Property CarStatusFilter_ShouldWorkWithOrLogic()
{
var carStatusArb = Gen.Choose(1, 3);
var carStatus1Arb = Gen.Choose(1, 2);
var carStatus2Arb = Gen.Choose(2, 3);
return Prop.ForAll(
carStatusArb.ToArbitrary(),
carStatus1Arb.ToArbitrary(),
carStatus2Arb.ToArbitrary(),
(profileCarStatus, status1, status2) =>
{
var searchCarStatuses = new List<int> { status1, status2 }.Distinct().ToList();
var profile = CreateBasicProfile(1990);
profile.CarStatus = profileCarStatus;
var request = new SearchRequest { CarStatus = searchCarStatuses };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 验证匹配当且仅当资料的车辆状态在搜索列表中OR逻辑
var expectedMatch = searchCarStatuses.Contains(profileCarStatus);
return matches == expectedMatch;
});
}
/// <summary>
/// 婚姻状态过滤应正确工作OR逻辑
/// </summary>
[Property(MaxTest = 100)]
public Property MarriageStatusFilter_ShouldWorkWithOrLogic()
{
var marriageStatusArb = Gen.Choose(1, 3);
var marriageStatus1Arb = Gen.Choose(1, 2);
var marriageStatus2Arb = Gen.Choose(2, 3);
return Prop.ForAll(
marriageStatusArb.ToArbitrary(),
marriageStatus1Arb.ToArbitrary(),
marriageStatus2Arb.ToArbitrary(),
(profileMarriageStatus, status1, status2) =>
{
var searchMarriageStatuses = new List<int> { status1, status2 }.Distinct().ToList();
var profile = CreateBasicProfile(1990);
profile.MarriageStatus = profileMarriageStatus;
var request = new SearchRequest { MarriageStatus = searchMarriageStatuses };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 验证匹配当且仅当资料的婚姻状态在搜索列表中OR逻辑
var expectedMatch = searchMarriageStatuses.Contains(profileMarriageStatus);
return matches == expectedMatch;
});
}
/// <summary>
/// 空搜索条件应匹配所有用户
/// </summary>
[Property(MaxTest = 100)]
public Property EmptySearchCriteria_ShouldMatchAllUsers()
{
var birthYearArb = Gen.Choose(1970, 2005);
return Prop.ForAll(
birthYearArb.ToArbitrary(),
birthYear =>
{
var profile = CreateBasicProfile(birthYear);
var request = new SearchRequest(); // 空搜索条件
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 空条件应匹配所有用户
return matches;
});
}
/// <summary>
/// 诚意会员每日搜索次数限制应为5
/// </summary>
[Fact]
public void SincereMemberDailySearchLimit_ShouldBe5()
{
Assert.Equal(5, SearchService.SincereMemberDailySearchLimit);
}
#region Helper Methods
private static UserProfile CreateBasicProfile(int birthYear, int height = 175, int education = 4, int income = 5)
{
return new UserProfile
{
Id = 1,
UserId = 1,
BirthYear = birthYear,
Height = height,
Education = education,
MonthlyIncome = income,
HouseStatus = 1,
CarStatus = 1,
MarriageStatus = 1,
WorkCity = "北京",
WorkProvince = "北京",
ChildGender = 1,
Relationship = 1,
Surname = "张",
Occupation = "工程师",
Weight = 70,
ExpectMarryTime = 1,
Introduction = "测试",
IsPhotoPublic = true,
WeChatNo = "test123",
AuditStatus = (int)AuditStatus.Approved
};
}
#endregion
}
/// <summary>
/// SearchService属性测试 - 多选学历OR逻辑
/// </summary>
public class EducationOrLogicPropertyTests
{
/// <summary>
/// **Feature: backend-api, Property 13: 多选学历OR逻辑**
/// **Validates: Requirements 5.4**
///
/// *For any* 包含多个学历条件的搜索, 返回的用户应满足其中任一学历条件
/// </summary>
[Property(MaxTest = 100)]
public Property EducationFilter_ShouldUseOrLogic()
{
// 生成用户的学历1-6
var profileEducationArb = Gen.Choose(1, 6);
// 生成两个不同的学历用于搜索
var edu1Arb = Gen.Choose(1, 3);
var edu2Arb = Gen.Choose(4, 6);
return Prop.ForAll(
profileEducationArb.ToArbitrary(),
edu1Arb.ToArbitrary(),
edu2Arb.ToArbitrary(),
(profileEducation, edu1, edu2) =>
{
// Arrange
var searchEducations = new List<int> { edu1, edu2 };
var profile = CreateBasicProfile(profileEducation);
var request = new SearchRequest { Education = searchEducations };
// Act
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// Assert - 匹配当且仅当用户学历在搜索列表中OR逻辑
var expectedMatch = searchEducations.Contains(profileEducation);
return matches == expectedMatch;
});
}
/// <summary>
/// 单个学历条件应正确匹配
/// </summary>
[Property(MaxTest = 100)]
public Property SingleEducation_ShouldMatchExactly()
{
var educationArb = Gen.Choose(1, 6);
return Prop.ForAll(
educationArb.ToArbitrary(),
educationArb.ToArbitrary(),
(profileEducation, searchEducation) =>
{
var profile = CreateBasicProfile(profileEducation);
var request = new SearchRequest { Education = new List<int> { searchEducation } };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 单个学历条件:匹配当且仅当学历相同
var expectedMatch = profileEducation == searchEducation;
return matches == expectedMatch;
});
}
/// <summary>
/// 多个学历条件应使用OR逻辑 - 三个学历
/// </summary>
[Property(MaxTest = 100)]
public Property ThreeEducations_ShouldMatchAny()
{
var profileEducationArb = Gen.Choose(1, 6);
var edu1Arb = Gen.Choose(1, 2);
var edu2Arb = Gen.Choose(3, 4);
var edu3Arb = Gen.Choose(5, 6);
return Prop.ForAll(
profileEducationArb.ToArbitrary(),
edu1Arb.ToArbitrary(),
edu2Arb.ToArbitrary(),
(profileEducation, edu1, edu2) =>
{
var searchEducations = new List<int> { edu1, edu2, 5 }; // 固定第三个为5
var profile = CreateBasicProfile(profileEducation);
var request = new SearchRequest { Education = searchEducations };
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 多个学历条件:匹配当且仅当用户学历在列表中任一
var expectedMatch = searchEducations.Contains(profileEducation);
return matches == expectedMatch;
});
}
/// <summary>
/// 空学历列表应匹配所有用户
/// </summary>
[Property(MaxTest = 100)]
public Property EmptyEducationList_ShouldMatchAll()
{
var profileEducationArb = Gen.Choose(1, 6);
return Prop.ForAll(
profileEducationArb.ToArbitrary(),
profileEducation =>
{
var profile = CreateBasicProfile(profileEducation);
var request = new SearchRequest { Education = new List<int>() }; // 空列表
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 空学历列表应匹配所有用户
return matches;
});
}
/// <summary>
/// null学历条件应匹配所有用户
/// </summary>
[Property(MaxTest = 100)]
public Property NullEducation_ShouldMatchAll()
{
var profileEducationArb = Gen.Choose(1, 6);
return Prop.ForAll(
profileEducationArb.ToArbitrary(),
profileEducation =>
{
var profile = CreateBasicProfile(profileEducation);
var request = new SearchRequest { Education = null }; // null
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// null学历条件应匹配所有用户
return matches;
});
}
/// <summary>
/// 包含所有学历的列表应匹配所有用户
/// </summary>
[Property(MaxTest = 100)]
public Property AllEducations_ShouldMatchAll()
{
var profileEducationArb = Gen.Choose(1, 6);
return Prop.ForAll(
profileEducationArb.ToArbitrary(),
profileEducation =>
{
var profile = CreateBasicProfile(profileEducation);
var request = new SearchRequest
{
Education = new List<int> { 1, 2, 3, 4, 5, 6 } // 所有学历
};
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
// 包含所有学历的列表应匹配所有用户
return matches;
});
}
/// <summary>
/// 学历枚举值验证
/// </summary>
[Fact]
public void EducationEnum_ShouldHaveCorrectValues()
{
Assert.Equal(0, (int)Education.None);
Assert.Equal(1, (int)Education.HighSchool);
Assert.Equal(2, (int)Education.Technical);
Assert.Equal(3, (int)Education.College);
Assert.Equal(4, (int)Education.Bachelor);
Assert.Equal(5, (int)Education.Master);
Assert.Equal(6, (int)Education.Doctor);
}
#region Helper Methods
private static UserProfile CreateBasicProfile(int education)
{
return new UserProfile
{
Id = 1,
UserId = 1,
BirthYear = 1990,
Height = 175,
Education = education,
MonthlyIncome = 5,
HouseStatus = 1,
CarStatus = 1,
MarriageStatus = 1,
WorkCity = "北京",
WorkProvince = "北京",
ChildGender = 1,
Relationship = 1,
Surname = "张",
Occupation = "工程师",
Weight = 70,
ExpectMarryTime = 1,
Introduction = "测试",
IsPhotoPublic = true,
WeChatNo = "test123",
AuditStatus = (int)AuditStatus.Approved
};
}
#endregion
}