Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions test/NodeGuard.Tests/Jobs/AuditLogCleanupJobTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ public async Task Execute_LogsCutoffDateAndRetentionDays()
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) =>
It.Is<It.IsAnyType>((v, t) =>
v.ToString().Contains("Deleting audit logs older than") &&
v.ToString().Contains($"retention: {Constants.AUDIT_LOG_RETENTION_DAYS} days")),
It.IsAny<Exception>(),
Expand All @@ -244,7 +244,7 @@ public async Task Execute_PassesCutoffDateToRepository()
// Assert
capturedCutoffDate.Should().NotBe(DateTimeOffset.MinValue, "repository should be called with a valid cutoff date");
capturedCutoffDate.Should().BeBefore(DateTimeOffset.UtcNow, "cutoff date should be in the past");

var expectedDaysAgo = DateTimeOffset.UtcNow.AddDays(-Constants.AUDIT_LOG_RETENTION_DAYS);
var difference = Math.Abs((capturedCutoffDate - expectedDaysAgo).TotalMinutes);
difference.Should().BeLessThan(1, "cutoff date should match the expected retention calculation");
Expand All @@ -256,7 +256,7 @@ public async Task Execute_MultipleExceptions_AllLogged()
// Arrange
var exception1 = new InvalidOperationException("First error");
var exception2 = new Exception("Second error");

_auditLogRepositoryMock
.SetupSequence(x => x.DeleteOlderThanAsync(It.IsAny<DateTimeOffset>()))
.ThrowsAsync(exception1)
Expand Down
78 changes: 78 additions & 0 deletions test/NodeGuard.Tests/Jobs/RetryRebalanceJobTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* NodeGuard
* Copyright (C) 2023 Elenpay
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

using FluentAssertions;
using Microsoft.Extensions.Logging;
using NodeGuard.Data.Models;
using NodeGuard.Services;
using Quartz;

namespace NodeGuard.Jobs;

public class RetryRebalanceJobTests
{
private readonly Mock<ILogger<RebalanceJob>> _logger = new();
private readonly Mock<IRebalanceService> _rebalanceService = new();

private RebalanceJob CreateJob() => new(_logger.Object, _rebalanceService.Object);

private static IJobExecutionContext BuildContext(int rebalanceId, CancellationToken token = default)
{
var ctxMock = new Mock<IJobExecutionContext>();
var data = new JobDataMap();
data.Put("rebalanceId", rebalanceId);
ctxMock.Setup(c => c.MergedJobDataMap).Returns(data);
ctxMock.Setup(c => c.CancellationToken).Returns(token);
return ctxMock.Object;
}

[Fact]
public async Task Execute_DelegatesToServiceWithRebalanceIdFromJobData()
{
_rebalanceService.Setup(s => s.ExecuteAsync(123, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Rebalance { Id = 123, Status = RebalanceStatus.Succeeded });

await CreateJob().Execute(BuildContext(123));

_rebalanceService.Verify(s => s.ExecuteAsync(123, It.IsAny<CancellationToken>()), Times.Once);
}

[Fact]
public async Task Execute_PropagatesCancellationToken()
{
using var cts = new CancellationTokenSource();
_rebalanceService.Setup(s => s.ExecuteAsync(7, cts.Token))
.ReturnsAsync(new Rebalance { Id = 7 });

await CreateJob().Execute(BuildContext(7, cts.Token));

_rebalanceService.Verify(s => s.ExecuteAsync(7, cts.Token), Times.Once);
}

[Fact]
public async Task Execute_ServiceThrows_RethrowsAsJobExecutionException()
{
_rebalanceService.Setup(s => s.ExecuteAsync(99, It.IsAny<CancellationToken>()))
.ThrowsAsync(new InvalidOperationException("boom"));

var act = () => CreateJob().Execute(BuildContext(99));

await act.Should().ThrowAsync<JobExecutionException>();
}
}
12 changes: 6 additions & 6 deletions test/NodeGuard.Tests/Rpc/NodeGuardServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -910,12 +910,12 @@ private static WalletRepository CreateWalletRepository(IDbContextFactory<Applica
new Mock<INBXplorerService>().Object);
}

private static Mock<IHtlcMonitoringScheduler> CreateHtlcMonitoringSchedulerMock()
{
var mock = new Mock<IHtlcMonitoringScheduler>();
mock.Setup(x => x.EnsureNodeWorkerScheduled(It.IsAny<Node>())).ReturnsAsync(true);
return mock;
}
private static Mock<IHtlcMonitoringScheduler> CreateHtlcMonitoringSchedulerMock()
{
var mock = new Mock<IHtlcMonitoringScheduler>();
mock.Setup(x => x.EnsureNodeWorkerScheduled(It.IsAny<Node>())).ReturnsAsync(true);
return mock;
}

private static ISchedulerFactory GetSchedulerFactoryMock()
{
Expand Down
132 changes: 132 additions & 0 deletions test/NodeGuard.Tests/Services/LightningServiceRebalanceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* NodeGuard
* Copyright (C) 2023 Elenpay
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

using FluentAssertions;
using Lnrpc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NodeGuard.Data;
using NodeGuard.Data.Models;
using NodeGuard.Data.Repositories.Interfaces;

namespace NodeGuard.Services;

/// <summary>
/// Tests for the rebalance-specific methods added to LightningService:
/// ProbeRouteAsync (BoS-style binary-search probing) and
/// GetLocalOutboundFeeRatePpmAsync (correct policy-side selection on a ChannelEdge).
/// </summary>
public class LightningServiceRebalanceTests
{
private readonly Mock<ILogger<LightningService>> _logger = new();
private readonly Mock<ILightningClientService> _lightningClient = new();
private readonly Mock<ILightningRouterService> _lightningRouter = new();
private readonly Mock<IDbContextFactory<ApplicationDbContext>> _dbContextFactory = new();

private LightningService CreateService() => new(
_logger.Object,
new Mock<IChannelOperationRequestRepository>().Object,
new Mock<INodeRepository>().Object,
_dbContextFactory.Object,
new Mock<IChannelOperationRequestPSBTRepository>().Object,
new Mock<IChannelRepository>().Object,
new Mock<IRemoteSignerService>().Object,
new Mock<INBXplorerService>().Object,
new Mock<ICoinSelectionService>().Object,
_lightningClient.Object,
_lightningRouter.Object);

private static Node CreateNode(string pubkey = "030000000000000000000000000000000000000000000000000000000000000001")
=> new() { Id = 1, PubKey = pubkey, Endpoint = "localhost:10009", ChannelAdminMacaroon = "mac" };

private static Lnrpc.Route MakeRoute(long amtMsat = 100_000_000)
{
var route = new Lnrpc.Route { TotalAmtMsat = amtMsat, TotalFeesMsat = 0 };
route.Hops.Add(new Hop { ChanId = 1, AmtToForwardMsat = amtMsat });
return route;
}

[Fact]
public async Task ProbeRouteAsync_QueryRoutesAlwaysEmpty_ReturnsNoRoute()
{
_lightningClient
.Setup(x => x.QueryRoutes(It.IsAny<Node>(), It.IsAny<QueryRoutesRequest>(), null))
.ReturnsAsync(new QueryRoutesResponse());

var service = CreateService();
var result = await service.ProbeRouteAsync(CreateNode(), 100_000, 1_000_000,
null, null, Constants.REBALANCE_PROBE_BACKOFF_RATIO, CancellationToken.None);

result.Should().BeOfType<ProbeResult.NoRoute>();
}

[Fact]
public async Task ProbeRouteAsync_LastHopIncorrectPaymentDetails_ReturnsSuccessAtFullAmount()
{
var route = MakeRoute();
var routesResponse = new QueryRoutesResponse();
routesResponse.Routes.Add(route);
_lightningClient
.Setup(x => x.QueryRoutes(It.IsAny<Node>(), It.IsAny<QueryRoutesRequest>(), null))
.ReturnsAsync(routesResponse);

// The probe-success signal: random hash failed at the last hop, but routing worked.
_lightningRouter
.Setup(x => x.SendToRouteV2Async(It.IsAny<Node>(), It.IsAny<Routerrpc.SendToRouteRequest>(),
It.IsAny<CancellationToken>(), null))
.ReturnsAsync(new HTLCAttempt
{
Failure = new Failure { Code = Failure.Types.FailureCode.IncorrectOrUnknownPaymentDetails },
});

var service = CreateService();
var result = await service.ProbeRouteAsync(CreateNode(), 100_000, 1_000_000,
null, null, Constants.REBALANCE_PROBE_BACKOFF_RATIO, CancellationToken.None);

var success = result.Should().BeOfType<ProbeResult.Success>().Which;
success.AmountSats.Should().Be(100_000);
success.Route.Should().BeSameAs(route);
}

[Fact]
public async Task ProbeRouteAsync_AllRoutesFailMidPath_HalvesUntilBelowMinThenReturnsNoRoute()
{
var routesResponse = new QueryRoutesResponse();
routesResponse.Routes.Add(MakeRoute());
_lightningClient
.Setup(x => x.QueryRoutes(It.IsAny<Node>(), It.IsAny<QueryRoutesRequest>(), null))
.ReturnsAsync(routesResponse);

// Mid-route failure: not the IncorrectOrUnknownPaymentDetails signal.
_lightningRouter
.Setup(x => x.SendToRouteV2Async(It.IsAny<Node>(), It.IsAny<Routerrpc.SendToRouteRequest>(),
It.IsAny<CancellationToken>(), null))
.ReturnsAsync(new HTLCAttempt
{
Failure = new Failure { Code = Failure.Types.FailureCode.TemporaryChannelFailure },
});

var service = CreateService();
var result = await service.ProbeRouteAsync(CreateNode(), 100_000, 1_000_000,
null, null, Constants.REBALANCE_PROBE_BACKOFF_RATIO, CancellationToken.None);

result.Should().BeOfType<ProbeResult.NoRoute>();
}

}
Loading
Loading