using System.ComponentModel.DataAnnotations; using MAX.Framework.Validation; using Store.Users.Contracts.Dtos; using Store.Users.Dtos; namespace Store.Users.Tests; /// 验证公共工具,以及 User DTO 仅保留 Required 的模块约定。 public sealed class RequestValidatorTests { [Fact] public void NullRequest_IsRejectedByBothEntrypoints() { Assert.Equal("request", Assert.Throws(() => RequestValidator.Validate(null)).ParamName); Assert.Throws(() => RequestValidator.TryValidate(null, out _)); } [Fact] public void ValidDto_PassesWithoutChangingInput() { var request = new UserCreateRequest { UserName = " main ", Password = "password", Notes = "" }; RequestValidator.Validate(request); Assert.True(RequestValidator.TryValidate(request, out var errors)); Assert.Empty(errors); Assert.Equal(" main ", request.UserName); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void RequiredFields_RejectNullEmptyAndWhitespace(string? value) { var request = new UserFundsRequest { UserName = value!, RequestId = "id", Amount = 1m, Operation = "充值" }; var error = Assert.Throws(() => RequestValidator.Validate(request)); Assert.Equal("目标账户不能为空", error.Message); Assert.Contains(nameof(UserFundsRequest.UserName), error.ValidationResult!.MemberNames); } [Fact] public void TryValidate_ReturnsAllMissingRequiredFields() { Assert.False(RequestValidator.TryValidate(new UserFundsRequest(), out var errors)); Assert.Equal(3, errors.Count); Assert.Contains(errors, e => e.MemberNames.Contains(nameof(UserFundsRequest.UserName))); Assert.Contains(errors, e => e.MemberNames.Contains(nameof(UserFundsRequest.RequestId))); Assert.Contains(errors, e => e.MemberNames.Contains(nameof(UserFundsRequest.Operation))); } [Theory] [InlineData(typeof(UserFundsRequest))] [InlineData(typeof(UserEditRequest))] [InlineData(typeof(UserCreateRequest))] [InlineData(typeof(ChangePasswordRequest))] [InlineData(typeof(UserSearchRequest))] [InlineData(typeof(UserRecordSearchRequest))] [InlineData(typeof(UserTopUpMessageRequest))] [InlineData(typeof(UserTopUpResultRequest))] [InlineData(typeof(UserTopUpQrcodeRequest))] [InlineData(typeof(UserNameRequest))] [InlineData(typeof(UserLoginRequest))] [InlineData(typeof(UserTopUpQrcodeQuery))] [InlineData(typeof(ShopRenewRequest))] public void UserDtos_DeclareOnlyRequiredValidation(Type type) { Assert.False(typeof(IValidatableObject).IsAssignableFrom(type)); foreach (var property in type.GetProperties()) Assert.All(property.GetCustomAttributes(typeof(ValidationAttribute), true), attribute => Assert.IsType(attribute)); } [Fact] public void UserDto_DoesNotCheckNonEmptyParameterValues() { // 这些值非空但不合法:DTO 应通过,服务负责拒绝。 RequestValidator.Validate(new UserFundsRequest { UserName = new string('u', 21), RequestId = "id", Amount = -1, Operation = "非法操作" }); RequestValidator.Validate(new UserTopUpQrcodeRequest { AgentCode = "agent", QrcodeBase64 = "invalid!" }); RequestValidator.Validate(new UserRecordSearchRequest { Page = 0, StartTime = new(2025, 2, 1), EndTime = new(2025, 1, 1) }); RequestValidator.Validate(new ShopRenewRequest { ShopId = "shop", RenewDuration = 0 }); RequestValidator.Validate(new UserCreateRequest { UserName = "user", Password = "123", ShopPointsPerMonth = 1 }); } [Fact] public void FrameworkValidator_StillSupportsOtherModulesAnnotationsAndCrossFieldRules() { // User 的约定不限制公共工具支持标准 DataAnnotations 的能力。 Assert.Throws(() => RequestValidator.Validate(new OtherModuleRequest { Name = "toolong" })); Assert.False(RequestValidator.TryValidate(new OtherModuleRequest { Name = "ok", Count = 0 }, out _)); var request = new OtherModuleRequest { Name = "ok", Count = 2, Start = 2, End = 1 }; Assert.Equal("顺序错误", Assert.Throws(() => RequestValidator.Validate(request)).Message); } private sealed class OtherModuleRequest : IValidatableObject { [Required, StringLength(4)] public string Name { get; set; } = ""; [Range(1, 10)] public int Count { get; set; } = 1; public int Start { get; set; } public int End { get; set; } public IEnumerable Validate(ValidationContext context) { if (Start > End) yield return new ValidationResult("顺序错误"); } } }