Files
StoreWeb/Tests/ShopRenewSmoke/Program.cs
T

211 lines
9.8 KiB
C#

using Store.Users.Behaviors;
using Store.Users.Entities;
using YourNamespace;
using System.Reflection;
using MAX.Framework.Mediation;
using MAX.Core.Mediation;
using System.ComponentModel.DataAnnotations;
using MAX.Core.DependencyInjection;
using MAX.Framework.DependencyInjection;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Store.Users.Commands;
using Store.Users.Contracts.Dtos;
// 使用实际 Startup 注册路径,替换数据库处理器以单独验证 MediatR 管道。
var services = new ServiceCollection();
var registrarType = typeof(ServiceCollectionExtensions).Assembly
.GetType("MAX.Framework.DependencyInjection.ServiceRegistrarProxy", throwOnError: true)!;
var registrar = (IServiceRegistrar)Activator.CreateInstance(registrarType, services)!;
new Store.Users.Startup().Register(registrar);
services.AddMediatRAttributedBehaviors(typeof(ShopRenewCommand).Assembly);
services.AddMediatRAttributedBehaviors(typeof(ShopRenewCommand).Assembly); // 重复扫描不重复注册
var pipelineType = typeof(IPipelineBehavior<ShopRenewCommand, ShopRenewResponse>);
var pipelines = services.Where(s => s.ServiceType == pipelineType).ToArray();
if (pipelines.Length != 4
|| pipelines[0].ImplementationType?.Name != "ValidationBehavior`2"
|| pipelines[1].ImplementationType?.Name != "TransactionBehavior`2"
|| pipelines[2].ImplementationType?.Name != "AccountConsumptionBehavior`2"
|| pipelines[3].ImplementationType?.Name != "ShopOwnershipBehavior`2")
throw new Exception("管道注册顺序错误,应为参数验证、事务、消费检查");
// 本测试只验证参数管道;事务与消费管道需在测试库中进行集成测试。
services.Remove(pipelines[1]);
services.Remove(pipelines[2]);
services.Remove(pipelines[3]);
services.AddSingleton<ProbeHandler>();
services.AddTransient<IRequestHandler<ShopRenewCommand, ShopRenewResponse>>(sp => sp.GetRequiredService<ProbeHandler>());
using var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();
var probe = provider.GetRequiredService<ProbeHandler>();
var valid = new ShopRenewCommand("user", "shop", 1);
ShopRenewCommand[] invalid =
[
valid with { UserName = " " }, valid with { UserName = new string('u', 21) },
valid with { ShopId = " " }, valid with { ShopId = new string('s', 51) },
valid with { RenewDuration = 0 }, valid with { RenewDuration = 121 },
valid with { Remark = new string('r', 256) }
];
foreach (var command in invalid)
{
try
{
await mediator.Send(command);
throw new Exception("无效参数未被管道拦截");
}
catch (ValidationException) { }
}
if (probe.Calls != 0) throw new Exception("无效命令进入了处理器");
await mediator.Send(valid);
await mediator.Send(valid with
{
UserName = " " + new string('u', 20) + " ", ShopId = new string('s', 50),
RenewDuration = 120, Remark = " " + new string('r', 255) + " "
});
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
try
{
await mediator.Send(valid, cancellation.Token);
throw new Exception("已取消命令未被拦截");
}
catch (OperationCanceledException) { }
if (probe.Calls != 2) throw new Exception("有效命令或取消命令的调用次数错误");
var minimumQuote = ((IAccountConsumptionRequest)(valid with { RenewDuration = 3 })).CalculateConsumption(new UserEntity { ShopPointsPerMonth = 10 });
var customQuote = ((IAccountConsumptionRequest)(valid with { RenewDuration = 2 })).CalculateConsumption(new UserEntity { ShopPointsPerMonth = 45 });
if (minimumQuote != new ConsumptionQuote(90m, 30m) || customQuote != new ConsumptionQuote(90m, 45m))
throw new Exception("续费最低价格或账户自定义价格计算错误");
var reuseServices = new ServiceCollection();
reuseServices.AddMediatRAttributedBehaviors(typeof(AccountOnlyRequest).Assembly);
var accountBehaviors = reuseServices.Where(s => s.ServiceType == typeof(IPipelineBehavior<AccountOnlyRequest, string>)).ToArray();
var shopBehaviors = reuseServices.Where(s => s.ServiceType == typeof(IPipelineBehavior<ShopOnlyRequest, string>)).ToArray();
if (accountBehaviors.Length != 2
|| accountBehaviors[0].ImplementationType != typeof(TransactionBehavior<AccountOnlyRequest, string>)
|| accountBehaviors[1].ImplementationType != typeof(AccountConsumptionBehavior<AccountOnlyRequest, string>)
|| shopBehaviors.Length != 1
|| shopBehaviors[0].ImplementationType != typeof(ShopOwnershipBehavior<ShopOnlyRequest, string>))
throw new Exception("非商铺消费和非消费商铺操作的管道注册未独立");
Console.WriteLine("PASS: 独立消费与商铺操作管道注册、续费计费策略验证通过。");
var orderedServices = new ServiceCollection();
orderedServices.AddSingleton<ExecutionLog>();
orderedServices.AddTransient<IRequestHandler<OrderRequest, string>, OrderHandler>();
orderedServices.AddMediatRAttributedBehaviors(typeof(OrderRequest).Assembly);
orderedServices.AddMediatRAttributedBehaviors(typeof(OrderRequest).Assembly);
using var orderedProvider = orderedServices.BuildServiceProvider();
await orderedProvider.GetRequiredService<IMediator>().Send(new OrderRequest());
var events = orderedProvider.GetRequiredService<ExecutionLog>().Events;
if (!events.SequenceEqual(new[] { "Outer before", "Inner before", "Handler", "Inner after", "Outer after" }))
throw new Exception("特性管道执行顺序不正确:" + string.Join(", ", events));
if (pipelines[1].ImplementationType != typeof(TransactionBehavior<ShopRenewCommand, ShopRenewResponse>))
throw new Exception("开放泛型事务管道未正确闭合");
var executionContext = new UnitOfWorkExecutionContext();
var enter = typeof(UnitOfWorkExecutionContext).GetMethod("Enter", BindingFlags.Instance | BindingFlags.NonPublic)!;
IFreeSql CreateOrm() => DispatchProxy.Create<IFreeSql, UnusedOrmProxy>();
IDisposable Enter(IFreeSql orm) => (IDisposable)enter.Invoke(executionContext, [orm])!;
try
{
_ = executionContext.Orm;
throw new Exception("无事务时不应允许访问 ORM");
}
catch (InvalidOperationException) { }
async Task VerifyIsolation(IFreeSql orm)
{
using (Enter(orm))
{
await Task.Yield();
if (!ReferenceEquals(executionContext.Orm, orm)) throw new Exception("并行事务上下文串用");
try
{
using var nested = Enter(CreateOrm());
throw new Exception("嵌套事务应被拒绝");
}
catch (TargetInvocationException exception) when (exception.InnerException is InvalidOperationException) { }
if (!ReferenceEquals(executionContext.Orm, orm)) throw new Exception("嵌套失败破坏了外层上下文");
}
if (executionContext.IsActive) throw new Exception("执行结束后上下文未清理");
}
await Task.WhenAll(VerifyIsolation(CreateOrm()), VerifyIsolation(CreateOrm()));
try
{
using var scope = Enter(CreateOrm());
throw new ApplicationException("模拟处理器失败");
}
catch (ApplicationException) { }
if (executionContext.IsActive) throw new Exception("异常后上下文未清理");
Console.WriteLine("PASS: 泛型事务管道闭合、并行上下文隔离、嵌套拒绝和异常清理验证通过。");
Console.WriteLine("PASS: CallerLineNumber 顺序、真实嵌套调用与重复扫描验证通过。");
Console.WriteLine("PASS: 实际 DI 注册、7 项非法输入、有效边界参数和取消请求验证通过。");
sealed class ProbeHandler : IRequestHandler<ShopRenewCommand, ShopRenewResponse>
{
public int Calls { get; private set; }
public Task<ShopRenewResponse> Handle(ShopRenewCommand request, CancellationToken cancellationToken)
{
Calls++;
return Task.FromResult(new ShopRenewResponse { ShopId = request.ShopId });
}
}
[MediatRBehavior(typeof(OuterOrderBehavior))]
[MediatRBehavior(typeof(InnerOrderBehavior))]
sealed record OrderRequest : IRequest<string>;
sealed class ExecutionLog
{
public List<string> Events { get; } = [];
}
sealed class OrderHandler(ExecutionLog log) : IRequestHandler<OrderRequest, string>
{
public Task<string> Handle(OrderRequest request, CancellationToken cancellationToken)
{
log.Events.Add("Handler");
return Task.FromResult("ok");
}
}
sealed class OuterOrderBehavior(ExecutionLog log) : IPipelineBehavior<OrderRequest, string>
{
public async Task<string> Handle(OrderRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<string> next)
{
log.Events.Add("Outer before");
var result = await next();
log.Events.Add("Outer after");
return result;
}
}
sealed class InnerOrderBehavior(ExecutionLog log) : IPipelineBehavior<OrderRequest, string>
{
public async Task<string> Handle(OrderRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<string> next)
{
log.Events.Add("Inner before");
var result = await next();
log.Events.Add("Inner after");
return result;
}
}
public class UnusedOrmProxy : DispatchProxy
{
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
=> throw new NotSupportedException("上下文测试不应访问数据库");
}
// 非商铺消费:没有 ShopId,也不启用商铺归属管道。
[MediatRBehavior(typeof(TransactionBehavior<,>))]
[MediatRBehavior(typeof(AccountConsumptionBehavior<,>))]
sealed record AccountOnlyRequest(string UserName, decimal Amount) : IRequest<string>, IAccountConsumptionRequest
{
public AccountConsumptionContext? ConsumptionContext { get; set; }
public ConsumptionQuote CalculateConsumption(UserEntity user) => new(Amount, Amount);
}
// 商铺操作:不涉及余额与计费,也可用于不需要事务的读取。
[MediatRBehavior(typeof(ShopOwnershipBehavior<,>))]
sealed record ShopOnlyRequest(string UserName, string ShopId) : IRequest<string>, IShopOwnershipRequest
{
public ShopEntity? OwnedShop { get; set; }
}