using NSubstitute; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Models; using PnvPanel.Application.Configs; using PnvPanel.Application.Configs.Create; using PnvPanel.Application.Tests.TestSupport; using PnvPanel.Domain.Inbounds; using PnvPanel.Infrastructure.Persistence; using Xunit; namespace PnvPanel.Application.Tests.Configs.Create; /// /// Только ветки ДО ReserveQuotaSlotAsync (billing-guard) — дальше хендлер уходит в /// pg_advisory_xact_lock, который InMemory-провайдер не поддерживает (см. CLAUDE.md/ /// ConfigQuotaTests в IntegrationTests для позитивного пути и гонок). /// public class CreateVpnConfigCommandHandlerTests { private readonly IIdentityService _identityService = Substitute.For(); private readonly IXuiPanelGateway _gateway = Substitute.For(); private static CurrentUserProfile Profile( Guid userId, Guid roleId, bool billingEnabled, DateTimeOffset? billingPaidUntil ) => new( userId, "alice", roleId, "premium", IsActivated: true, IsBlocked: false, MaxConfigs: 5, MaxIpLimit: 3, SubscriptionToken: "sub-token", BillingEnabled: billingEnabled, BillingPaidUntil: billingPaidUntil, BillingSuspended: false ); private async Task<(Inbound inbound, Guid roleId)> SeedAllowedInboundAsync(AppDbContext dbContext) { var roleId = Guid.NewGuid(); var node = Domain.Nodes.Node.Register( "node-1", new Uri("https://node1.example.com"), new Domain.Nodes.NodeCredentials("admin", "protected"), null ); var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443); inbound.Publish(null, [roleId]); dbContext.Nodes.Add(node); dbContext.Inbounds.Add(inbound); await dbContext.SaveChangesAsync(CancellationToken.None); return (inbound, roleId); } [Fact] public async Task Handle_WhenBillingEnabledAndPaidUntilExpired_ReturnsBillingRequired() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var (inbound, roleId) = await SeedAllowedInboundAsync(dbContext); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId, roleId, billingEnabled: true, billingPaidUntil: DateTimeOffset.UtcNow.AddDays(-1))); var handler = new CreateVpnConfigCommandHandler( dbContext, _identityService, _gateway, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreateVpnConfigCommand(inbound.Id, null), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.BillingRequired, result.Error); await _gateway .DidNotReceive() .AddClientAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() ); } [Fact] public async Task Handle_WhenBillingEnabledAndPaidUntilNull_ReturnsBillingRequired() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var (inbound, roleId) = await SeedAllowedInboundAsync(dbContext); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(Profile(userId, roleId, billingEnabled: true, billingPaidUntil: null)); var handler = new CreateVpnConfigCommandHandler( dbContext, _identityService, _gateway, FakeCurrentUser.Authenticated(userId) ); var result = await handler.Handle( new CreateVpnConfigCommand(inbound.Id, null), CancellationToken.None ); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.BillingRequired, result.Error); } }