using NSubstitute; using PnvPanel.Application.Auth; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Models; using PnvPanel.Application.Configs; using PnvPanel.Application.Configs.Rotate; using PnvPanel.Application.Tests.TestSupport; using PnvPanel.Domain.Configs; using PnvPanel.Domain.Inbounds; using PnvPanel.Domain.Nodes; using Xunit; namespace PnvPanel.Application.Tests.Configs.Rotate; public class RotateVpnConfigCommandHandlerTests { private readonly IXuiPanelGateway _gateway = Substitute.For(); [Fact] public async Task Handle_WhenActiveConfigOwnedByUser_RotatesAndAddsNewClient() { 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, "my-config"); config.AssignRemoteClient("old-external-id"); dbContext.Nodes.Add(node); dbContext.Inbounds.Add(inbound); dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); _gateway.AddClientAsync( Arg.Any(), inbound.RemoteInboundId, config.Protocol, Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Result.Success("new-external-id")); var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, FakeCurrentUser.Authenticated(userId)); var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("new-external-id", config.ClientExternalId); await _gateway.Received(1).RemoveClientAsync( Arg.Any(), inbound.RemoteInboundId, "old-external-id", config.Protocol, Arg.Any()); } [Fact] public async Task Handle_WhenConfigNotFound_ReturnsNotFound() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, FakeCurrentUser.Authenticated(userId)); var result = await handler.Handle(new RotateVpnConfigCommand(Guid.NewGuid()), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); } [Fact] public async Task Handle_WhenConfigBelongsToAnotherUser_ReturnsNotFound() { using var dbContext = InMemoryDbContextFactory.Create(); var ownerId = Guid.NewGuid(); var otherUserId = Guid.NewGuid(); var inbound = Inbound.FromRemote(Guid.NewGuid(), "1", VpnProtocol.Vless, "remark", 443); var config = VpnConfig.Create(ownerId, inbound.Id, VpnProtocol.Vless, null); dbContext.Inbounds.Add(inbound); dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, FakeCurrentUser.Authenticated(otherUserId)); var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); } [Fact] public async Task Handle_WhenConfigAlreadyRevoked_ReturnsNotFound() { 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 RotateVpnConfigCommandHandler(dbContext, _gateway, FakeCurrentUser.Authenticated(userId)); var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); } [Fact] public async Task Handle_WhenGatewayAddClientFails_ReturnsFailureWithoutMutatingConfig() { 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("old-external-id"); dbContext.Nodes.Add(node); dbContext.Inbounds.Add(inbound); dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); var gatewayError = Error.Failure("Xui.Unreachable", "Панель недоступна."); _gateway.AddClientAsync( Arg.Any(), inbound.RemoteInboundId, config.Protocol, Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Result.Failure(gatewayError)); var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, FakeCurrentUser.Authenticated(userId)); var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(gatewayError, result.Error); Assert.Equal("old-external-id", config.ClientExternalId); } }