551 lines
19 KiB
C#
551 lines
19 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Xunit;
|
||
using XiangYi.Application.Services;
|
||
using XiangYi.Core.Entities.Biz;
|
||
|
||
namespace XiangYi.Application.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// InteractService属性测试 - 浏览记录一致性
|
||
/// </summary>
|
||
public class ViewRecordPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 7: 浏览记录一致性**
|
||
/// **Validates: Requirements 4.1**
|
||
///
|
||
/// *For any* 用户查看他人资料操作, 应创建或更新浏览记录,且被浏览者的"看过我"列表应包含该浏览者
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ViewRecord_ShouldBeConsistent()
|
||
{
|
||
// 生成浏览者ID(1-500)
|
||
var viewerIdArb = Gen.Choose(1, 500);
|
||
// 生成被浏览者ID(501-1000)
|
||
var targetUserIdArb = Gen.Choose(501, 1000);
|
||
|
||
return Prop.ForAll(
|
||
viewerIdArb.ToArbitrary(),
|
||
targetUserIdArb.ToArbitrary(),
|
||
(viewerId, targetUserId) =>
|
||
{
|
||
// Arrange - 创建空的浏览记录列表
|
||
var viewRecords = new List<UserView>();
|
||
|
||
// Act - 模拟添加浏览记录
|
||
var newRecord = new UserView
|
||
{
|
||
UserId = viewerId,
|
||
TargetUserId = targetUserId,
|
||
ViewDate = DateTime.Today,
|
||
ViewCount = 1,
|
||
LastViewTime = DateTime.Now,
|
||
CreateTime = DateTime.Now
|
||
};
|
||
viewRecords.Add(newRecord);
|
||
|
||
// Assert - 验证浏览记录存在
|
||
var viewerExists = InteractService.IsViewRecordExistsStatic(viewRecords, viewerId, targetUserId);
|
||
|
||
// 验证被浏览者的"看过我"列表包含浏览者
|
||
var viewedMeList = viewRecords.Where(v => v.TargetUserId == targetUserId).ToList();
|
||
var viewerInViewedMeList = viewedMeList.Any(v => v.UserId == viewerId);
|
||
|
||
return viewerExists && viewerInViewedMeList;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 浏览记录 - 同一用户多次浏览应更新次数而非创建新记录
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ViewRecord_MultipleViews_ShouldUpdateCount()
|
||
{
|
||
var viewerIdArb = Gen.Choose(1, 500);
|
||
var targetUserIdArb = Gen.Choose(501, 1000);
|
||
var viewCountArb = Gen.Choose(1, 10);
|
||
|
||
return Prop.ForAll(
|
||
viewerIdArb.ToArbitrary(),
|
||
targetUserIdArb.ToArbitrary(),
|
||
viewCountArb.ToArbitrary(),
|
||
(viewerId, targetUserId, viewCount) =>
|
||
{
|
||
// Arrange - 创建初始浏览记录
|
||
var viewRecords = new List<UserView>
|
||
{
|
||
new UserView
|
||
{
|
||
UserId = viewerId,
|
||
TargetUserId = targetUserId,
|
||
ViewDate = DateTime.Today,
|
||
ViewCount = viewCount,
|
||
LastViewTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
// Act - 模拟再次浏览(更新次数)
|
||
var existingRecord = viewRecords.FirstOrDefault(v =>
|
||
v.UserId == viewerId && v.TargetUserId == targetUserId && v.ViewDate == DateTime.Today);
|
||
|
||
if (existingRecord != null)
|
||
{
|
||
existingRecord.ViewCount++;
|
||
}
|
||
|
||
// Assert - 验证次数增加且记录数不变
|
||
var recordCount = viewRecords.Count(v => v.UserId == viewerId && v.TargetUserId == targetUserId);
|
||
var updatedCount = viewRecords.First(v => v.UserId == viewerId && v.TargetUserId == targetUserId).ViewCount;
|
||
|
||
return recordCount == 1 && updatedCount == viewCount + 1;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 浏览记录 - 不同日期应创建新记录
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ViewRecord_DifferentDays_ShouldCreateNewRecord()
|
||
{
|
||
var viewerIdArb = Gen.Choose(1, 500);
|
||
var targetUserIdArb = Gen.Choose(501, 1000);
|
||
|
||
return Prop.ForAll(
|
||
viewerIdArb.ToArbitrary(),
|
||
targetUserIdArb.ToArbitrary(),
|
||
(viewerId, targetUserId) =>
|
||
{
|
||
// Arrange - 创建昨天的浏览记录
|
||
var viewRecords = new List<UserView>
|
||
{
|
||
new UserView
|
||
{
|
||
UserId = viewerId,
|
||
TargetUserId = targetUserId,
|
||
ViewDate = DateTime.Today.AddDays(-1),
|
||
ViewCount = 1,
|
||
LastViewTime = DateTime.Now.AddDays(-1)
|
||
}
|
||
};
|
||
|
||
// Act - 今天再次浏览,应创建新记录
|
||
var todayRecord = viewRecords.FirstOrDefault(v =>
|
||
v.UserId == viewerId && v.TargetUserId == targetUserId && v.ViewDate == DateTime.Today);
|
||
|
||
if (todayRecord == null)
|
||
{
|
||
viewRecords.Add(new UserView
|
||
{
|
||
UserId = viewerId,
|
||
TargetUserId = targetUserId,
|
||
ViewDate = DateTime.Today,
|
||
ViewCount = 1,
|
||
LastViewTime = DateTime.Now
|
||
});
|
||
}
|
||
|
||
// Assert - 应该有两条记录(昨天和今天)
|
||
var totalRecords = viewRecords.Count(v => v.UserId == viewerId && v.TargetUserId == targetUserId);
|
||
return totalRecords == 2;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 浏览记录 - 浏览者和被浏览者不能是同一人(业务规则验证)
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ViewRecord_SelfView_ShouldBeInvalid()
|
||
{
|
||
var userIdArb = Gen.Choose(1, 1000);
|
||
|
||
return Prop.ForAll(
|
||
userIdArb.ToArbitrary(),
|
||
userId =>
|
||
{
|
||
// 验证业务规则:自己浏览自己应该是无效的
|
||
// 这里验证的是:当viewerId == targetUserId时,应该被拒绝
|
||
var viewerId = userId;
|
||
var targetUserId = userId;
|
||
var isSelfView = viewerId == targetUserId;
|
||
|
||
// 自己浏览自己应该返回true(表示是无效的自我浏览)
|
||
return isSelfView;
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// InteractService属性测试 - 收藏操作幂等性
|
||
/// </summary>
|
||
public class FavoriteIdempotencyPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 8: 收藏操作幂等性**
|
||
/// **Validates: Requirements 4.2**
|
||
///
|
||
/// *For any* 用户收藏操作, 重复收藏同一用户不应创建重复记录
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Favorite_ShouldBeIdempotent()
|
||
{
|
||
var userIdArb = Gen.Choose(1, 500);
|
||
var targetUserIdArb = Gen.Choose(501, 1000);
|
||
|
||
return Prop.ForAll(
|
||
userIdArb.ToArbitrary(),
|
||
targetUserIdArb.ToArbitrary(),
|
||
(userId, targetUserId) =>
|
||
{
|
||
// Arrange - 创建已有收藏记录
|
||
var favorites = new List<UserFavorite>
|
||
{
|
||
new UserFavorite
|
||
{
|
||
UserId = userId,
|
||
TargetUserId = targetUserId,
|
||
CreateTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
// Act - 计算再次收藏后的记录数
|
||
var countAfterOperation = InteractService.CalculateFavoriteCountAfterOperation(
|
||
favorites, userId, targetUserId);
|
||
|
||
// Assert - 记录数应该保持不变(幂等性)
|
||
return countAfterOperation == favorites.Count;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收藏 - 首次收藏应增加记录
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Favorite_FirstTime_ShouldAddRecord()
|
||
{
|
||
var userIdArb = Gen.Choose(1, 500);
|
||
var targetUserIdArb = Gen.Choose(501, 1000);
|
||
|
||
return Prop.ForAll(
|
||
userIdArb.ToArbitrary(),
|
||
targetUserIdArb.ToArbitrary(),
|
||
(userId, targetUserId) =>
|
||
{
|
||
// Arrange - 空收藏列表
|
||
var favorites = new List<UserFavorite>();
|
||
|
||
// Act - 计算首次收藏后的记录数
|
||
var countAfterOperation = InteractService.CalculateFavoriteCountAfterOperation(
|
||
favorites, userId, targetUserId);
|
||
|
||
// Assert - 记录数应该增加1
|
||
return countAfterOperation == 1;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收藏 - 收藏不同用户应创建不同记录
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Favorite_DifferentTargets_ShouldCreateSeparateRecords()
|
||
{
|
||
var userIdArb = Gen.Choose(1, 500);
|
||
var targetUserId1Arb = Gen.Choose(501, 750);
|
||
var targetUserId2Arb = Gen.Choose(751, 1000);
|
||
|
||
return Prop.ForAll(
|
||
userIdArb.ToArbitrary(),
|
||
targetUserId1Arb.ToArbitrary(),
|
||
targetUserId2Arb.ToArbitrary(),
|
||
(userId, targetUserId1, targetUserId2) =>
|
||
{
|
||
// Arrange - 已收藏第一个用户
|
||
var favorites = new List<UserFavorite>
|
||
{
|
||
new UserFavorite
|
||
{
|
||
UserId = userId,
|
||
TargetUserId = targetUserId1,
|
||
CreateTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
// Act - 收藏第二个用户
|
||
var countAfterOperation = InteractService.CalculateFavoriteCountAfterOperation(
|
||
favorites, userId, targetUserId2);
|
||
|
||
// Assert - 记录数应该增加1
|
||
return countAfterOperation == 2;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收藏 - 检查收藏是否存在
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Favorite_Exists_ShouldReturnCorrectResult()
|
||
{
|
||
var userIdArb = Gen.Choose(1, 500);
|
||
var targetUserIdArb = Gen.Choose(501, 1000);
|
||
|
||
return Prop.ForAll(
|
||
userIdArb.ToArbitrary(),
|
||
targetUserIdArb.ToArbitrary(),
|
||
(userId, targetUserId) =>
|
||
{
|
||
// Arrange - 创建收藏记录
|
||
var favorites = new List<UserFavorite>
|
||
{
|
||
new UserFavorite
|
||
{
|
||
UserId = userId,
|
||
TargetUserId = targetUserId,
|
||
CreateTime = DateTime.Now
|
||
}
|
||
};
|
||
|
||
// Act
|
||
var exists = InteractService.IsFavoriteExistsStatic(favorites, userId, targetUserId);
|
||
var notExists = InteractService.IsFavoriteExistsStatic(favorites, userId, targetUserId + 1);
|
||
|
||
// Assert
|
||
return exists && !notExists;
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// InteractService属性测试 - 解锁联系次数扣减
|
||
/// </summary>
|
||
public class UnlockContactCountPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 9: 解锁联系次数扣减**
|
||
/// **Validates: Requirements 4.3**
|
||
///
|
||
/// *For any* 非会员用户解锁操作, 解锁成功后联系次数应减少1,且应创建聊天会话
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_NonMember_ShouldDeductContactCount()
|
||
{
|
||
// 生成有效的联系次数(1-10)
|
||
var contactCountArb = Gen.Choose(1, 10);
|
||
|
||
return Prop.ForAll(
|
||
contactCountArb.ToArbitrary(),
|
||
contactCount =>
|
||
{
|
||
// Arrange
|
||
var isMember = false;
|
||
var isAlreadyUnlocked = false;
|
||
|
||
// Act
|
||
var countAfterUnlock = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember, isAlreadyUnlocked);
|
||
|
||
// Assert - 联系次数应减少1
|
||
return countAfterUnlock == contactCount - 1;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解锁 - 非会员解锁后次数应正好减少1
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_NonMember_ShouldDeductExactlyOne()
|
||
{
|
||
var contactCountArb = Gen.Choose(1, 100);
|
||
|
||
return Prop.ForAll(
|
||
contactCountArb.ToArbitrary(),
|
||
contactCount =>
|
||
{
|
||
var countAfterUnlock = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember: false, isAlreadyUnlocked: false);
|
||
|
||
// 验证扣减正好是1
|
||
return contactCount - countAfterUnlock == 1;
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// InteractService属性测试 - 联系次数为0时拒绝解锁
|
||
/// </summary>
|
||
public class UnlockZeroContactCountPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 10: 联系次数为0时拒绝解锁**
|
||
/// **Validates: Requirements 4.4**
|
||
///
|
||
/// *For any* 联系次数为0的非会员用户, 解锁操作应被拒绝并返回错误提示
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_ZeroContactCount_ShouldBeRejected()
|
||
{
|
||
return Prop.ForAll(
|
||
Arb.Default.PositiveInt(),
|
||
_ =>
|
||
{
|
||
// Arrange
|
||
var contactCount = 0;
|
||
var isMember = false;
|
||
var isAlreadyUnlocked = false;
|
||
|
||
// Act
|
||
var shouldReject = InteractService.ShouldRejectUnlock(
|
||
contactCount, isMember, isAlreadyUnlocked);
|
||
var countAfterUnlock = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember, isAlreadyUnlocked);
|
||
|
||
// Assert - 应该拒绝解锁,返回-1表示无法解锁
|
||
return shouldReject && countAfterUnlock == -1;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解锁 - 负数联系次数也应该被拒绝
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_NegativeContactCount_ShouldBeRejected()
|
||
{
|
||
var negativeCountArb = Gen.Choose(-100, -1);
|
||
|
||
return Prop.ForAll(
|
||
negativeCountArb.ToArbitrary(),
|
||
negativeCount =>
|
||
{
|
||
var shouldReject = InteractService.ShouldRejectUnlock(
|
||
negativeCount, isMember: false, isAlreadyUnlocked: false);
|
||
var countAfterUnlock = InteractService.CalculateContactCountAfterUnlock(
|
||
negativeCount, isMember: false, isAlreadyUnlocked: false);
|
||
|
||
return shouldReject && countAfterUnlock == -1;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解锁 - 已解锁的用户应该被拒绝
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_AlreadyUnlocked_ShouldBeRejected()
|
||
{
|
||
var contactCountArb = Gen.Choose(0, 10);
|
||
var isMemberArb = Gen.Elements(true, false);
|
||
|
||
return Prop.ForAll(
|
||
contactCountArb.ToArbitrary(),
|
||
isMemberArb.ToArbitrary(),
|
||
(contactCount, isMember) =>
|
||
{
|
||
// 已解锁的情况
|
||
var shouldReject = InteractService.ShouldRejectUnlock(
|
||
contactCount, isMember, isAlreadyUnlocked: true);
|
||
|
||
return shouldReject;
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// InteractService属性测试 - 会员解锁不扣次数
|
||
/// </summary>
|
||
public class MemberUnlockPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 11: 会员解锁不扣次数**
|
||
/// **Validates: Requirements 7.5**
|
||
///
|
||
/// *For any* 会员用户解锁操作, 解锁成功后联系次数应保持不变
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_Member_ShouldNotDeductContactCount()
|
||
{
|
||
var contactCountArb = Gen.Choose(0, 10);
|
||
|
||
return Prop.ForAll(
|
||
contactCountArb.ToArbitrary(),
|
||
contactCount =>
|
||
{
|
||
// Arrange
|
||
var isMember = true;
|
||
var isAlreadyUnlocked = false;
|
||
|
||
// Act
|
||
var countAfterUnlock = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember, isAlreadyUnlocked);
|
||
|
||
// Assert - 会员解锁后联系次数应保持不变
|
||
return countAfterUnlock == contactCount;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 会员解锁 - 即使联系次数为0也应该允许解锁
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_MemberWithZeroCount_ShouldBeAllowed()
|
||
{
|
||
return Prop.ForAll(
|
||
Arb.Default.PositiveInt(),
|
||
_ =>
|
||
{
|
||
// Arrange
|
||
var contactCount = 0;
|
||
var isMember = true;
|
||
var isAlreadyUnlocked = false;
|
||
|
||
// Act
|
||
var shouldReject = InteractService.ShouldRejectUnlock(
|
||
contactCount, isMember, isAlreadyUnlocked);
|
||
var countAfterUnlock = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember, isAlreadyUnlocked);
|
||
|
||
// Assert - 会员不应该被拒绝,次数保持为0
|
||
return !shouldReject && countAfterUnlock == 0;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 会员解锁 - 任意联系次数都应该保持不变
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_Member_ContactCountShouldRemainUnchanged()
|
||
{
|
||
var contactCountArb = Gen.Choose(-10, 100);
|
||
|
||
return Prop.ForAll(
|
||
contactCountArb.ToArbitrary(),
|
||
contactCount =>
|
||
{
|
||
var countAfterUnlock = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember: true, isAlreadyUnlocked: false);
|
||
|
||
// 会员解锁后次数应该完全不变
|
||
return countAfterUnlock == contactCount;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 会员 vs 非会员 - 会员解锁次数应大于等于非会员
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property Unlock_MemberVsNonMember_MemberShouldHaveMoreOrEqualCount()
|
||
{
|
||
var contactCountArb = Gen.Choose(1, 10);
|
||
|
||
return Prop.ForAll(
|
||
contactCountArb.ToArbitrary(),
|
||
contactCount =>
|
||
{
|
||
var memberCountAfter = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember: true, isAlreadyUnlocked: false);
|
||
var nonMemberCountAfter = InteractService.CalculateContactCountAfterUnlock(
|
||
contactCount, isMember: false, isAlreadyUnlocked: false);
|
||
|
||
// 会员解锁后的次数应该大于等于非会员
|
||
return memberCountAfter >= nonMemberCountAfter;
|
||
});
|
||
}
|
||
}
|