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(); private readonly IIdentityService _identityService = Substitute.For(); private static CurrentUserProfile MakeProfile(Guid userId) => new( userId, "alice", Guid.NewGuid(), "user", true, false, 3, RoleQuota.Unlimited, "sub-token" ); [Fact] public async Task Handle_WhenActiveConfigOwnedByUser_RotatesAndAddsNewClient() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(MakeProfile(userId)); 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(), Arg.Any() ) .Returns(Result.Success("new-external-id")); var handler = new RotateVpnConfigCommandHandler( dbContext, _gateway, _identityService, 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, _identityService, 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, _identityService, 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, _identityService, 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(); _identityService .GetProfileAsync(userId, Arg.Any()) .Returns(MakeProfile(userId)); 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(), Arg.Any() ) .Returns(Result.Failure(gatewayError)); var handler = new RotateVpnConfigCommandHandler( dbContext, _gateway, _identityService, 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); } }