Two new maintenance actions remove VpnConfig records (with their traffic samples) from the database: all revoked configs, and configs whose inbound or node no longer exists. 3x-ui is not touched; both write an AuditLog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
4.6 KiB
C#
115 lines
4.6 KiB
C#
using PnvPanel.Application.Admin.Maintenance;
|
|
using PnvPanel.Application.Tests.TestSupport;
|
|
using PnvPanel.Domain.Configs;
|
|
using PnvPanel.Domain.Inbounds;
|
|
using PnvPanel.Domain.Nodes;
|
|
using Xunit;
|
|
|
|
namespace PnvPanel.Application.Tests.Admin.Maintenance;
|
|
|
|
public class DeleteConfigsCommandHandlerTests
|
|
{
|
|
private static Node CreateNode() =>
|
|
Node.Register(
|
|
"node-1",
|
|
new Uri("https://node-1.example.com"),
|
|
new NodeCredentials("admin", "protected"),
|
|
null
|
|
);
|
|
|
|
private static VpnConfig CreateConfig(Guid inboundId) =>
|
|
VpnConfig.Create(Guid.NewGuid(), inboundId, VpnProtocol.Vless, null);
|
|
|
|
[Fact]
|
|
public async Task DeleteRevoked_DeletesRevokedConfigsAndTheirTraffic_KeepsOthers()
|
|
{
|
|
using var dbContext = InMemoryDbContextFactory.Create();
|
|
var node = CreateNode();
|
|
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
|
var revoked = CreateConfig(inbound.Id);
|
|
revoked.Revoke();
|
|
var active = CreateConfig(inbound.Id);
|
|
|
|
dbContext.Nodes.Add(node);
|
|
dbContext.Inbounds.Add(inbound);
|
|
dbContext.VpnConfigs.AddRange(revoked, active);
|
|
dbContext.TrafficSamples.AddRange(
|
|
TrafficSample.Create(revoked.Id, DateTimeOffset.UtcNow, 1, 2),
|
|
TrafficSample.Create(active.Id, DateTimeOffset.UtcNow, 3, 4)
|
|
);
|
|
await dbContext.SaveChangesAsync(CancellationToken.None);
|
|
|
|
var handler = new DeleteRevokedConfigsCommandHandler(
|
|
dbContext,
|
|
FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin")
|
|
);
|
|
|
|
var result = await handler.Handle(new DeleteRevokedConfigsCommand(), CancellationToken.None);
|
|
// Хендлер не коммитит сам (в проде это делает UnitOfWorkBehavior после диспетчера) — коммитим явно.
|
|
await dbContext.SaveChangesAsync(CancellationToken.None);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(1, result.Value);
|
|
Assert.Equal([active.Id], dbContext.VpnConfigs.Select(c => c.Id).ToList());
|
|
Assert.Equal([active.Id], dbContext.TrafficSamples.Select(s => s.ConfigId).ToList());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteOrphaned_DeletesConfigsWithoutInboundOrNode_KeepsLiveOnes()
|
|
{
|
|
using var dbContext = InMemoryDbContextFactory.Create();
|
|
var node = CreateNode();
|
|
var liveInbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "live", 443);
|
|
// Инбаунд, чья нода удалена.
|
|
var orphanInbound = Inbound.FromRemote(Guid.NewGuid(), "2", VpnProtocol.Vless, "orphan", 444);
|
|
|
|
var live = CreateConfig(liveInbound.Id);
|
|
var onDeletedNode = CreateConfig(orphanInbound.Id);
|
|
var onDeletedInbound = CreateConfig(Guid.NewGuid());
|
|
|
|
dbContext.Nodes.Add(node);
|
|
dbContext.Inbounds.AddRange(liveInbound, orphanInbound);
|
|
dbContext.VpnConfigs.AddRange(live, onDeletedNode, onDeletedInbound);
|
|
dbContext.TrafficSamples.Add(
|
|
TrafficSample.Create(onDeletedInbound.Id, DateTimeOffset.UtcNow, 1, 2)
|
|
);
|
|
await dbContext.SaveChangesAsync(CancellationToken.None);
|
|
|
|
var handler = new DeleteOrphanedConfigsCommandHandler(
|
|
dbContext,
|
|
FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin")
|
|
);
|
|
|
|
var result = await handler.Handle(new DeleteOrphanedConfigsCommand(), CancellationToken.None);
|
|
await dbContext.SaveChangesAsync(CancellationToken.None);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(2, result.Value);
|
|
Assert.Equal([live.Id], dbContext.VpnConfigs.Select(c => c.Id).ToList());
|
|
Assert.Empty(dbContext.TrafficSamples);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteOrphaned_WhenNothingToDelete_ReturnsZero()
|
|
{
|
|
using var dbContext = InMemoryDbContextFactory.Create();
|
|
var node = CreateNode();
|
|
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "live", 443);
|
|
dbContext.Nodes.Add(node);
|
|
dbContext.Inbounds.Add(inbound);
|
|
dbContext.VpnConfigs.Add(CreateConfig(inbound.Id));
|
|
await dbContext.SaveChangesAsync(CancellationToken.None);
|
|
|
|
var handler = new DeleteOrphanedConfigsCommandHandler(
|
|
dbContext,
|
|
FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin")
|
|
);
|
|
|
|
var result = await handler.Handle(new DeleteOrphanedConfigsCommand(), CancellationToken.None);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(0, result.Value);
|
|
Assert.Single(dbContext.VpnConfigs);
|
|
}
|
|
}
|