using FsCheck; using FsCheck.Xunit; using Microsoft.Extensions.Logging; using MiAssessment.Core.Interfaces; using MiAssessment.Core.Services; using MiAssessment.Model.Models.System; using Moq; using Xunit; namespace MiAssessment.Tests.Services; /// /// SystemService 属性测试 /// 验证系统配置服务的响应格式一致性 /// public class SystemServicePropertyTests { private readonly Mock> _mockLogger = new(); #region Property 8: 响应格式一致性 - GetAgreementAsync /// /// Property 8: GetAgreementAsync 返回包含 Content 属性的 AgreementDto /// *For any* GetAgreementAsync call, the returned AgreementDto SHALL have a Content property. /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool GetAgreementAsync_ReturnsAgreementDtoWithContentProperty(NonEmptyString content) { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("user_agreement")) .ReturnsAsync(content.Get); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAgreementAsync().GetAwaiter().GetResult(); // Assert: 验证返回的 AgreementDto 包含 Content 属性且值正确 return result != null && result.Content == content.Get; } /// /// Property 8: GetAgreementAsync 返回非空 DTO,即使配置值为空 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 16.1, 16.2, 16.3** /// [Fact] public void GetAgreementAsync_ReturnsNonNullDto_WhenConfigValueIsNull() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("user_agreement")) .ReturnsAsync((string?)null); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAgreementAsync().GetAwaiter().GetResult(); // Assert: 即使配置值为空,也应返回非空 DTO Assert.NotNull(result); Assert.NotNull(result.Content); Assert.Equal(string.Empty, result.Content); } /// /// Property 8: GetAgreementAsync 返回非空 DTO,即使配置值为空字符串 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 16.1, 16.2, 16.3** /// [Fact] public void GetAgreementAsync_ReturnsNonNullDto_WhenConfigValueIsEmpty() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("user_agreement")) .ReturnsAsync(string.Empty); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAgreementAsync().GetAwaiter().GetResult(); // Assert Assert.NotNull(result); Assert.NotNull(result.Content); Assert.Equal(string.Empty, result.Content); } /// /// Property 8: GetAgreementAsync 响应格式一致性 - Content 属性始终存在 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool GetAgreementAsync_ContentPropertyAlwaysExists(PositiveInt seed) { // Arrange: 使用不同的配置值 var configValues = new string?[] { null, "", "协议内容", "用户协议\n第一条...", $"Agreement_{seed.Get}" }; var configValue = configValues[seed.Get % configValues.Length]; var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("user_agreement")) .ReturnsAsync(configValue); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAgreementAsync().GetAwaiter().GetResult(); // Assert: Content 属性始终存在且不为 null return result != null && result.Content != null; } #endregion #region Property 8: 响应格式一致性 - GetPrivacyAsync /// /// Property 8: GetPrivacyAsync 返回包含 Content 属性的 PrivacyDto /// *For any* GetPrivacyAsync call, the returned PrivacyDto SHALL have a Content property. /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.2, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool GetPrivacyAsync_ReturnsPrivacyDtoWithContentProperty(NonEmptyString content) { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("privacy_policy")) .ReturnsAsync(content.Get); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetPrivacyAsync().GetAwaiter().GetResult(); // Assert: 验证返回的 PrivacyDto 包含 Content 属性且值正确 return result != null && result.Content == content.Get; } /// /// Property 8: GetPrivacyAsync 返回非空 DTO,即使配置值为空 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.2, 16.1, 16.2, 16.3** /// [Fact] public void GetPrivacyAsync_ReturnsNonNullDto_WhenConfigValueIsNull() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("privacy_policy")) .ReturnsAsync((string?)null); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetPrivacyAsync().GetAwaiter().GetResult(); // Assert: 即使配置值为空,也应返回非空 DTO Assert.NotNull(result); Assert.NotNull(result.Content); Assert.Equal(string.Empty, result.Content); } /// /// Property 8: GetPrivacyAsync 返回非空 DTO,即使配置值为空字符串 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.2, 16.1, 16.2, 16.3** /// [Fact] public void GetPrivacyAsync_ReturnsNonNullDto_WhenConfigValueIsEmpty() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("privacy_policy")) .ReturnsAsync(string.Empty); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetPrivacyAsync().GetAwaiter().GetResult(); // Assert Assert.NotNull(result); Assert.NotNull(result.Content); Assert.Equal(string.Empty, result.Content); } /// /// Property 8: GetPrivacyAsync 响应格式一致性 - Content 属性始终存在 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.2, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool GetPrivacyAsync_ContentPropertyAlwaysExists(PositiveInt seed) { // Arrange: 使用不同的配置值 var configValues = new string?[] { null, "", "隐私政策", "隐私政策\n第一条...", $"Privacy_{seed.Get}" }; var configValue = configValues[seed.Get % configValues.Length]; var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("privacy_policy")) .ReturnsAsync(configValue); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetPrivacyAsync().GetAwaiter().GetResult(); // Assert: Content 属性始终存在且不为 null return result != null && result.Content != null; } #endregion #region Property 8: 响应格式一致性 - GetAboutAsync /// /// Property 8: GetAboutAsync 返回包含 Content 和 Version 属性的 AboutDto /// *For any* GetAboutAsync call, the returned AboutDto SHALL have Content and Version properties. /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.3, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool GetAboutAsync_ReturnsAboutDtoWithContentAndVersionProperties(NonEmptyString content, NonEmptyString version) { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync(content.Get); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync(version.Get); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: 验证返回的 AboutDto 包含 Content 和 Version 属性且值正确 return result != null && result.Content == content.Get && result.Version == version.Get; } /// /// Property 8: GetAboutAsync 返回非空 DTO,即使配置值为空 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.3, 16.1, 16.2, 16.3** /// [Fact] public void GetAboutAsync_ReturnsNonNullDto_WhenConfigValuesAreNull() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync((string?)null); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync((string?)null); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: 即使配置值为空,也应返回非空 DTO Assert.NotNull(result); Assert.NotNull(result.Content); Assert.NotNull(result.Version); Assert.Equal(string.Empty, result.Content); } /// /// Property 8: GetAboutAsync Version 有默认值,当未配置时 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.3, 16.1, 16.2, 16.3** /// [Fact] public void GetAboutAsync_VersionHasDefaultValue_WhenNotConfigured() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync("关于我们内容"); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync((string?)null); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: Version 应该有默认值 "1.0.0" Assert.NotNull(result); Assert.Equal("1.0.0", result.Version); } /// /// Property 8: GetAboutAsync Version 有默认值,当配置为空字符串时 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.3, 16.1, 16.2, 16.3** /// [Fact] public void GetAboutAsync_VersionHasDefaultValue_WhenConfiguredAsEmpty() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync("关于我们内容"); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync(string.Empty); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: 当配置为空字符串时,应该返回空字符串(不是默认值) // 因为 SystemService 只在 null 时使用默认值 Assert.NotNull(result); Assert.Equal(string.Empty, result.Version); } /// /// Property 8: GetAboutAsync 响应格式一致性 - Content 和 Version 属性始终存在 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.3, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool GetAboutAsync_ContentAndVersionPropertiesAlwaysExist(PositiveInt seed) { // Arrange: 使用不同的配置值组合 var contentValues = new string?[] { null, "", "关于我们", "公司介绍\n...", $"About_{seed.Get}" }; var versionValues = new string?[] { null, "", "1.0.0", "2.1.3", $"v{seed.Get % 10}.{seed.Get % 100}.{seed.Get % 1000}" }; var contentValue = contentValues[seed.Get % contentValues.Length]; var versionValue = versionValues[(seed.Get / contentValues.Length) % versionValues.Length]; var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync(contentValue); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync(versionValue); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var result = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: Content 和 Version 属性始终存在且不为 null return result != null && result.Content != null && result.Version != null; } #endregion #region 综合属性测试 /// /// Property 8: 所有系统配置方法返回非空 DTO /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 14.2, 14.3, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool AllSystemMethods_ReturnNonNullDtos(PositiveInt seed) { // Arrange: 随机配置值 var configValues = new string?[] { null, "", $"Content_{seed.Get}" }; var agreementValue = configValues[seed.Get % configValues.Length]; var privacyValue = configValues[(seed.Get + 1) % configValues.Length]; var aboutValue = configValues[(seed.Get + 2) % configValues.Length]; var versionValue = configValues[(seed.Get + 3) % configValues.Length]; var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("user_agreement")) .ReturnsAsync(agreementValue); mockConfigService .Setup(x => x.GetConfigValueAsync("privacy_policy")) .ReturnsAsync(privacyValue); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync(aboutValue); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync(versionValue); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var agreement = service.GetAgreementAsync().GetAwaiter().GetResult(); var privacy = service.GetPrivacyAsync().GetAwaiter().GetResult(); var about = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: 所有方法都返回非空 DTO return agreement != null && privacy != null && about != null; } /// /// Property 8: 所有系统配置方法的响应属性不为 null /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 14.2, 14.3, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool AllSystemMethods_ResponsePropertiesAreNotNull(PositiveInt seed) { // Arrange: 随机配置值 var configValues = new string?[] { null, "", $"Content_{seed.Get}" }; var agreementValue = configValues[seed.Get % configValues.Length]; var privacyValue = configValues[(seed.Get + 1) % configValues.Length]; var aboutValue = configValues[(seed.Get + 2) % configValues.Length]; var versionValue = configValues[(seed.Get + 3) % configValues.Length]; var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("user_agreement")) .ReturnsAsync(agreementValue); mockConfigService .Setup(x => x.GetConfigValueAsync("privacy_policy")) .ReturnsAsync(privacyValue); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync(aboutValue); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync(versionValue); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var agreement = service.GetAgreementAsync().GetAwaiter().GetResult(); var privacy = service.GetPrivacyAsync().GetAwaiter().GetResult(); var about = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: 所有响应属性都不为 null return agreement.Content != null && privacy.Content != null && about.Content != null && about.Version != null; } /// /// Property 8: 配置值正确传递到响应中 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 14.2, 14.3, 16.1, 16.2, 16.3** /// [Property(MaxTest = 100)] public bool ConfigValues_CorrectlyPassedToResponse(NonEmptyString agreementContent, NonEmptyString privacyContent, NonEmptyString aboutContent, NonEmptyString version) { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync("user_agreement")) .ReturnsAsync(agreementContent.Get); mockConfigService .Setup(x => x.GetConfigValueAsync("privacy_policy")) .ReturnsAsync(privacyContent.Get); mockConfigService .Setup(x => x.GetConfigValueAsync("about_us")) .ReturnsAsync(aboutContent.Get); mockConfigService .Setup(x => x.GetConfigValueAsync("app_version")) .ReturnsAsync(version.Get); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var agreement = service.GetAgreementAsync().GetAwaiter().GetResult(); var privacy = service.GetPrivacyAsync().GetAwaiter().GetResult(); var about = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert: 配置值正确传递到响应中 return agreement.Content == agreementContent.Get && privacy.Content == privacyContent.Get && about.Content == aboutContent.Get && about.Version == version.Get; } /// /// Property 8: 空配置值时使用默认值或空字符串 /// /// **Feature: miniapp-api, Property 8: 响应格式一致性** /// **Validates: Requirements 14.1, 14.2, 14.3, 16.1, 16.2, 16.3** /// [Fact] public void NullConfigValues_UseDefaultOrEmptyString() { // Arrange var mockConfigService = new Mock(); mockConfigService .Setup(x => x.GetConfigValueAsync(It.IsAny())) .ReturnsAsync((string?)null); var service = new SystemService(mockConfigService.Object, _mockLogger.Object); // Act var agreement = service.GetAgreementAsync().GetAwaiter().GetResult(); var privacy = service.GetPrivacyAsync().GetAwaiter().GetResult(); var about = service.GetAboutAsync().GetAwaiter().GetResult(); // Assert Assert.Equal(string.Empty, agreement.Content); Assert.Equal(string.Empty, privacy.Content); Assert.Equal(string.Empty, about.Content); Assert.Equal("1.0.0", about.Version); // Version 有默认值 } #endregion }