using NSubstitute; using PnvPanel.Application.Admin.Nodes; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Models; using PnvPanel.Application.Tests.TestSupport; using PnvPanel.Domain.Nodes; using Xunit; namespace PnvPanel.Application.Tests.Admin.Nodes; public class UpdateNodeCommandHandlerTests { private readonly IXuiPanelGateway _gateway = Substitute.For(); private readonly ISecretProtector _secretProtector = Substitute.For(); private readonly ICurrentUser _currentUser = Substitute.For(); private static Node SeedNode( string baseAddress = "https://node1.example.com", string username = "admin", string protectedPassword = "protected-old-password" ) => Node.Register("node-1", new Uri(baseAddress), new NodeCredentials(username, protectedPassword), null); [Fact] public async Task Handle_WhenAddressChanged_ValidatesAndUpdatesAddressAndInvalidatesClient() { using var dbContext = InMemoryDbContextFactory.Create(); var node = SeedNode(); dbContext.Nodes.Add(node); await dbContext.SaveChangesAsync(CancellationToken.None); _gateway.ValidateBaseAddress(Arg.Any()).Returns(Result.Success()); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand( node.Id, "node-1", "https://node1-new.example.com", null, true, false, null, null ); var result = await handler.Handle(command, CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("https://node1-new.example.com/", node.BaseAddress.ToString()); _gateway.Received(1).ValidateBaseAddress(Arg.Is(u => u!.Host == "node1-new.example.com")); _gateway.Received(1).InvalidateClient(node.Id); } [Fact] public async Task Handle_WhenAddressUnchanged_DoesNotValidateOrInvalidateClient() { using var dbContext = InMemoryDbContextFactory.Create(); var node = SeedNode(); dbContext.Nodes.Add(node); await dbContext.SaveChangesAsync(CancellationToken.None); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand( node.Id, "node-1-renamed", "https://node1.example.com", "eu", true, false, null, null ); var result = await handler.Handle(command, CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("node-1-renamed", node.Name); _gateway.DidNotReceive().ValidateBaseAddress(Arg.Any()); _gateway.DidNotReceive().InvalidateClient(Arg.Any()); } [Fact] public async Task Handle_WithInvalidBaseAddress_ReturnsValidationErrorWithoutChangingNode() { using var dbContext = InMemoryDbContextFactory.Create(); var node = SeedNode(); dbContext.Nodes.Add(node); await dbContext.SaveChangesAsync(CancellationToken.None); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand(node.Id, "node-1", "not-a-uri", null, true, false, null, null); var result = await handler.Handle(command, CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(NodeErrors.InvalidBaseAddress, result.Error); Assert.Equal("https://node1.example.com/", node.BaseAddress.ToString()); _gateway.DidNotReceive().ValidateBaseAddress(Arg.Any()); } [Fact] public async Task Handle_WhenGatewayRejectsNewBaseAddress_ReturnsFailureWithoutChangingNode() { using var dbContext = InMemoryDbContextFactory.Create(); var node = SeedNode(); dbContext.Nodes.Add(node); await dbContext.SaveChangesAsync(CancellationToken.None); var error = Error.Validation("Nodes.SchemeNotAllowed", "Разрешён только HTTPS."); _gateway.ValidateBaseAddress(Arg.Any()).Returns(Result.Failure(error)); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand( node.Id, "node-1", "http://node1-new.example.com", null, true, false, null, null ); var result = await handler.Handle(command, CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(error, result.Error); Assert.Equal("https://node1.example.com/", node.BaseAddress.ToString()); _gateway.DidNotReceive().InvalidateClient(Arg.Any()); } [Fact] public async Task Handle_WhenUsernameAndPasswordProvided_UpdatesCredentialsAndInvalidatesClient() { using var dbContext = InMemoryDbContextFactory.Create(); var node = SeedNode(); dbContext.Nodes.Add(node); await dbContext.SaveChangesAsync(CancellationToken.None); _secretProtector.Protect("new-password").Returns("protected-new-password"); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand( node.Id, "node-1", "https://node1.example.com", null, true, false, "new-admin", "new-password" ); var result = await handler.Handle(command, CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("new-admin", node.Credentials.Username); Assert.Equal("protected-new-password", node.Credentials.ProtectedPassword); _gateway.Received(1).InvalidateClient(node.Id); } [Fact] public async Task Handle_WhenOnlyUsernameProvided_DoesNotChangeCredentials() { using var dbContext = InMemoryDbContextFactory.Create(); var node = SeedNode(); dbContext.Nodes.Add(node); await dbContext.SaveChangesAsync(CancellationToken.None); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand( node.Id, "node-1", "https://node1.example.com", null, true, false, "new-admin", null ); var result = await handler.Handle(command, CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("admin", node.Credentials.Username); Assert.Equal("protected-old-password", node.Credentials.ProtectedPassword); _gateway.DidNotReceive().InvalidateClient(Arg.Any()); } [Fact] public async Task Handle_SetsNotifyOnStatusChange() { using var dbContext = InMemoryDbContextFactory.Create(); var node = SeedNode(); dbContext.Nodes.Add(node); await dbContext.SaveChangesAsync(CancellationToken.None); Assert.False(node.NotifyOnStatusChange); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand( node.Id, "node-1", "https://node1.example.com", null, true, true, null, null ); var result = await handler.Handle(command, CancellationToken.None); Assert.True(result.IsSuccess); Assert.True(node.NotifyOnStatusChange); } [Fact] public async Task Handle_WhenNodeNotFound_ReturnsNotFound() { using var dbContext = InMemoryDbContextFactory.Create(); var handler = new UpdateNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); var command = new UpdateNodeCommand( Guid.NewGuid(), "node-1", "https://node1.example.com", null, true, false, null, null ); var result = await handler.Handle(command, CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(NodeErrors.NotFound, result.Error); } }