using FsCheck; using FsCheck.Xunit; using FreeSql; using LiveForum.IService.Others; using LiveForum.Model; using LiveForum.Service.Cdk; using Moq; using Xunit; namespace LiveForum.Tests { /// /// CDK服务属性测试 /// Feature: cdk-activation /// public class CdkServicePropertyTests { /// /// Property 2: CDK 激活成功更新状态 /// *For any* valid and unused CDK, when a user activates it, the system SHALL mark the CDK as used, /// record the user ID, and update the user's IsCdkActivated to true. /// **Validates: Requirements 2.3, 2.5** /// [Property(MaxTest = 100)] public bool CdkActivation_ShouldUpdateBothCdkAndUserStatus(PositiveInt userIdArb, NonEmptyString cdkCodeArb) { // Arrange var userId = (long)userIdArb.Get; var cdkCode = cdkCodeArb.Get; // Skip whitespace-only codes as they are handled by input validation, not CDK lookup if (string.IsNullOrWhiteSpace(cdkCode)) { return true; // Trivially true for invalid input - tested by Property 4 } var cdk = new T_CDKs { Id = 1, Code = cdkCode, Status = 0, // Unused UsedByUserId = null, UsedAt = null, CreatedAt = DateTime.Now }; var user = new T_Users { Id = userId, NickName = "TestUser", IsCdkActivated = false, CdkActivatedAt = null }; T_CDKs? updatedCdk = null; T_Users? updatedUser = null; // Mock CDK repository var cdkRepoMock = new Mock>(); var cdkSelectMock = new Mock>(); cdkSelectMock.Setup(s => s.Where(It.IsAny>>())) .Returns(cdkSelectMock.Object); cdkSelectMock.Setup(s => s.FirstAsync(default)) .ReturnsAsync(cdk); cdkRepoMock.Setup(r => r.Select).Returns(cdkSelectMock.Object); cdkRepoMock.Setup(r => r.UpdateAsync(It.IsAny(), default)) .Callback((c, _) => updatedCdk = c) .ReturnsAsync(1); // Mock User repository var userRepoMock = new Mock>(); var userSelectMock = new Mock>(); userSelectMock.Setup(s => s.Where(It.IsAny>>())) .Returns(userSelectMock.Object); userSelectMock.Setup(s => s.FirstAsync(default)) .ReturnsAsync(user); userRepoMock.Setup(r => r.Select).Returns(userSelectMock.Object); userRepoMock.Setup(r => r.UpdateAsync(It.IsAny(), default)) .Callback((u, _) => updatedUser = u) .ReturnsAsync(1); // Mock System Settings Service var settingsServiceMock = new Mock(); settingsServiceMock.Setup(s => s.GetSettingAsync(CdkService.CDK_ENABLED_KEY)) .ReturnsAsync("true"); var service = new CdkService(cdkRepoMock.Object, userRepoMock.Object, settingsServiceMock.Object); // Act var result = service.ActivateCdkAsync(userId, cdkCode).GetAwaiter().GetResult(); // Assert - Property: After successful activation, both CDK and User should be updated var cdkUpdated = updatedCdk != null && updatedCdk.Status == 1 && updatedCdk.UsedByUserId == userId && updatedCdk.UsedAt != null; var userUpdated = updatedUser != null && updatedUser.IsCdkActivated == true && updatedUser.CdkActivatedAt != null; var successMessage = result == "CDK 激活成功"; return cdkUpdated && userUpdated && successMessage; } /// /// Property 3: CDK 唯一性验证 /// *For any* CDK that has been used, attempting to activate it again SHALL return an error /// indicating the CDK has already been activated. /// **Validates: Requirements 2.2, 3.2** /// [Property(MaxTest = 100)] public bool UsedCdk_ShouldReturnAlreadyActivatedError(PositiveInt userIdArb, NonEmptyString cdkCodeArb) { // Arrange var userId = (long)userIdArb.Get; var cdkCode = cdkCodeArb.Get; // Skip whitespace-only codes as they are handled by input validation, not CDK lookup if (string.IsNullOrWhiteSpace(cdkCode)) { return true; // Trivially true for invalid input - tested by Property 4 } // Create a CDK that is already used (Status = 1) var usedCdk = new T_CDKs { Id = 1, Code = cdkCode, Status = 1, // Already used UsedByUserId = 999, // Used by another user UsedAt = DateTime.Now.AddDays(-1), CreatedAt = DateTime.Now.AddDays(-7) }; var user = new T_Users { Id = userId, NickName = "TestUser", IsCdkActivated = false, CdkActivatedAt = null }; T_CDKs? updatedCdk = null; T_Users? updatedUser = null; // Mock CDK repository var cdkRepoMock = new Mock>(); var cdkSelectMock = new Mock>(); cdkSelectMock.Setup(s => s.Where(It.IsAny>>())) .Returns(cdkSelectMock.Object); cdkSelectMock.Setup(s => s.FirstAsync(default)) .ReturnsAsync(usedCdk); cdkRepoMock.Setup(r => r.Select).Returns(cdkSelectMock.Object); cdkRepoMock.Setup(r => r.UpdateAsync(It.IsAny(), default)) .Callback((c, _) => updatedCdk = c) .ReturnsAsync(1); // Mock User repository var userRepoMock = new Mock>(); var userSelectMock = new Mock>(); userSelectMock.Setup(s => s.Where(It.IsAny>>())) .Returns(userSelectMock.Object); userSelectMock.Setup(s => s.FirstAsync(default)) .ReturnsAsync(user); userRepoMock.Setup(r => r.Select).Returns(userSelectMock.Object); userRepoMock.Setup(r => r.UpdateAsync(It.IsAny(), default)) .Callback((u, _) => updatedUser = u) .ReturnsAsync(1); // Mock System Settings Service var settingsServiceMock = new Mock(); settingsServiceMock.Setup(s => s.GetSettingAsync(CdkService.CDK_ENABLED_KEY)) .ReturnsAsync("true"); var service = new CdkService(cdkRepoMock.Object, userRepoMock.Object, settingsServiceMock.Object); // Act var result = service.ActivateCdkAsync(userId, cdkCode).GetAwaiter().GetResult(); // Assert - Property: Used CDK should return error and NOT update anything var errorMessageCorrect = result == "该 CDK 已被激活"; var cdkNotUpdated = updatedCdk == null; // CDK should not be updated var userNotUpdated = updatedUser == null; // User should not be updated return errorMessageCorrect && cdkNotUpdated && userNotUpdated; } } }