Files
StoreWeb/Tests/Store.Users.Tests/RequestValidatorTests.cs
2026-10-29 11:34:35 +08:00

95 lines
4.3 KiB
C#

using System.ComponentModel.DataAnnotations;
using MAX.Framework.Validation;
using Store.Users.Contracts.Dtos;
using Store.Users.Dtos;
namespace Store.Users.Tests;
/// <summary>验证服务入口使用的 DTO 注解和跨字段规则。</summary>
public sealed class RequestValidatorTests
{
[Fact]
public void NullRequest_IsRejectedByBothEntrypoints()
{
Assert.Equal("request", Assert.Throws<ArgumentNullException>(() => RequestValidator.Validate(null)).ParamName);
Assert.Throws<ArgumentNullException>(() => 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<ValidationException>(() => 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(4, errors.Count);
Assert.Contains(errors, e => e.MemberNames.Contains(nameof(UserFundsRequest.Amount)));
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)));
}
public static TheoryData<object> InvalidRequests => new()
{
new UserFundsRequest { UserName = new string('u', 21), RequestId = "id", Amount = 1, Operation = "充值" },
new UserFundsRequest { UserName = "user", RequestId = "id", Amount = -1, Operation = "充值" },
new UserFundsRequest { UserName = "user", RequestId = "id", Amount = 1.001m, Operation = "充值" },
new UserTopUpQrcodeRequest { AgentCode = "agent", QrcodeBase64 = "invalid!" },
new UserRecordSearchRequest { Page = 0 },
new UserRecordSearchRequest { StartTime = new(2025, 2, 1), EndTime = new(2025, 1, 1) },
new ShopRenewRequest { ShopId = "shop", RenewDuration = 0 },
new ShopRenewRequest { ShopId = "shop", RenewDuration = 121 },
new UserCreateRequest { UserName = "user", Password = "123" },
new UserCreateRequest { UserName = "user", Password = "password", ShopPointsPerMonth = 1 }
};
[Theory]
[MemberData(nameof(InvalidRequests))]
public void InvalidDto_IsRejectedAtValidationEntry(object request)
{
Assert.Throws<ValidationException>(() => RequestValidator.Validate(request));
Assert.False(RequestValidator.TryValidate(request, out var errors));
Assert.NotEmpty(errors);
}
[Fact]
public void FrameworkValidator_StillSupportsOtherModulesAnnotationsAndCrossFieldRules()
{
Assert.Throws<ValidationException>(() => 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<ValidationException>(() => 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<ValidationResult> Validate(ValidationContext context)
{
if (Start > End) yield return new ValidationResult("顺序错误");
}
}
}