Add inbound country flags and node load in location picker
- Inbound.CountryCode (ISO alpha-2), set on publish; flag shown before the location name (SVG via country-flag-icons on the site, emoji in bot) - Node.Capacity (optional, not a limit); available inbounds report load percent of capacity or active config count, sorted least loaded first Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7743977a1
commit
bb5e59904f
51 files changed
+3160
-284
No files matched your search
+1
-1
@@ -52,7 +52,7 @@ public class CreateVpnConfigCommandHandlerTests
|
||||
null
|
||||
);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
inbound.Publish(null, [roleId]);
|
||||
inbound.Publish(null, null, [roleId]);
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class GetMyConfigsQueryHandlerTests
|
||||
var otherUserId = Guid.NewGuid();
|
||||
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "1", VpnProtocol.Vless, "remark", 443);
|
||||
inbound.Publish("My inbound", []);
|
||||
inbound.Publish("My inbound", null, []);
|
||||
|
||||
var activeConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Active");
|
||||
var revokedConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Revoked");
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
using NSubstitute;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
using PnvPanel.Application.Configs.ListAvailableInbounds;
|
||||
using PnvPanel.Application.Tests.TestSupport;
|
||||
using PnvPanel.Domain.Configs;
|
||||
using PnvPanel.Domain.Inbounds;
|
||||
using PnvPanel.Domain.Nodes;
|
||||
using Xunit;
|
||||
|
||||
namespace PnvPanel.Application.Tests.Configs.ListAvailableInbounds;
|
||||
|
||||
public class ListAvailableInboundsQueryHandlerTests
|
||||
{
|
||||
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
|
||||
private readonly Guid _userId = Guid.NewGuid();
|
||||
private readonly Guid _roleId = Guid.NewGuid();
|
||||
|
||||
public ListAvailableInboundsQueryHandlerTests()
|
||||
{
|
||||
var profile = new CurrentUserProfile(
|
||||
_userId,
|
||||
"alice",
|
||||
_roleId,
|
||||
"user",
|
||||
true,
|
||||
false,
|
||||
5,
|
||||
null,
|
||||
RoleQuota.Unlimited,
|
||||
"sub-token",
|
||||
false,
|
||||
null,
|
||||
false
|
||||
);
|
||||
_identityService.GetProfileAsync(_userId, Arg.Any<CancellationToken>()).Returns(profile);
|
||||
}
|
||||
|
||||
private static Node CreateNode(string name, int? capacity)
|
||||
{
|
||||
var node = Node.Register(
|
||||
name,
|
||||
new Uri($"https://{name}.example.com"),
|
||||
new NodeCredentials("admin", "protected"),
|
||||
null
|
||||
);
|
||||
node.SetCapacity(capacity);
|
||||
return node;
|
||||
}
|
||||
|
||||
private Inbound CreatePublishedInbound(Node node, string remoteId, string displayName)
|
||||
{
|
||||
var inbound = Inbound.FromRemote(node.Id, remoteId, VpnProtocol.Vless, "remark", 443);
|
||||
inbound.Publish(displayName, "DE", [_roleId]);
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private static IEnumerable<VpnConfig> ActiveConfigs(Inbound inbound, int count) =>
|
||||
Enumerable
|
||||
.Range(0, count)
|
||||
.Select(_ => VpnConfig.Create(Guid.NewGuid(), inbound.Id, VpnProtocol.Vless, null))
|
||||
.ToList();
|
||||
|
||||
private ListAvailableInboundsQueryHandler CreateHandler(IAppDbContext dbContext) =>
|
||||
new(dbContext, _identityService, FakeCurrentUser.Authenticated(_userId));
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithCapacity_ReturnsPercentOfAllActiveConfigsOnNode()
|
||||
{
|
||||
using var dbContext = InMemoryDbContextFactory.Create();
|
||||
var node = CreateNode("node-1", capacity: 10);
|
||||
var published = CreatePublishedInbound(node, "1", "Germany");
|
||||
// Неопубликованный инбаунд той же ноды тоже грузит сервер.
|
||||
var hidden = Inbound.FromRemote(node.Id, "2", VpnProtocol.Vless, "hidden", 444);
|
||||
var revoked = VpnConfig.Create(Guid.NewGuid(), published.Id, VpnProtocol.Vless, null);
|
||||
revoked.Revoke();
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
dbContext.Inbounds.AddRange(published, hidden);
|
||||
dbContext.VpnConfigs.AddRange(ActiveConfigs(published, 2));
|
||||
dbContext.VpnConfigs.AddRange(ActiveConfigs(hidden, 1));
|
||||
dbContext.VpnConfigs.Add(revoked);
|
||||
await dbContext.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var result = await CreateHandler(dbContext)
|
||||
.Handle(new ListAvailableInboundsQuery(), CancellationToken.None);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
var item = Assert.Single(result.Value);
|
||||
Assert.Equal(30, item.LoadPercent);
|
||||
Assert.Null(item.NodeConfigCount);
|
||||
Assert.Equal("DE", item.CountryCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithoutCapacity_ReturnsConfigCount()
|
||||
{
|
||||
using var dbContext = InMemoryDbContextFactory.Create();
|
||||
var node = CreateNode("node-1", capacity: null);
|
||||
var inbound = CreatePublishedInbound(node, "1", "Germany");
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
dbContext.VpnConfigs.AddRange(ActiveConfigs(inbound, 4));
|
||||
await dbContext.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var result = await CreateHandler(dbContext)
|
||||
.Handle(new ListAvailableInboundsQuery(), CancellationToken.None);
|
||||
|
||||
var item = Assert.Single(result.Value);
|
||||
Assert.Null(item.LoadPercent);
|
||||
Assert.Equal(4, item.NodeConfigCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_SortsByLoadAscending_NodesWithCapacityFirst()
|
||||
{
|
||||
using var dbContext = InMemoryDbContextFactory.Create();
|
||||
var busy = CreateNode("busy", capacity: 10);
|
||||
var idle = CreateNode("idle", capacity: 100);
|
||||
var noCapacity = CreateNode("no-capacity", capacity: null);
|
||||
var busyInbound = CreatePublishedInbound(busy, "1", "A busy");
|
||||
var idleInbound = CreatePublishedInbound(idle, "1", "B idle");
|
||||
var noCapacityInbound = CreatePublishedInbound(noCapacity, "1", "C unknown");
|
||||
|
||||
dbContext.Nodes.AddRange(busy, idle, noCapacity);
|
||||
dbContext.Inbounds.AddRange(busyInbound, idleInbound, noCapacityInbound);
|
||||
dbContext.VpnConfigs.AddRange(ActiveConfigs(busyInbound, 8));
|
||||
dbContext.VpnConfigs.AddRange(ActiveConfigs(idleInbound, 8));
|
||||
await dbContext.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var result = await CreateHandler(dbContext)
|
||||
.Handle(new ListAvailableInboundsQuery(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(
|
||||
["B idle", "A busy", "C unknown"],
|
||||
result.Value.Select(i => i.DisplayName).ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ExcludesInboundsOfDisabledNodes()
|
||||
{
|
||||
using var dbContext = InMemoryDbContextFactory.Create();
|
||||
var node = CreateNode("node-1", capacity: null);
|
||||
node.Disable();
|
||||
var inbound = CreatePublishedInbound(node, "1", "Germany");
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
await dbContext.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var result = await CreateHandler(dbContext)
|
||||
.Handle(new ListAvailableInboundsQuery(), CancellationToken.None);
|
||||
|
||||
Assert.Empty(result.Value);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user