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); 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); 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); } }