using NSubstitute; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Configs; using PnvPanel.Application.Configs.Revoke; using PnvPanel.Application.Tests.TestSupport; using PnvPanel.Domain.Configs; using PnvPanel.Domain.Inbounds; using PnvPanel.Domain.Nodes; using Xunit; namespace PnvPanel.Application.Tests.Configs.Revoke; public class RevokeVpnConfigCommandHandlerTests { private readonly IXuiPanelGateway _gateway = Substitute.For(); private readonly IRealtimeNotifier _notifier = Substitute.For(); [Fact] public async Task Handle_WhenActiveConfigOwnedByUser_RevokesRemovesRemoteClientAndNotifies() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443); var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null, 0); config.AssignRemoteClient("external-id"); dbContext.Nodes.Add(node); dbContext.Inbounds.Add(inbound); dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId)); var result = await handler.Handle(new RevokeVpnConfigCommand(config.Id), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(ConfigStatus.Revoked, config.Status); await _gateway.Received(1).RemoveClientAsync( Arg.Any(), inbound.RemoteInboundId, "external-id", config.Protocol, Arg.Any()); await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Revoked, Arg.Any()); } [Fact] public async Task Handle_WhenAlreadyRevoked_IsIdempotentAndDoesNotCallGateway() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var inbound = Inbound.FromRemote(Guid.NewGuid(), "1", VpnProtocol.Vless, "remark", 443); var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null, 0); config.Revoke(); dbContext.Inbounds.Add(inbound); dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId)); var result = await handler.Handle(new RevokeVpnConfigCommand(config.Id), CancellationToken.None); Assert.True(result.IsSuccess); await _gateway.DidNotReceive().RemoveClientAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public async Task Handle_WhenConfigNotFound_ReturnsNotFound() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId)); var result = await handler.Handle(new RevokeVpnConfigCommand(Guid.NewGuid()), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); } [Fact] public async Task Handle_WhenNotAuthenticated_ReturnsUnauthorized() { using var dbContext = InMemoryDbContextFactory.Create(); var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Anonymous()); var result = await handler.Handle(new RevokeVpnConfigCommand(Guid.NewGuid()), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(PnvPanel.Application.Auth.AuthErrors.Unauthorized, result.Error); } }