Refactor project files for improved readability and structure
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 31s

- Cleaned up whitespace in Directory.Build.props and Directory.Packages.props for consistency.
- Reformatted project file references in PnvPanel.Api.csproj for better clarity.
- Enhanced code readability in various endpoint files by adjusting line breaks and indentation.
- Standardized method signatures and improved formatting in ResultExtensions and multiple endpoint classes for better maintainability.
This commit is contained in:
Leonid Pershin
2026-07-14 07:24:13 +03:00
parent 9d5424bb9c
commit df137ca5a7
285 changed files with 6911 additions and 2063 deletions
@@ -27,15 +27,28 @@ public class ApproveActivationCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
_currentUser.UserId.Returns(adminId);
_identityService.ActivateUserAsync(request.UserId, adminId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.ActivateUserAsync(request.UserId, adminId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser);
var handler = new ApproveActivationCommandHandler(
dbContext,
_identityService,
_notifier,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new ApproveActivationCommand(request.Id), CancellationToken.None);
var result = await handler.Handle(
new ApproveActivationCommand(request.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(ActivationStatus.Approved, request.Status);
await _notifier.Received(1).NotifyUserActivatedAsync(request.UserId, Arg.Any<CancellationToken>());
await _notifier
.Received(1)
.NotifyUserActivatedAsync(request.UserId, Arg.Any<CancellationToken>());
}
[Fact]
@@ -44,9 +57,18 @@ public class ApproveActivationCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
_currentUser.UserId.Returns(Guid.NewGuid());
var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser);
var handler = new ApproveActivationCommandHandler(
dbContext,
_identityService,
_notifier,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new ApproveActivationCommand(Guid.NewGuid()), CancellationToken.None);
var result = await handler.Handle(
new ApproveActivationCommand(Guid.NewGuid()),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ActivationErrors.NotFound, result.Error);
@@ -63,9 +85,18 @@ public class ApproveActivationCommandHandlerTests
_currentUser.UserId.Returns(Guid.NewGuid());
var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser);
var handler = new ApproveActivationCommandHandler(
dbContext,
_identityService,
_notifier,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new ApproveActivationCommand(request.Id), CancellationToken.None);
var result = await handler.Handle(
new ApproveActivationCommand(request.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ActivationErrors.AlreadyDecided, result.Error);
@@ -82,14 +113,27 @@ public class ApproveActivationCommandHandlerTests
_currentUser.UserId.Returns(adminId);
var failure = Error.NotFound("User.NotFound", "Пользователь не найден.");
_identityService.ActivateUserAsync(request.UserId, adminId, Arg.Any<CancellationToken>()).Returns(Result.Failure(failure));
_identityService
.ActivateUserAsync(request.UserId, adminId, Arg.Any<CancellationToken>())
.Returns(Result.Failure(failure));
var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser);
var handler = new ApproveActivationCommandHandler(
dbContext,
_identityService,
_notifier,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new ApproveActivationCommand(request.Id), CancellationToken.None);
var result = await handler.Handle(
new ApproveActivationCommand(request.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(failure, result.Error);
await _notifier.DidNotReceive().NotifyUserActivatedAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
await _notifier
.DidNotReceive()
.NotifyUserActivatedAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
}
}
@@ -25,7 +25,10 @@ public class RejectActivationCommandHandlerTests
var handler = new RejectActivationCommandHandler(dbContext, _currentUser);
var result = await handler.Handle(new RejectActivationCommand(request.Id, "недостаточно информации"), CancellationToken.None);
var result = await handler.Handle(
new RejectActivationCommand(request.Id, "недостаточно информации"),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(ActivationStatus.Rejected, request.Status);
@@ -40,7 +43,10 @@ public class RejectActivationCommandHandlerTests
var handler = new RejectActivationCommandHandler(dbContext, _currentUser);
var result = await handler.Handle(new RejectActivationCommand(Guid.NewGuid(), null), CancellationToken.None);
var result = await handler.Handle(
new RejectActivationCommand(Guid.NewGuid(), null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ActivationErrors.NotFound, result.Error);
@@ -59,7 +65,10 @@ public class RejectActivationCommandHandlerTests
var handler = new RejectActivationCommandHandler(dbContext, _currentUser);
var result = await handler.Handle(new RejectActivationCommand(request.Id, null), CancellationToken.None);
var result = await handler.Handle(
new RejectActivationCommand(request.Id, null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ActivationErrors.AlreadyDecided, result.Error);
@@ -22,18 +22,40 @@ public class RequestActivationCommandHandlerTests
_currentUser.UserId.Returns(userId);
_currentUser.UserName.Returns("alice");
var handler = new RequestActivationCommandHandler(dbContext, _notifier, _telegramNotifier, _currentUser);
var handler = new RequestActivationCommandHandler(
dbContext,
_notifier,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new RequestActivationCommand("Please activate"), CancellationToken.None);
var result = await handler.Handle(
new RequestActivationCommand("Please activate"),
CancellationToken.None
);
await dbContext.SaveChangesAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal("Please activate", result.Value.Comment);
Assert.Single(dbContext.ActivationRequests);
await _notifier.Received(1).NotifyActivationRequestedAsync(
Arg.Any<Guid>(), userId, "alice", "Please activate", Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>());
await _telegramNotifier.Received(1).NotifyAdminsActivationRequestedAsync(
Arg.Any<Guid>(), "alice", "Please activate", Arg.Any<CancellationToken>());
await _notifier
.Received(1)
.NotifyActivationRequestedAsync(
Arg.Any<Guid>(),
userId,
"alice",
"Please activate",
Arg.Any<DateTimeOffset>(),
Arg.Any<CancellationToken>()
);
await _telegramNotifier
.Received(1)
.NotifyAdminsActivationRequestedAsync(
Arg.Any<Guid>(),
"alice",
"Please activate",
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -47,14 +69,30 @@ public class RequestActivationCommandHandlerTests
_currentUser.UserId.Returns(userId);
_currentUser.UserName.Returns("alice");
var handler = new RequestActivationCommandHandler(dbContext, _notifier, _telegramNotifier, _currentUser);
var handler = new RequestActivationCommandHandler(
dbContext,
_notifier,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new RequestActivationCommand(null), CancellationToken.None);
var result = await handler.Handle(
new RequestActivationCommand(null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ActivationErrors.AlreadyPending, result.Error);
await _notifier.DidNotReceive().NotifyActivationRequestedAsync(
Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>());
await _notifier
.DidNotReceive()
.NotifyActivationRequestedAsync(
Arg.Any<Guid>(),
Arg.Any<Guid>(),
Arg.Any<string>(),
Arg.Any<string?>(),
Arg.Any<DateTimeOffset>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -63,9 +101,17 @@ public class RequestActivationCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
_currentUser.UserId.Returns((Guid?)null);
var handler = new RequestActivationCommandHandler(dbContext, _notifier, _telegramNotifier, _currentUser);
var handler = new RequestActivationCommandHandler(
dbContext,
_notifier,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new RequestActivationCommand(null), CancellationToken.None);
var result = await handler.Handle(
new RequestActivationCommand(null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.Unauthorized, result.Error);
@@ -19,7 +19,12 @@ public class ListAllConfigsQueryHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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);
inbound.Publish("Germany", [], null);
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config");
@@ -29,12 +34,16 @@ public class ListAllConfigsQueryHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetUserNamesAsync(Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<CancellationToken>())
_identityService
.GetUserNamesAsync(Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<CancellationToken>())
.Returns(new Dictionary<Guid, string> { [userId] = "alice" });
var handler = new ListAllConfigsQueryHandler(dbContext, _identityService);
var result = await handler.Handle(new ListAllConfigsQuery(1, 20, Search: null, Status: null), CancellationToken.None);
var result = await handler.Handle(
new ListAllConfigsQuery(1, 20, Search: null, Status: null),
CancellationToken.None
);
Assert.True(result.IsSuccess);
var item = Assert.Single(result.Value.Items);
@@ -52,7 +61,12 @@ public class ListAllConfigsQueryHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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 matching = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "phone-config");
var other = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "laptop-config");
@@ -62,12 +76,16 @@ public class ListAllConfigsQueryHandlerTests
dbContext.VpnConfigs.AddRange(matching, other);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetUserNamesAsync(Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<CancellationToken>())
_identityService
.GetUserNamesAsync(Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<CancellationToken>())
.Returns(new Dictionary<Guid, string> { [userId] = "alice" });
var handler = new ListAllConfigsQueryHandler(dbContext, _identityService);
var result = await handler.Handle(new ListAllConfigsQuery(1, 20, Search: "phone", Status: null), CancellationToken.None);
var result = await handler.Handle(
new ListAllConfigsQuery(1, 20, Search: "phone", Status: null),
CancellationToken.None
);
Assert.True(result.IsSuccess);
var item = Assert.Single(result.Value.Items);
@@ -80,7 +98,12 @@ public class ListAllConfigsQueryHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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 active = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "active-config");
var revoked = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "revoked-config");
@@ -91,12 +114,16 @@ public class ListAllConfigsQueryHandlerTests
dbContext.VpnConfigs.AddRange(active, revoked);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetUserNamesAsync(Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<CancellationToken>())
_identityService
.GetUserNamesAsync(Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<CancellationToken>())
.Returns(new Dictionary<Guid, string> { [userId] = "alice" });
var handler = new ListAllConfigsQueryHandler(dbContext, _identityService);
var result = await handler.Handle(new ListAllConfigsQuery(1, 20, Search: null, Status: ConfigStatus.Revoked), CancellationToken.None);
var result = await handler.Handle(
new ListAllConfigsQuery(1, 20, Search: null, Status: ConfigStatus.Revoked),
CancellationToken.None
);
Assert.True(result.IsSuccess);
var item = Assert.Single(result.Value.Items);
@@ -21,9 +21,20 @@ public class RegisterNodeCommandHandlerTests
_gateway.ValidateBaseAddress(Arg.Any<Uri>()).Returns(Result.Success());
_secretProtector.Protect("secret-password").Returns("protected-secret-password");
var handler = new RegisterNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser);
var handler = new RegisterNodeCommandHandler(
dbContext,
_gateway,
_secretProtector,
_currentUser
);
var command = new RegisterNodeCommand("node-1", "https://node1.example.com", "admin", "secret-password", "eu-west");
var command = new RegisterNodeCommand(
"node-1",
"https://node1.example.com",
"admin",
"secret-password",
"eu-west"
);
var result = await handler.Handle(command, CancellationToken.None);
@@ -31,7 +42,10 @@ public class RegisterNodeCommandHandlerTests
Assert.Equal("node-1", result.Value.Name);
Assert.Equal("admin", result.Value.Username);
Assert.Single(dbContext.Nodes.Local);
Assert.Equal("protected-secret-password", dbContext.Nodes.Local.Single().Credentials.ProtectedPassword);
Assert.Equal(
"protected-secret-password",
dbContext.Nodes.Local.Single().Credentials.ProtectedPassword
);
}
[Fact]
@@ -39,9 +53,20 @@ public class RegisterNodeCommandHandlerTests
{
using var dbContext = InMemoryDbContextFactory.Create();
var handler = new RegisterNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser);
var handler = new RegisterNodeCommandHandler(
dbContext,
_gateway,
_secretProtector,
_currentUser
);
var command = new RegisterNodeCommand("node-1", "not-a-uri", "admin", "secret-password", null);
var command = new RegisterNodeCommand(
"node-1",
"not-a-uri",
"admin",
"secret-password",
null
);
var result = await handler.Handle(command, CancellationToken.None);
@@ -59,9 +84,20 @@ public class RegisterNodeCommandHandlerTests
var error = Error.Validation("Nodes.SchemeNotAllowed", "Разрешён только HTTPS.");
_gateway.ValidateBaseAddress(Arg.Any<Uri>()).Returns(Result.Failure(error));
var handler = new RegisterNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser);
var handler = new RegisterNodeCommandHandler(
dbContext,
_gateway,
_secretProtector,
_currentUser
);
var command = new RegisterNodeCommand("node-1", "http://node1.example.com", "admin", "secret-password", null);
var command = new RegisterNodeCommand(
"node-1",
"http://node1.example.com",
"admin",
"secret-password",
null
);
var result = await handler.Handle(command, CancellationToken.None);
@@ -25,20 +25,38 @@ public class ApproveRoleRequestCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
var newRoleId = Guid.NewGuid();
_roleService.CreateRoleAsync("premium", 10, 5, Arg.Any<CancellationToken>())
_roleService
.CreateRoleAsync("premium", 10, 5, Arg.Any<CancellationToken>())
.Returns(Result.Success(new RoleDto(newRoleId, "premium", 10, 5, false)));
_roleService.ChangeUserRoleAsync(userId, newRoleId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_roleService
.ChangeUserRoleAsync(userId, newRoleId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin");
var handler = new ApproveRoleRequestCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser);
var handler = new ApproveRoleRequestCommandHandler(
dbContext,
_roleService,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None);
var result = await handler.Handle(
new ApproveRoleRequestCommand(ticket.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TicketStatus.Resolved, ticket.Status);
await _roleService.Received(1).CreateRoleAsync("premium", 10, 5, Arg.Any<CancellationToken>());
await _roleService.Received(1).ChangeUserRoleAsync(userId, newRoleId, Arg.Any<CancellationToken>());
await _telegramNotifier.Received(1).NotifyUserAsync(userId, Arg.Any<string>(), Arg.Any<CancellationToken>());
await _roleService
.Received(1)
.CreateRoleAsync("premium", 10, 5, Arg.Any<CancellationToken>());
await _roleService
.Received(1)
.ChangeUserRoleAsync(userId, newRoleId, Arg.Any<CancellationToken>());
await _telegramNotifier
.Received(1)
.NotifyUserAsync(userId, Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Fact]
@@ -51,16 +69,33 @@ public class ApproveRoleRequestCommandHandlerTests
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_roleService
.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
var handler = new ApproveRoleRequestCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser);
var handler = new ApproveRoleRequestCommandHandler(
dbContext,
_roleService,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None);
var result = await handler.Handle(
new ApproveRoleRequestCommand(ticket.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
await _roleService.DidNotReceive().CreateRoleAsync(
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>());
await _roleService
.DidNotReceive()
.CreateRoleAsync(
Arg.Any<string>(),
Arg.Any<int>(),
Arg.Any<int>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -72,9 +107,18 @@ public class ApproveRoleRequestCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
var handler = new ApproveRoleRequestCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser);
var handler = new ApproveRoleRequestCommandHandler(
dbContext,
_roleService,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None);
var result = await handler.Handle(
new ApproveRoleRequestCommand(ticket.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.NotRoleRequest, result.Error);
@@ -18,7 +18,9 @@ public class BlockUserCommandHandlerTests
private readonly IRealtimeNotifier _notifier = Substitute.For<IRealtimeNotifier>();
private readonly ITelegramNotifier _telegramNotifier = Substitute.For<ITelegramNotifier>();
private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
private readonly ILogger<BlockUserCommandHandler> _logger = Substitute.For<ILogger<BlockUserCommandHandler>>();
private readonly ILogger<BlockUserCommandHandler> _logger = Substitute.For<
ILogger<BlockUserCommandHandler>
>();
[Fact]
public async Task Handle_IdentityServiceFails_ReturnsFailureWithoutTouchingConfigs()
@@ -26,17 +28,35 @@ public class BlockUserCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var failure = UserErrors.NotFound;
_identityService.BlockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Failure(failure));
_identityService
.BlockUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Failure(failure));
var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger);
var handler = new BlockUserCommandHandler(
dbContext,
_identityService,
_gateway,
_notifier,
_telegramNotifier,
_currentUser,
_logger
);
var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(failure, result.Error);
await _gateway.DidNotReceive().UpdateClientAsync(
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
await _gateway
.DidNotReceive()
.UpdateClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<string>(),
Arg.Any<bool>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -46,7 +66,12 @@ public class BlockUserCommandHandlerTests
var userId = Guid.NewGuid();
var adminId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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");
@@ -55,24 +80,56 @@ public class BlockUserCommandHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.BlockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.BlockUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
_currentUser.UserId.Returns(adminId);
_gateway.UpdateClientAsync(
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_gateway
.UpdateClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<string>(),
Arg.Any<bool>(),
Arg.Any<CancellationToken>()
)
.Returns(Result.Success());
var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger);
var handler = new BlockUserCommandHandler(
dbContext,
_identityService,
_gateway,
_notifier,
_telegramNotifier,
_currentUser,
_logger
);
var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(ConfigStatus.Disabled, config.Status);
await _gateway.Received(1).UpdateClientAsync(
Arg.Is<Node>(n => n.Id == node.Id), inbound.RemoteInboundId, config.ClientExternalId, config.Protocol,
"my-config", enable: false, Arg.Any<CancellationToken>());
await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Disabled, Arg.Any<CancellationToken>());
await _gateway
.Received(1)
.UpdateClientAsync(
Arg.Is<Node>(n => n.Id == node.Id),
inbound.RemoteInboundId,
config.ClientExternalId,
config.Protocol,
"my-config",
enable: false,
Arg.Any<CancellationToken>()
);
await _notifier
.Received(1)
.NotifyConfigStatusChangedAsync(
userId,
config.Id,
ConfigStatus.Disabled,
Arg.Any<CancellationToken>()
);
var audit = Assert.Single(dbContext.AuditLogs.Local);
Assert.Equal("UserBlocked", audit.Action);
@@ -85,17 +142,35 @@ public class BlockUserCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
_identityService.BlockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.BlockUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
_currentUser.UserId.Returns(Guid.NewGuid());
var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger);
var handler = new BlockUserCommandHandler(
dbContext,
_identityService,
_gateway,
_notifier,
_telegramNotifier,
_currentUser,
_logger
);
var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None);
Assert.True(result.IsSuccess);
await _gateway.DidNotReceive().UpdateClientAsync(
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
await _gateway
.DidNotReceive()
.UpdateClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<string>(),
Arg.Any<bool>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -105,7 +180,12 @@ public class BlockUserCommandHandlerTests
var userId = Guid.NewGuid();
var adminId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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");
@@ -114,20 +194,43 @@ public class BlockUserCommandHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.BlockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.BlockUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
_currentUser.UserId.Returns(adminId);
_gateway.UpdateClientAsync(
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_gateway
.UpdateClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<string>(),
Arg.Any<bool>(),
Arg.Any<CancellationToken>()
)
.Returns(Result.Failure(Error.Failure("Xui.UpdateClientFailed", "Нода недоступна.")));
var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger);
var handler = new BlockUserCommandHandler(
dbContext,
_identityService,
_gateway,
_notifier,
_telegramNotifier,
_currentUser,
_logger
);
var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(ConfigStatus.Active, config.Status);
await _notifier.DidNotReceive().NotifyConfigStatusChangedAsync(
Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<ConfigStatus>(), Arg.Any<CancellationToken>());
await _notifier
.DidNotReceive()
.NotifyConfigStatusChangedAsync(
Arg.Any<Guid>(),
Arg.Any<Guid>(),
Arg.Any<ConfigStatus>(),
Arg.Any<CancellationToken>()
);
}
}
@@ -19,14 +19,21 @@ public class ChangeUserRoleCommandHandlerTests
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
_roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_roleService
.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var handler = new ChangeUserRoleCommandHandler(_roleService, dbContext, _currentUser);
var result = await handler.Handle(new ChangeUserRoleCommand(userId, roleId), CancellationToken.None);
var result = await handler.Handle(
new ChangeUserRoleCommand(userId, roleId),
CancellationToken.None
);
Assert.True(result.IsSuccess);
await _roleService.Received(1).ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>());
await _roleService
.Received(1)
.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>());
Assert.Single(dbContext.AuditLogs.Local);
}
@@ -38,11 +45,16 @@ public class ChangeUserRoleCommandHandlerTests
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var error = UserErrors.NotFound;
_roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>()).Returns(Result.Failure(error));
_roleService
.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>())
.Returns(Result.Failure(error));
var handler = new ChangeUserRoleCommandHandler(_roleService, dbContext, _currentUser);
var result = await handler.Handle(new ChangeUserRoleCommand(userId, roleId), CancellationToken.None);
var result = await handler.Handle(
new ChangeUserRoleCommand(userId, roleId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(error, result.Error);
@@ -24,13 +24,21 @@ public class DeleteUserCommandHandlerTests
var adminId = Guid.NewGuid();
_currentUser.UserId.Returns(adminId);
var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser);
var handler = new DeleteUserCommandHandler(
dbContext,
_identityService,
_gateway,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new DeleteUserCommand(adminId), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(UserErrors.CannotDeleteSelf, result.Error);
await _identityService.DidNotReceive().DeleteUserAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
await _identityService
.DidNotReceive()
.DeleteUserAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
}
[Fact]
@@ -40,7 +48,12 @@ public class DeleteUserCommandHandlerTests
var userId = Guid.NewGuid();
var adminId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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("external-id");
@@ -51,20 +64,44 @@ public class DeleteUserCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
_currentUser.UserId.Returns(adminId);
_gateway.RemoveClientAsync(Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(), Arg.Any<CancellationToken>())
_gateway
.RemoveClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<CancellationToken>()
)
.Returns(Result.Success());
_identityService
.DeleteUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
_identityService.DeleteUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser);
var handler = new DeleteUserCommandHandler(
dbContext,
_identityService,
_gateway,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new DeleteUserCommand(userId), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(ConfigStatus.Revoked, config.Status);
await _gateway.Received(1).RemoveClientAsync(
Arg.Is<Node>(n => n.Id == node.Id), inbound.RemoteInboundId, "external-id", config.Protocol, Arg.Any<CancellationToken>());
await _telegramNotifier.Received(1).NotifyUserAsync(userId, Arg.Any<string>(), Arg.Any<CancellationToken>());
await _gateway
.Received(1)
.RemoveClientAsync(
Arg.Is<Node>(n => n.Id == node.Id),
inbound.RemoteInboundId,
"external-id",
config.Protocol,
Arg.Any<CancellationToken>()
);
await _telegramNotifier
.Received(1)
.NotifyUserAsync(userId, Arg.Any<string>(), Arg.Any<CancellationToken>());
await _identityService.Received(1).DeleteUserAsync(userId, Arg.Any<CancellationToken>());
var audit = Assert.Single(dbContext.AuditLogs.Local);
@@ -79,15 +116,30 @@ public class DeleteUserCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
_currentUser.UserId.Returns(Guid.NewGuid());
_identityService.DeleteUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.DeleteUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser);
var handler = new DeleteUserCommandHandler(
dbContext,
_identityService,
_gateway,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new DeleteUserCommand(userId), CancellationToken.None);
Assert.True(result.IsSuccess);
await _gateway.DidNotReceive().RemoveClientAsync(
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(), Arg.Any<CancellationToken>());
await _gateway
.DidNotReceive()
.RemoveClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -96,9 +148,17 @@ public class DeleteUserCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
_currentUser.UserId.Returns(Guid.NewGuid());
_identityService.DeleteUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Failure(UserErrors.NotFound));
_identityService
.DeleteUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Failure(UserErrors.NotFound));
var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser);
var handler = new DeleteUserCommandHandler(
dbContext,
_identityService,
_gateway,
_telegramNotifier,
_currentUser
);
var result = await handler.Handle(new DeleteUserCommand(userId), CancellationToken.None);
@@ -17,7 +17,9 @@ public class UnblockUserCommandHandlerTests
private readonly IXuiPanelGateway _gateway = Substitute.For<IXuiPanelGateway>();
private readonly IRealtimeNotifier _notifier = Substitute.For<IRealtimeNotifier>();
private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
private readonly ILogger<UnblockUserCommandHandler> _logger = Substitute.For<ILogger<UnblockUserCommandHandler>>();
private readonly ILogger<UnblockUserCommandHandler> _logger = Substitute.For<
ILogger<UnblockUserCommandHandler>
>();
[Fact]
public async Task Handle_WhenUnblockSucceeds_ReEnablesDisabledConfigsAndWritesAudit()
@@ -26,7 +28,12 @@ public class UnblockUserCommandHandlerTests
var userId = Guid.NewGuid();
var adminId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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.Disable();
@@ -37,22 +44,53 @@ public class UnblockUserCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
_currentUser.UserId.Returns(adminId);
_identityService.UnblockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_gateway.UpdateClientAsync(
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_identityService
.UnblockUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
_gateway
.UpdateClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<string>(),
Arg.Any<bool>(),
Arg.Any<CancellationToken>()
)
.Returns(Result.Success());
var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger);
var handler = new UnblockUserCommandHandler(
dbContext,
_identityService,
_gateway,
_notifier,
_currentUser,
_logger
);
var result = await handler.Handle(new UnblockUserCommand(userId), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(ConfigStatus.Active, config.Status);
await _gateway.Received(1).UpdateClientAsync(
node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol,
Arg.Any<string>(), true, Arg.Any<CancellationToken>());
await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Active, Arg.Any<CancellationToken>());
await _gateway
.Received(1)
.UpdateClientAsync(
node,
inbound.RemoteInboundId,
config.ClientExternalId,
config.Protocol,
Arg.Any<string>(),
true,
Arg.Any<CancellationToken>()
);
await _notifier
.Received(1)
.NotifyConfigStatusChangedAsync(
userId,
config.Id,
ConfigStatus.Active,
Arg.Any<CancellationToken>()
);
Assert.Single(dbContext.AuditLogs.Local);
Assert.Equal("UserUnblocked", dbContext.AuditLogs.Local.Single().Action);
}
@@ -64,9 +102,18 @@ public class UnblockUserCommandHandlerTests
var userId = Guid.NewGuid();
var error = UserErrors.NotFound;
_identityService.UnblockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Failure(error));
_identityService
.UnblockUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Failure(error));
var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger);
var handler = new UnblockUserCommandHandler(
dbContext,
_identityService,
_gateway,
_notifier,
_currentUser,
_logger
);
var result = await handler.Handle(new UnblockUserCommand(userId), CancellationToken.None);
@@ -82,7 +129,12 @@ public class UnblockUserCommandHandlerTests
var userId = Guid.NewGuid();
var adminId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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.Disable();
@@ -93,19 +145,41 @@ public class UnblockUserCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
_currentUser.UserId.Returns(adminId);
_identityService.UnblockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_gateway.UpdateClientAsync(
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
_identityService
.UnblockUserAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
_gateway
.UpdateClientAsync(
Arg.Any<Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<string>(),
Arg.Any<bool>(),
Arg.Any<CancellationToken>()
)
.Returns(Result.Failure(Error.Failure("Xui.UpdateClientFailed", "Нода недоступна.")));
var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger);
var handler = new UnblockUserCommandHandler(
dbContext,
_identityService,
_gateway,
_notifier,
_currentUser,
_logger
);
var result = await handler.Handle(new UnblockUserCommand(userId), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(ConfigStatus.Disabled, config.Status);
await _notifier.DidNotReceive().NotifyConfigStatusChangedAsync(
Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<ConfigStatus>(), Arg.Any<CancellationToken>());
await _notifier
.DidNotReceive()
.NotifyConfigStatusChangedAsync(
Arg.Any<Guid>(),
Arg.Any<Guid>(),
Arg.Any<ConfigStatus>(),
Arg.Any<CancellationToken>()
);
}
}
@@ -15,29 +15,50 @@ public class ChangePasswordCommandHandlerTests
[Fact]
public async Task Handle_Unauthenticated_ReturnsUnauthorized()
{
var handler = new ChangePasswordCommandHandler(_identityService, FakeCurrentUser.Anonymous());
var handler = new ChangePasswordCommandHandler(
_identityService,
FakeCurrentUser.Anonymous()
);
var result = await handler.Handle(new ChangePasswordCommand("old", "new"), CancellationToken.None);
var result = await handler.Handle(
new ChangePasswordCommand("old", "new"),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.Unauthorized, result.Error);
await _identityService.DidNotReceive().ChangePasswordAsync(
Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
await _identityService
.DidNotReceive()
.ChangePasswordAsync(
Arg.Any<Guid>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
public async Task Handle_Authenticated_DelegatesToIdentityService()
{
var userId = Guid.NewGuid();
_identityService.ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any<CancellationToken>())
_identityService
.ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any<CancellationToken>())
.Returns(Result.Success());
var handler = new ChangePasswordCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId));
var handler = new ChangePasswordCommandHandler(
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new ChangePasswordCommand("old-pass", "new-pass"), CancellationToken.None);
var result = await handler.Handle(
new ChangePasswordCommand("old-pass", "new-pass"),
CancellationToken.None
);
Assert.True(result.IsSuccess);
await _identityService.Received(1).ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any<CancellationToken>());
await _identityService
.Received(1)
.ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any<CancellationToken>());
}
[Fact]
@@ -45,12 +66,19 @@ public class ChangePasswordCommandHandlerTests
{
var userId = Guid.NewGuid();
var error = Error.Validation("Auth.WrongCurrentPassword", "Текущий пароль неверен.");
_identityService.ChangePasswordAsync(userId, "wrong", "new-pass", Arg.Any<CancellationToken>())
_identityService
.ChangePasswordAsync(userId, "wrong", "new-pass", Arg.Any<CancellationToken>())
.Returns(Result.Failure(error));
var handler = new ChangePasswordCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId));
var handler = new ChangePasswordCommandHandler(
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new ChangePasswordCommand("wrong", "new-pass"), CancellationToken.None);
var result = await handler.Handle(
new ChangePasswordCommand("wrong", "new-pass"),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(error, result.Error);
@@ -27,9 +27,14 @@ public class GetCurrentUserQueryHandlerTests
public async Task Handle_ProfileMissing_ReturnsUnauthorized()
{
var userId = Guid.NewGuid();
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns((CurrentUserProfile?)null);
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns((CurrentUserProfile?)null);
var handler = new GetCurrentUserQueryHandler(_identityService, FakeCurrentUser.Authenticated(userId));
var handler = new GetCurrentUserQueryHandler(
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new GetCurrentUserQuery(), CancellationToken.None);
@@ -41,12 +46,26 @@ public class GetCurrentUserQueryHandlerTests
public async Task Handle_AuthenticatedWithProfile_ReturnsCurrentUserDto()
{
var userId = Guid.NewGuid();
var profile = new CurrentUserProfile(userId, "alice", Guid.NewGuid(), "user", true, false, 3, RoleQuota.Unlimited, "sub-token");
var profile = new CurrentUserProfile(
userId,
"alice",
Guid.NewGuid(),
"user",
true,
false,
3,
RoleQuota.Unlimited,
"sub-token"
);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(profile);
_identityService.GetTelegramLinkInfoAsync(userId, Arg.Any<CancellationToken>())
_identityService
.GetTelegramLinkInfoAsync(userId, Arg.Any<CancellationToken>())
.Returns(new TelegramLinkInfo(true, 42, "alice_tg"));
var handler = new GetCurrentUserQueryHandler(_identityService, FakeCurrentUser.Authenticated(userId, "alice"));
var handler = new GetCurrentUserQueryHandler(
_identityService,
FakeCurrentUser.Authenticated(userId, "alice")
);
var result = await handler.Handle(new GetCurrentUserQuery(), CancellationToken.None);
@@ -11,9 +11,11 @@ public class LoginCommandHandlerTests
{
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
private readonly IJwtTokenService _jwtTokenService = Substitute.For<IJwtTokenService>();
private readonly IRefreshTokenService _refreshTokenService = Substitute.For<IRefreshTokenService>();
private readonly IRefreshTokenService _refreshTokenService =
Substitute.For<IRefreshTokenService>();
private LoginCommandHandler CreateHandler() => new(_identityService, _jwtTokenService, _refreshTokenService);
private LoginCommandHandler CreateHandler() =>
new(_identityService, _jwtTokenService, _refreshTokenService);
[Fact]
public async Task Handle_WithValidCredentials_ReturnsAuthResult()
@@ -21,20 +23,33 @@ public class LoginCommandHandlerTests
var userId = Guid.NewGuid();
var authUser = new AuthenticatedUser(userId, "alice", "user");
var profile = new CurrentUserProfile(
userId, "alice", Guid.NewGuid(), "user", IsActivated: true, IsBlocked: false, MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "sub-token");
userId,
"alice",
Guid.NewGuid(),
"user",
IsActivated: true,
IsBlocked: false,
MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited,
SubscriptionToken: "sub-token"
);
_identityService.ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any<CancellationToken>())
_identityService
.ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any<CancellationToken>())
.Returns(Result.Success(authUser));
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(profile);
_identityService.GetTelegramLinkInfoAsync(userId, Arg.Any<CancellationToken>())
_identityService
.GetTelegramLinkInfoAsync(userId, Arg.Any<CancellationToken>())
.Returns(new TelegramLinkInfo(true, 123456, "alice_tg"));
_jwtTokenService.GenerateAccessToken(Arg.Any<AuthenticatedUser>())
_jwtTokenService
.GenerateAccessToken(Arg.Any<AuthenticatedUser>())
.Returns(("access-token", DateTimeOffset.UtcNow.AddMinutes(15)));
_refreshTokenService.IssueAsync(userId, Arg.Any<CancellationToken>())
_refreshTokenService
.IssueAsync(userId, Arg.Any<CancellationToken>())
.Returns(new IssuedRefreshToken("refresh-token", DateTimeOffset.UtcNow.AddDays(30)));
var result = await CreateHandler().Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None);
var result = await CreateHandler()
.Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal("access-token", result.Value.AccessToken);
@@ -46,14 +61,18 @@ public class LoginCommandHandlerTests
[Fact]
public async Task Handle_WithInvalidCredentials_ReturnsFailureWithoutIssuingTokens()
{
_identityService.ValidateCredentialsAsync("alice", "wrong", Arg.Any<CancellationToken>())
_identityService
.ValidateCredentialsAsync("alice", "wrong", Arg.Any<CancellationToken>())
.Returns(Result.Failure<AuthenticatedUser>(AuthErrors.InvalidCredentials));
var result = await CreateHandler().Handle(new LoginCommand("alice", "wrong"), CancellationToken.None);
var result = await CreateHandler()
.Handle(new LoginCommand("alice", "wrong"), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.InvalidCredentials, result.Error);
await _refreshTokenService.DidNotReceive().IssueAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
await _refreshTokenService
.DidNotReceive()
.IssueAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
}
[Fact]
@@ -62,11 +81,15 @@ public class LoginCommandHandlerTests
var userId = Guid.NewGuid();
var authUser = new AuthenticatedUser(userId, "alice", "user");
_identityService.ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any<CancellationToken>())
_identityService
.ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any<CancellationToken>())
.Returns(Result.Success(authUser));
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns((CurrentUserProfile?)null);
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns((CurrentUserProfile?)null);
var result = await CreateHandler().Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None);
var result = await CreateHandler()
.Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.InvalidCredentials, result.Error);
@@ -11,27 +11,46 @@ public class RefreshCommandHandlerTests
{
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
private readonly IJwtTokenService _jwtTokenService = Substitute.For<IJwtTokenService>();
private readonly IRefreshTokenService _refreshTokenService = Substitute.For<IRefreshTokenService>();
private readonly IRefreshTokenService _refreshTokenService =
Substitute.For<IRefreshTokenService>();
private RefreshCommandHandler CreateHandler() => new(_identityService, _jwtTokenService, _refreshTokenService);
private RefreshCommandHandler CreateHandler() =>
new(_identityService, _jwtTokenService, _refreshTokenService);
[Fact]
public async Task Handle_WithValidToken_RotatesAndReturnsNewAuthResult()
{
var userId = Guid.NewGuid();
var profile = new CurrentUserProfile(
userId, "alice", Guid.NewGuid(), "user", IsActivated: true, IsBlocked: false, MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "sub-token");
var rotated = new RotatedRefreshToken(userId, "new-refresh-token", DateTimeOffset.UtcNow.AddDays(30));
userId,
"alice",
Guid.NewGuid(),
"user",
IsActivated: true,
IsBlocked: false,
MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited,
SubscriptionToken: "sub-token"
);
var rotated = new RotatedRefreshToken(
userId,
"new-refresh-token",
DateTimeOffset.UtcNow.AddDays(30)
);
_refreshTokenService.RotateAsync("old-token", Arg.Any<CancellationToken>()).Returns(Result.Success(rotated));
_refreshTokenService
.RotateAsync("old-token", Arg.Any<CancellationToken>())
.Returns(Result.Success(rotated));
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(profile);
_identityService.GetTelegramLinkInfoAsync(userId, Arg.Any<CancellationToken>())
_identityService
.GetTelegramLinkInfoAsync(userId, Arg.Any<CancellationToken>())
.Returns(new TelegramLinkInfo(false, null, null));
_jwtTokenService.GenerateAccessToken(Arg.Any<AuthenticatedUser>())
_jwtTokenService
.GenerateAccessToken(Arg.Any<AuthenticatedUser>())
.Returns(("new-access-token", DateTimeOffset.UtcNow.AddMinutes(15)));
var result = await CreateHandler().Handle(new RefreshCommand("old-token"), CancellationToken.None);
var result = await CreateHandler()
.Handle(new RefreshCommand("old-token"), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal("new-access-token", result.Value.AccessToken);
@@ -41,26 +60,39 @@ public class RefreshCommandHandlerTests
[Fact]
public async Task Handle_WithInvalidOrReusedToken_ReturnsFailure()
{
_refreshTokenService.RotateAsync("stolen-token", Arg.Any<CancellationToken>())
_refreshTokenService
.RotateAsync("stolen-token", Arg.Any<CancellationToken>())
.Returns(Result.Failure<RotatedRefreshToken>(AuthErrors.InvalidRefreshToken));
var result = await CreateHandler().Handle(new RefreshCommand("stolen-token"), CancellationToken.None);
var result = await CreateHandler()
.Handle(new RefreshCommand("stolen-token"), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.InvalidRefreshToken, result.Error);
await _identityService.DidNotReceive().GetProfileAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
await _identityService
.DidNotReceive()
.GetProfileAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_WhenProfileNoLongerExists_ReturnsInvalidRefreshToken()
{
var userId = Guid.NewGuid();
var rotated = new RotatedRefreshToken(userId, "new-refresh-token", DateTimeOffset.UtcNow.AddDays(30));
var rotated = new RotatedRefreshToken(
userId,
"new-refresh-token",
DateTimeOffset.UtcNow.AddDays(30)
);
_refreshTokenService.RotateAsync("old-token", Arg.Any<CancellationToken>()).Returns(Result.Success(rotated));
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns((CurrentUserProfile?)null);
_refreshTokenService
.RotateAsync("old-token", Arg.Any<CancellationToken>())
.Returns(Result.Success(rotated));
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns((CurrentUserProfile?)null);
var result = await CreateHandler().Handle(new RefreshCommand("old-token"), CancellationToken.None);
var result = await CreateHandler()
.Handle(new RefreshCommand("old-token"), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.InvalidRefreshToken, result.Error);
@@ -15,21 +15,37 @@ public class RequireActivationBehaviorTests
private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
private RequireActivationBehavior<DummyRequest, Result<string>> CreateBehavior() => new(_currentUser, _identityService);
private RequireActivationBehavior<DummyRequest, Result<string>> CreateBehavior() =>
new(_currentUser, _identityService);
private static CurrentUserProfile CreateProfile(Guid userId, bool isActivated) =>
new(userId, "alice", Guid.NewGuid(), "user", IsActivated: isActivated, IsBlocked: false, MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "sub-token");
new(
userId,
"alice",
Guid.NewGuid(),
"user",
IsActivated: isActivated,
IsBlocked: false,
MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited,
SubscriptionToken: "sub-token"
);
[Fact]
public async Task Handle_WhenActivated_CallsNext()
{
var userId = Guid.NewGuid();
_currentUser.UserId.Returns(userId);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(CreateProfile(userId, isActivated: true));
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(CreateProfile(userId, isActivated: true));
var result = await CreateBehavior().Handle(
new DummyRequest(), () => Task.FromResult(Result.Success("ok")), CancellationToken.None);
var result = await CreateBehavior()
.Handle(
new DummyRequest(),
() => Task.FromResult(Result.Success("ok")),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal("ok", result.Value);
@@ -40,17 +56,21 @@ public class RequireActivationBehaviorTests
{
var userId = Guid.NewGuid();
_currentUser.UserId.Returns(userId);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(CreateProfile(userId, isActivated: false));
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(CreateProfile(userId, isActivated: false));
var nextCalled = false;
var result = await CreateBehavior().Handle(
new DummyRequest(),
() =>
{
nextCalled = true;
return Task.FromResult(Result.Success("ok"));
},
CancellationToken.None);
var result = await CreateBehavior()
.Handle(
new DummyRequest(),
() =>
{
nextCalled = true;
return Task.FromResult(Result.Success("ok"));
},
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.NotActivated, result.Error);
@@ -62,8 +82,12 @@ public class RequireActivationBehaviorTests
{
_currentUser.UserId.Returns((Guid?)null);
var result = await CreateBehavior().Handle(
new DummyRequest(), () => Task.FromResult(Result.Success("ok")), CancellationToken.None);
var result = await CreateBehavior()
.Handle(
new DummyRequest(),
() => Task.FromResult(Result.Success("ok")),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.Unauthorized, result.Error);
@@ -74,10 +98,16 @@ public class RequireActivationBehaviorTests
{
var userId = Guid.NewGuid();
_currentUser.UserId.Returns(userId);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns((CurrentUserProfile?)null);
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns((CurrentUserProfile?)null);
var result = await CreateBehavior().Handle(
new DummyRequest(), () => Task.FromResult(Result.Success("ok")), CancellationToken.None);
var result = await CreateBehavior()
.Handle(
new DummyRequest(),
() => Task.FromResult(Result.Success("ok")),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.Unauthorized, result.Error);
@@ -27,16 +27,35 @@ public class GetMyConfigsQueryHandlerTests
var activeConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Active");
var revokedConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Revoked");
revokedConfig.Revoke();
var otherUsersConfig = VpnConfig.Create(otherUserId, inbound.Id, VpnProtocol.Vless, "Other");
var otherUsersConfig = VpnConfig.Create(
otherUserId,
inbound.Id,
VpnProtocol.Vless,
"Other"
);
dbContext.Inbounds.Add(inbound);
dbContext.VpnConfigs.AddRange(activeConfig, revokedConfig, otherUsersConfig);
await dbContext.SaveChangesAsync(CancellationToken.None);
var profile = new CurrentUserProfile(userId, "alice", Guid.NewGuid(), "user", true, false, 5, RoleQuota.Unlimited, "sub-token");
var profile = new CurrentUserProfile(
userId,
"alice",
Guid.NewGuid(),
"user",
true,
false,
5,
RoleQuota.Unlimited,
"sub-token"
);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(profile);
var handler = new GetMyConfigsQueryHandler(dbContext, _identityService, FakeCurrentUser.Authenticated(userId));
var handler = new GetMyConfigsQueryHandler(
dbContext,
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new GetMyConfigsQuery(), CancellationToken.None);
@@ -51,7 +70,11 @@ public class GetMyConfigsQueryHandlerTests
{
using var dbContext = InMemoryDbContextFactory.Create();
var handler = new GetMyConfigsQueryHandler(dbContext, _identityService, FakeCurrentUser.Anonymous());
var handler = new GetMyConfigsQueryHandler(
dbContext,
_identityService,
FakeCurrentUser.Anonymous()
);
var result = await handler.Handle(new GetMyConfigsQuery(), CancellationToken.None);
@@ -64,9 +87,15 @@ public class GetMyConfigsQueryHandlerTests
{
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns((CurrentUserProfile?)null);
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns((CurrentUserProfile?)null);
var handler = new GetMyConfigsQueryHandler(dbContext, _identityService, FakeCurrentUser.Authenticated(userId));
var handler = new GetMyConfigsQueryHandler(
dbContext,
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new GetMyConfigsQuery(), CancellationToken.None);
@@ -21,7 +21,12 @@ public class RevokeVpnConfigCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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("external-id");
@@ -31,15 +36,37 @@ public class RevokeVpnConfigCommandHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId));
var handler = new RevokeVpnConfigCommandHandler(
dbContext,
_gateway,
_notifier,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new RevokeVpnConfigCommand(config.Id), CancellationToken.None);
var result = await handler.Handle(
new RevokeVpnConfigCommand(config.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(ConfigStatus.Revoked, config.Status);
await _gateway.Received(1).RemoveClientAsync(
Arg.Any<Node>(), inbound.RemoteInboundId, "external-id", config.Protocol, Arg.Any<CancellationToken>());
await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Revoked, Arg.Any<CancellationToken>());
await _gateway
.Received(1)
.RemoveClientAsync(
Arg.Any<Node>(),
inbound.RemoteInboundId,
"external-id",
config.Protocol,
Arg.Any<CancellationToken>()
);
await _notifier
.Received(1)
.NotifyConfigStatusChangedAsync(
userId,
config.Id,
ConfigStatus.Revoked,
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -56,13 +83,28 @@ public class RevokeVpnConfigCommandHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId));
var handler = new RevokeVpnConfigCommandHandler(
dbContext,
_gateway,
_notifier,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new RevokeVpnConfigCommand(config.Id), CancellationToken.None);
var result = await handler.Handle(
new RevokeVpnConfigCommand(config.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
await _gateway.DidNotReceive().RemoveClientAsync(
Arg.Any<PnvPanel.Domain.Nodes.Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(), Arg.Any<CancellationToken>());
await _gateway
.DidNotReceive()
.RemoveClientAsync(
Arg.Any<PnvPanel.Domain.Nodes.Node>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<VpnProtocol>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -71,9 +113,17 @@ public class RevokeVpnConfigCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId));
var handler = new RevokeVpnConfigCommandHandler(
dbContext,
_gateway,
_notifier,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new RevokeVpnConfigCommand(Guid.NewGuid()), CancellationToken.None);
var result = await handler.Handle(
new RevokeVpnConfigCommand(Guid.NewGuid()),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ConfigErrors.NotFound, result.Error);
@@ -84,9 +134,17 @@ public class RevokeVpnConfigCommandHandlerTests
{
using var dbContext = InMemoryDbContextFactory.Create();
var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Anonymous());
var handler = new RevokeVpnConfigCommandHandler(
dbContext,
_gateway,
_notifier,
FakeCurrentUser.Anonymous()
);
var result = await handler.Handle(new RevokeVpnConfigCommand(Guid.NewGuid()), CancellationToken.None);
var result = await handler.Handle(
new RevokeVpnConfigCommand(Guid.NewGuid()),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(PnvPanel.Application.Auth.AuthErrors.Unauthorized, result.Error);
@@ -18,16 +18,33 @@ public class RotateVpnConfigCommandHandlerTests
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
private static CurrentUserProfile MakeProfile(Guid userId) =>
new(userId, "alice", Guid.NewGuid(), "user", true, false, 3, RoleQuota.Unlimited, "sub-token");
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<CancellationToken>()).Returns(MakeProfile(userId));
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(MakeProfile(userId));
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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");
@@ -37,19 +54,41 @@ public class RotateVpnConfigCommandHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
_gateway.AddClientAsync(
Arg.Any<Node>(), inbound.RemoteInboundId, config.Protocol, Arg.Any<string>(),
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
_gateway
.AddClientAsync(
Arg.Any<Node>(),
inbound.RemoteInboundId,
config.Protocol,
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<int>(),
Arg.Any<CancellationToken>()
)
.Returns(Result.Success("new-external-id"));
var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId));
var handler = new RotateVpnConfigCommandHandler(
dbContext,
_gateway,
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None);
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<Node>(), inbound.RemoteInboundId, "old-external-id", config.Protocol, Arg.Any<CancellationToken>());
await _gateway
.Received(1)
.RemoveClientAsync(
Arg.Any<Node>(),
inbound.RemoteInboundId,
"old-external-id",
config.Protocol,
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -58,9 +97,17 @@ public class RotateVpnConfigCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId));
var handler = new RotateVpnConfigCommandHandler(
dbContext,
_gateway,
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new RotateVpnConfigCommand(Guid.NewGuid()), CancellationToken.None);
var result = await handler.Handle(
new RotateVpnConfigCommand(Guid.NewGuid()),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ConfigErrors.NotFound, result.Error);
@@ -80,9 +127,17 @@ public class RotateVpnConfigCommandHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(otherUserId));
var handler = new RotateVpnConfigCommandHandler(
dbContext,
_gateway,
_identityService,
FakeCurrentUser.Authenticated(otherUserId)
);
var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None);
var result = await handler.Handle(
new RotateVpnConfigCommand(config.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ConfigErrors.NotFound, result.Error);
@@ -102,9 +157,17 @@ public class RotateVpnConfigCommandHandlerTests
dbContext.VpnConfigs.Add(config);
await dbContext.SaveChangesAsync(CancellationToken.None);
var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId));
var handler = new RotateVpnConfigCommandHandler(
dbContext,
_gateway,
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None);
var result = await handler.Handle(
new RotateVpnConfigCommand(config.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(ConfigErrors.NotFound, result.Error);
@@ -115,9 +178,16 @@ public class RotateVpnConfigCommandHandlerTests
{
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(MakeProfile(userId));
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(MakeProfile(userId));
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
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");
@@ -128,14 +198,29 @@ public class RotateVpnConfigCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
var gatewayError = Error.Failure("Xui.Unreachable", "Панель недоступна.");
_gateway.AddClientAsync(
Arg.Any<Node>(), inbound.RemoteInboundId, config.Protocol, Arg.Any<string>(),
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
_gateway
.AddClientAsync(
Arg.Any<Node>(),
inbound.RemoteInboundId,
config.Protocol,
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<int>(),
Arg.Any<CancellationToken>()
)
.Returns(Result.Failure<string>(gatewayError));
var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId));
var handler = new RotateVpnConfigCommandHandler(
dbContext,
_gateway,
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None);
var result = await handler.Handle(
new RotateVpnConfigCommand(config.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(gatewayError, result.Error);
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
@@ -24,5 +23,4 @@
<ProjectReference Include="..\..\src\PnvPanel.Application\PnvPanel.Application.csproj" />
<ProjectReference Include="..\..\src\PnvPanel.Infrastructure\PnvPanel.Infrastructure.csproj" />
</ItemGroup>
</Project>
@@ -16,8 +16,17 @@ public class AddTicketCommentCommandHandlerTests
private readonly IRealtimeNotifier _notifier = Substitute.For<IRealtimeNotifier>();
private static CurrentUserProfile Profile(Guid userId, string role) =>
new(userId, "user", Guid.NewGuid(), role, IsActivated: true, IsBlocked: false, MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "token");
new(
userId,
"user",
Guid.NewGuid(),
role,
IsActivated: true,
IsBlocked: false,
MaxConfigs: 3,
MaxIpLimit: RoleQuota.Unlimited,
SubscriptionToken: "token"
);
[Fact]
public async Task Handle_WhenOwnerComments_AddsCommentAndDoesNotNotifySelf()
@@ -28,16 +37,33 @@ public class AddTicketCommentCommandHandlerTests
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(Profile(userId, "user"));
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(Profile(userId, "user"));
var currentUser = FakeCurrentUser.Authenticated(userId, "alice");
var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser);
var handler = new AddTicketCommentCommandHandler(
dbContext,
_identityService,
_fileStorage,
_notifier,
currentUser
);
var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "апдейт", []), CancellationToken.None);
var result = await handler.Handle(
new AddTicketCommentCommand(ticket.Id, "апдейт", []),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal("апдейт", result.Value.Body);
await _notifier.DidNotReceive().NotifyTicketUpdatedAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<CancellationToken>());
await _notifier
.DidNotReceive()
.NotifyTicketUpdatedAsync(
Arg.Any<Guid>(),
Arg.Any<Guid>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -50,15 +76,28 @@ public class AddTicketCommentCommandHandlerTests
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetProfileAsync(adminId, Arg.Any<CancellationToken>()).Returns(Profile(adminId, "admin"));
_identityService
.GetProfileAsync(adminId, Arg.Any<CancellationToken>())
.Returns(Profile(adminId, "admin"));
var currentUser = FakeCurrentUser.Authenticated(adminId, "admin");
var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser);
var handler = new AddTicketCommentCommandHandler(
dbContext,
_identityService,
_fileStorage,
_notifier,
currentUser
);
var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "ответ админа", []), CancellationToken.None);
var result = await handler.Handle(
new AddTicketCommentCommand(ticket.Id, "ответ админа", []),
CancellationToken.None
);
Assert.True(result.IsSuccess);
await _notifier.Received(1).NotifyTicketUpdatedAsync(ticket.Id, ownerId, Arg.Any<CancellationToken>());
await _notifier
.Received(1)
.NotifyTicketUpdatedAsync(ticket.Id, ownerId, Arg.Any<CancellationToken>());
}
[Fact]
@@ -71,12 +110,23 @@ public class AddTicketCommentCommandHandlerTests
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetProfileAsync(strangerId, Arg.Any<CancellationToken>()).Returns(Profile(strangerId, "user"));
_identityService
.GetProfileAsync(strangerId, Arg.Any<CancellationToken>())
.Returns(Profile(strangerId, "user"));
var currentUser = FakeCurrentUser.Authenticated(strangerId);
var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser);
var handler = new AddTicketCommentCommandHandler(
dbContext,
_identityService,
_fileStorage,
_notifier,
currentUser
);
var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "текст", []), CancellationToken.None);
var result = await handler.Handle(
new AddTicketCommentCommand(ticket.Id, "текст", []),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.NotFound, result.Error);
@@ -92,12 +142,23 @@ public class AddTicketCommentCommandHandlerTests
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(Profile(userId, "user"));
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(Profile(userId, "user"));
var currentUser = FakeCurrentUser.Authenticated(userId);
var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser);
var handler = new AddTicketCommentCommandHandler(
dbContext,
_identityService,
_fileStorage,
_notifier,
currentUser
);
var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "текст", []), CancellationToken.None);
var result = await handler.Handle(
new AddTicketCommentCommand(ticket.Id, "текст", []),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.TicketClosed, result.Error);
@@ -21,9 +21,18 @@ public class CreateBugReportTicketCommandHandlerTests
var userId = Guid.NewGuid();
var currentUser = FakeCurrentUser.Authenticated(userId, "alice");
var handler = new CreateBugReportTicketCommandHandler(dbContext, _fileStorage, _notifier, _telegramNotifier, currentUser);
var handler = new CreateBugReportTicketCommandHandler(
dbContext,
_fileStorage,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(new CreateBugReportTicketCommand("Что-то сломалось", []), CancellationToken.None);
var result = await handler.Handle(
new CreateBugReportTicketCommand("Что-то сломалось", []),
CancellationToken.None
);
await dbContext.SaveChangesAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
@@ -33,8 +42,14 @@ public class CreateBugReportTicketCommandHandlerTests
Assert.Equal("Что-то сломалось", result.Value.Comments[0].Body);
Assert.Single(dbContext.SupportTickets);
Assert.Single(dbContext.TicketComments);
await _telegramNotifier.Received(1).NotifyAdminsBugReportCreatedAsync(
Arg.Any<Guid>(), "alice", "Что-то сломалось", Arg.Any<CancellationToken>());
await _telegramNotifier
.Received(1)
.NotifyAdminsBugReportCreatedAsync(
Arg.Any<Guid>(),
"alice",
"Что-то сломалось",
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -42,13 +57,23 @@ public class CreateBugReportTicketCommandHandlerTests
{
using var dbContext = InMemoryDbContextFactory.Create();
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
var attachments = Enumerable.Range(0, 6)
var attachments = Enumerable
.Range(0, 6)
.Select(_ => new TicketAttachmentUpload(new MemoryStream(), "a.png", "image/png", 10))
.ToList();
var handler = new CreateBugReportTicketCommandHandler(dbContext, _fileStorage, _notifier, _telegramNotifier, currentUser);
var handler = new CreateBugReportTicketCommandHandler(
dbContext,
_fileStorage,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(new CreateBugReportTicketCommand("текст", attachments), CancellationToken.None);
var result = await handler.Handle(
new CreateBugReportTicketCommand("текст", attachments),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.TooManyAttachments, result.Error);
@@ -59,11 +84,23 @@ public class CreateBugReportTicketCommandHandlerTests
{
using var dbContext = InMemoryDbContextFactory.Create();
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
var attachments = new[] { new TicketAttachmentUpload(new MemoryStream(), "a.exe", "application/x-msdownload", 10) };
var attachments = new[]
{
new TicketAttachmentUpload(new MemoryStream(), "a.exe", "application/x-msdownload", 10),
};
var handler = new CreateBugReportTicketCommandHandler(dbContext, _fileStorage, _notifier, _telegramNotifier, currentUser);
var handler = new CreateBugReportTicketCommandHandler(
dbContext,
_fileStorage,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(new CreateBugReportTicketCommand("текст", attachments), CancellationToken.None);
var result = await handler.Handle(
new CreateBugReportTicketCommand("текст", attachments),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.UnsupportedAttachmentType, result.Error);
@@ -21,20 +21,36 @@ public class CreateRoleRequestTicketCommandHandlerTests
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var currentUser = FakeCurrentUser.Authenticated(userId, "alice");
_roleService.ListRolesAsync(Arg.Any<CancellationToken>())
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(new List<RoleDto> { new(roleId, "premium", 5, 2, false) });
var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser);
var handler = new CreateRoleRequestTicketCommandHandler(
dbContext,
_roleService,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(
new CreateRoleRequestTicketCommand(roleId, null, null, null, "нужно больше конфигов"), CancellationToken.None);
new CreateRoleRequestTicketCommand(roleId, null, null, null, "нужно больше конфигов"),
CancellationToken.None
);
await dbContext.SaveChangesAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(roleId, result.Value.RequestedRoleId);
Assert.Equal("premium", result.Value.RequestedRoleName);
await _telegramNotifier.Received(1).NotifyAdminsRoleRequestCreatedAsync(
Arg.Any<Guid>(), "alice", "premium", "нужно больше конфигов", Arg.Any<CancellationToken>());
await _telegramNotifier
.Received(1)
.NotifyAdminsRoleRequestCreatedAsync(
Arg.Any<Guid>(),
"alice",
"premium",
"нужно больше конфигов",
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -43,13 +59,22 @@ public class CreateRoleRequestTicketCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var roleId = Guid.NewGuid();
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
_roleService.ListRolesAsync(Arg.Any<CancellationToken>())
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(new List<RoleDto> { new(roleId, "admin", -1, -1, true) });
var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser);
var handler = new CreateRoleRequestTicketCommandHandler(
dbContext,
_roleService,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(
new CreateRoleRequestTicketCommand(roleId, null, null, null, "хочу быть админом"), CancellationToken.None);
new CreateRoleRequestTicketCommand(roleId, null, null, null, "хочу быть админом"),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.CannotRequestAdminRole, result.Error);
@@ -61,10 +86,18 @@ public class CreateRoleRequestTicketCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid(), "bob");
var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser);
var handler = new CreateRoleRequestTicketCommandHandler(
dbContext,
_roleService,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(
new CreateRoleRequestTicketCommand(null, "custom", 10, 4, "нужна кастомная роль"), CancellationToken.None);
new CreateRoleRequestTicketCommand(null, "custom", 10, 4, "нужна кастомная роль"),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal("custom", result.Value.ProposedRoleName);
@@ -81,10 +114,18 @@ public class CreateRoleRequestTicketCommandHandlerTests
await dbContext.SaveChangesAsync(CancellationToken.None);
var currentUser = FakeCurrentUser.Authenticated(userId);
var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser);
var handler = new CreateRoleRequestTicketCommandHandler(
dbContext,
_roleService,
_notifier,
_telegramNotifier,
currentUser
);
var result = await handler.Handle(
new CreateRoleRequestTicketCommand(null, "y", 2, 2, "ещё заявка"), CancellationToken.None);
new CreateRoleRequestTicketCommand(null, "y", 2, 2, "ещё заявка"),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.RoleRequestAlreadyPending, result.Error);
@@ -25,12 +25,21 @@ public class ReopenTicketCommandHandlerTests
var currentUser = FakeCurrentUser.Authenticated(userId);
var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser);
var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None);
var result = await handler.Handle(
new ReopenTicketCommand(ticket.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TicketStatus.Open, ticket.Status);
await _telegramNotifier.Received(1).NotifyAdminsTicketReopenedAsync(
ticket.Id, Arg.Any<string>(), TicketType.BugReport, Arg.Any<CancellationToken>());
await _telegramNotifier
.Received(1)
.NotifyAdminsTicketReopenedAsync(
ticket.Id,
Arg.Any<string>(),
TicketType.BugReport,
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -45,7 +54,10 @@ public class ReopenTicketCommandHandlerTests
var currentUser = FakeCurrentUser.Authenticated(userId);
var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser);
var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None);
var result = await handler.Handle(
new ReopenTicketCommand(ticket.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.NotResolved, result.Error);
@@ -63,7 +75,10 @@ public class ReopenTicketCommandHandlerTests
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser);
var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None);
var result = await handler.Handle(
new ReopenTicketCommand(ticket.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.NotFound, result.Error);
@@ -23,11 +23,16 @@ public class ApproveTelegramLoginCommandHandlerTests
dbContext.TelegramLoginRequests.Add(request);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>()).Returns(userId);
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns(userId);
var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new ApproveTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new ApproveTelegramLoginCommand(request.Id, telegramUserId),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TelegramLoginStatus.Approved, request.Status);
@@ -40,12 +45,16 @@ public class ApproveTelegramLoginCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
const long telegramUserId = 123456L;
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns((Guid?)null);
var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramErrors.NotLinked, result.Error);
@@ -58,11 +67,16 @@ public class ApproveTelegramLoginCommandHandlerTests
var userId = Guid.NewGuid();
const long telegramUserId = 123456L;
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>()).Returns(userId);
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns(userId);
var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramErrors.LoginRequestNotFound, result.Error);
@@ -80,11 +94,16 @@ public class ApproveTelegramLoginCommandHandlerTests
dbContext.TelegramLoginRequests.Add(request);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>()).Returns(userId);
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns(userId);
var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new ApproveTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new ApproveTelegramLoginCommand(request.Id, telegramUserId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal("Telegram.LoginRequestInvalid", result.Error.Code);
@@ -101,11 +120,16 @@ public class ApproveTelegramLoginCommandHandlerTests
dbContext.TelegramLoginRequests.Add(request);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>()).Returns(userId);
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns(userId);
var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new ApproveTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new ApproveTelegramLoginCommand(request.Id, telegramUserId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal("Telegram.LoginRequestInvalid", result.Error.Code);
@@ -22,12 +22,16 @@ public class LinkTelegramCommandHandlerTests
dbContext.TelegramLinkTokens.Add(token);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.LinkTelegramAsync(userId, 123456L, "alice_tg", Arg.Any<CancellationToken>())
_identityService
.LinkTelegramAsync(userId, 123456L, "alice_tg", Arg.Any<CancellationToken>())
.Returns(Result.Success());
var handler = new LinkTelegramCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, "alice_tg"), CancellationToken.None);
var result = await handler.Handle(
new LinkTelegramCommand(token.Token, 123456L, "alice_tg"),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(userId, result.Value);
@@ -41,7 +45,10 @@ public class LinkTelegramCommandHandlerTests
var handler = new LinkTelegramCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new LinkTelegramCommand("missing-token", 123456L, null), CancellationToken.None);
var result = await handler.Handle(
new LinkTelegramCommand("missing-token", 123456L, null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramErrors.LinkTokenNotFound, result.Error);
@@ -59,12 +66,21 @@ public class LinkTelegramCommandHandlerTests
var handler = new LinkTelegramCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, null), CancellationToken.None);
var result = await handler.Handle(
new LinkTelegramCommand(token.Token, 123456L, null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramErrors.LinkTokenNotFound, result.Error);
await _identityService.DidNotReceive().LinkTelegramAsync(
Arg.Any<Guid>(), Arg.Any<long>(), Arg.Any<string?>(), Arg.Any<CancellationToken>());
await _identityService
.DidNotReceive()
.LinkTelegramAsync(
Arg.Any<Guid>(),
Arg.Any<long>(),
Arg.Any<string?>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
@@ -78,7 +94,10 @@ public class LinkTelegramCommandHandlerTests
var handler = new LinkTelegramCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, null), CancellationToken.None);
var result = await handler.Handle(
new LinkTelegramCommand(token.Token, 123456L, null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramErrors.LinkTokenNotFound, result.Error);
@@ -93,13 +112,20 @@ public class LinkTelegramCommandHandlerTests
dbContext.TelegramLinkTokens.Add(token);
await dbContext.SaveChangesAsync(CancellationToken.None);
var error = Error.Conflict("Telegram.AlreadyLinked", "Этот Telegram уже привязан к другому аккаунту.");
_identityService.LinkTelegramAsync(userId, 123456L, null, Arg.Any<CancellationToken>())
var error = Error.Conflict(
"Telegram.AlreadyLinked",
"Этот Telegram уже привязан к другому аккаунту."
);
_identityService
.LinkTelegramAsync(userId, 123456L, null, Arg.Any<CancellationToken>())
.Returns(Result.Failure(error));
var handler = new LinkTelegramCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, null), CancellationToken.None);
var result = await handler.Handle(
new LinkTelegramCommand(token.Token, 123456L, null),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(error, result.Error);
@@ -23,11 +23,16 @@ public class RejectTelegramLoginCommandHandlerTests
dbContext.TelegramLoginRequests.Add(request);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>()).Returns(userId);
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns(userId);
var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new RejectTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new RejectTelegramLoginCommand(request.Id, telegramUserId),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TelegramLoginStatus.Rejected, request.Status);
@@ -39,12 +44,16 @@ public class RejectTelegramLoginCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
const long telegramUserId = 123456L;
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns((Guid?)null);
var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramErrors.NotLinked, result.Error);
@@ -57,11 +66,16 @@ public class RejectTelegramLoginCommandHandlerTests
var userId = Guid.NewGuid();
const long telegramUserId = 123456L;
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>()).Returns(userId);
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns(userId);
var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramErrors.LoginRequestNotFound, result.Error);
@@ -79,11 +93,16 @@ public class RejectTelegramLoginCommandHandlerTests
dbContext.TelegramLoginRequests.Add(request);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>()).Returns(userId);
_identityService
.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any<CancellationToken>())
.Returns(userId);
var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService);
var result = await handler.Handle(new RejectTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None);
var result = await handler.Handle(
new RejectTelegramLoginCommand(request.Id, telegramUserId),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal("Telegram.LoginRequestInvalid", result.Error.Code);
@@ -13,7 +13,10 @@ public class CreateLinkTokenCommandHandlerTests
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var handler = new CreateLinkTokenCommandHandler(dbContext, FakeCurrentUser.Authenticated(userId));
var handler = new CreateLinkTokenCommandHandler(
dbContext,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new CreateLinkTokenCommand(), CancellationToken.None);
@@ -13,10 +13,12 @@ public class GetLoginRequestStatusQueryHandlerTests
{
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
private readonly IJwtTokenService _jwtTokenService = Substitute.For<IJwtTokenService>();
private readonly IRefreshTokenService _refreshTokenService = Substitute.For<IRefreshTokenService>();
private readonly IRefreshTokenService _refreshTokenService =
Substitute.For<IRefreshTokenService>();
private TelegramNs.GetLoginRequestStatusQueryHandler CreateHandler(PnvPanel.Infrastructure.Persistence.AppDbContext dbContext)
=> new(dbContext, _identityService, _jwtTokenService, _refreshTokenService);
private TelegramNs.GetLoginRequestStatusQueryHandler CreateHandler(
PnvPanel.Infrastructure.Persistence.AppDbContext dbContext
) => new(dbContext, _identityService, _jwtTokenService, _refreshTokenService);
[Fact]
public async Task Handle_WhenRequestNotFound_ReturnsNotFound()
@@ -25,7 +27,10 @@ public class GetLoginRequestStatusQueryHandlerTests
var handler = CreateHandler(dbContext);
var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(Guid.NewGuid()), CancellationToken.None);
var result = await handler.Handle(
new TelegramNs.GetLoginRequestStatusQuery(Guid.NewGuid()),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(TelegramNs.TelegramErrors.LoginRequestNotFound, result.Error);
@@ -41,12 +46,17 @@ public class GetLoginRequestStatusQueryHandlerTests
var handler = CreateHandler(dbContext);
var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None);
var result = await handler.Handle(
new TelegramNs.GetLoginRequestStatusQuery(request.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TelegramLoginStatus.Expired, result.Value.Status);
Assert.Null(result.Value.Auth);
await _refreshTokenService.DidNotReceive().IssueAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
await _refreshTokenService
.DidNotReceive()
.IssueAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
}
[Fact]
@@ -59,7 +69,10 @@ public class GetLoginRequestStatusQueryHandlerTests
var handler = CreateHandler(dbContext);
var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None);
var result = await handler.Handle(
new TelegramNs.GetLoginRequestStatusQuery(request.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TelegramLoginStatus.Pending, result.Value.Status);
@@ -76,16 +89,31 @@ public class GetLoginRequestStatusQueryHandlerTests
dbContext.TelegramLoginRequests.Add(request);
await dbContext.SaveChangesAsync(CancellationToken.None);
var profile = new CurrentUserProfile(userId, "alice", Guid.NewGuid(), "user", true, false, 3, RoleQuota.Unlimited, "sub-token");
var profile = new CurrentUserProfile(
userId,
"alice",
Guid.NewGuid(),
"user",
true,
false,
3,
RoleQuota.Unlimited,
"sub-token"
);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns(profile);
_jwtTokenService.GenerateAccessToken(Arg.Any<AuthenticatedUser>())
_jwtTokenService
.GenerateAccessToken(Arg.Any<AuthenticatedUser>())
.Returns(("access-token", DateTimeOffset.UtcNow.AddMinutes(15)));
_refreshTokenService.IssueAsync(userId, Arg.Any<CancellationToken>())
_refreshTokenService
.IssueAsync(userId, Arg.Any<CancellationToken>())
.Returns(new IssuedRefreshToken("refresh-token", DateTimeOffset.UtcNow.AddDays(30)));
var handler = CreateHandler(dbContext);
var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None);
var result = await handler.Handle(
new TelegramNs.GetLoginRequestStatusQuery(request.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TelegramLoginStatus.Approved, result.Value.Status);
@@ -105,11 +133,16 @@ public class GetLoginRequestStatusQueryHandlerTests
dbContext.TelegramLoginRequests.Add(request);
await dbContext.SaveChangesAsync(CancellationToken.None);
_identityService.GetProfileAsync(userId, Arg.Any<CancellationToken>()).Returns((CurrentUserProfile?)null);
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns((CurrentUserProfile?)null);
var handler = CreateHandler(dbContext);
var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None);
var result = await handler.Handle(
new TelegramNs.GetLoginRequestStatusQuery(request.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.Unauthorized, result.Error);
@@ -16,26 +16,38 @@ public class UnlinkTelegramCommandHandlerTests
public async Task Handle_WhenAuthenticated_DelegatesToIdentityService()
{
var userId = Guid.NewGuid();
_identityService.UnlinkTelegramAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.UnlinkTelegramAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var handler = new UnlinkTelegramCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId));
var handler = new UnlinkTelegramCommandHandler(
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new UnlinkTelegramCommand(), CancellationToken.None);
Assert.True(result.IsSuccess);
await _identityService.Received(1).UnlinkTelegramAsync(userId, Arg.Any<CancellationToken>());
await _identityService
.Received(1)
.UnlinkTelegramAsync(userId, Arg.Any<CancellationToken>());
}
[Fact]
public async Task Handle_WhenNotAuthenticated_ReturnsUnauthorized()
{
var handler = new UnlinkTelegramCommandHandler(_identityService, FakeCurrentUser.Anonymous());
var handler = new UnlinkTelegramCommandHandler(
_identityService,
FakeCurrentUser.Anonymous()
);
var result = await handler.Handle(new UnlinkTelegramCommand(), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Equal(AuthErrors.Unauthorized, result.Error);
await _identityService.DidNotReceive().UnlinkTelegramAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
await _identityService
.DidNotReceive()
.UnlinkTelegramAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>());
}
[Fact]
@@ -43,9 +55,14 @@ public class UnlinkTelegramCommandHandlerTests
{
var userId = Guid.NewGuid();
var error = TelegramErrors.NotLinked;
_identityService.UnlinkTelegramAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Failure(error));
_identityService
.UnlinkTelegramAsync(userId, Arg.Any<CancellationToken>())
.Returns(Result.Failure(error));
var handler = new UnlinkTelegramCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId));
var handler = new UnlinkTelegramCommandHandler(
_identityService,
FakeCurrentUser.Authenticated(userId)
);
var result = await handler.Handle(new UnlinkTelegramCommand(), CancellationToken.None);
@@ -10,8 +10,8 @@ public sealed class FakeCurrentUser : ICurrentUser
public bool IsAuthenticated => UserId is not null;
public static FakeCurrentUser Authenticated(Guid userId, string userName = "testuser")
=> new() { UserId = userId, UserName = userName };
public static FakeCurrentUser Authenticated(Guid userId, string userName = "testuser") =>
new() { UserId = userId, UserName = userName };
public static FakeCurrentUser Anonymous() => new();
}
@@ -8,7 +8,14 @@ public class ClientAppTests
[Fact]
public void Create_SetsEnabledByDefault()
{
var app = ClientApp.Create("v2rayNG", new Uri("https://play.google.com/store/apps/details?id=x"), OsPlatform.Android, "desc", null, 1);
var app = ClientApp.Create(
"v2rayNG",
new Uri("https://play.google.com/store/apps/details?id=x"),
OsPlatform.Android,
"desc",
null,
1
);
Assert.Equal("v2rayNG", app.Name);
Assert.Equal(OsPlatform.Android, app.OperatingSystem);
@@ -19,9 +26,24 @@ public class ClientAppTests
[Fact]
public void Update_ReplacesAllMutableFields()
{
var app = ClientApp.Create("Old", new Uri("https://old.example.com"), OsPlatform.IOS, "old", "old-icon", 1);
var app = ClientApp.Create(
"Old",
new Uri("https://old.example.com"),
OsPlatform.IOS,
"old",
"old-icon",
1
);
app.Update("New", new Uri("https://new.example.com"), OsPlatform.MacOS, "new", "new-icon", 2, isEnabled: false);
app.Update(
"New",
new Uri("https://new.example.com"),
OsPlatform.MacOS,
"new",
"new-icon",
2,
isEnabled: false
);
Assert.Equal("New", app.Name);
Assert.Equal(new Uri("https://new.example.com"), app.DownloadUrl);
@@ -10,7 +10,14 @@ public class AuditLogTests
{
var actorId = Guid.NewGuid();
var log = AuditLog.Create(actorId, "user.blocked", "AppUser", actorId.ToString(), "{\"reason\":\"abuse\"}", AuditSource.Web);
var log = AuditLog.Create(
actorId,
"user.blocked",
"AppUser",
actorId.ToString(),
"{\"reason\":\"abuse\"}",
AuditSource.Web
);
Assert.Equal(actorId, log.ActorId);
Assert.Equal("user.blocked", log.Action);
@@ -24,7 +31,14 @@ public class AuditLogTests
[Fact]
public void Create_AllowsNullActorForSystemActions()
{
var log = AuditLog.Create(null, "node.healthcheck", "Node", Guid.NewGuid().ToString(), null, AuditSource.System);
var log = AuditLog.Create(
null,
"node.healthcheck",
"Node",
Guid.NewGuid().ToString(),
null,
AuditSource.System
);
Assert.Null(log.ActorId);
Assert.Equal(AuditSource.System, log.Source);
@@ -11,7 +11,12 @@ public class NodeTests
[Fact]
public void Register_WithAbsoluteUri_CreatesEnabledUnknownStatusNode()
{
var node = Node.Register("Germany-1", new Uri("https://de1.example.com:2053"), Credentials, "Germany");
var node = Node.Register(
"Germany-1",
new Uri("https://de1.example.com:2053"),
Credentials,
"Germany"
);
Assert.Equal("Germany-1", node.Name);
Assert.Equal("Germany", node.Location);
@@ -25,13 +30,20 @@ public class NodeTests
{
var relativeUri = new Uri("de1.example.com", UriKind.Relative);
Assert.Throws<DomainException>(() => Node.Register("Germany-1", relativeUri, Credentials, null));
Assert.Throws<DomainException>(() =>
Node.Register("Germany-1", relativeUri, Credentials, null)
);
}
[Fact]
public void UpdateDetails_ChangesNameAndLocation()
{
var node = Node.Register("Old", new Uri("https://example.com"), Credentials, "Old location");
var node = Node.Register(
"Old",
new Uri("https://example.com"),
Credentials,
"Old location"
);
node.UpdateDetails("New", "New location");
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
@@ -23,5 +22,4 @@
<ItemGroup>
<ProjectReference Include="..\..\src\PnvPanel.Domain\PnvPanel.Domain.csproj" />
</ItemGroup>
</Project>
@@ -8,7 +8,11 @@ namespace PnvPanel.IntegrationTests.Activation;
[Collection(IntegrationTestCollection.Name)]
public class ActivationFlowTests(PnvPanelWebApplicationFactory factory)
{
private sealed record ActivationRequestResponse(Guid Id, string? Comment, DateTimeOffset CreatedAt);
private sealed record ActivationRequestResponse(
Guid Id,
string? Comment,
DateTimeOffset CreatedAt
);
[Fact]
public async Task RequestThenAdminApprove_ActivatesUser()
@@ -18,7 +22,10 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory)
var (userId, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123");
userClient.UseBearerToken(userToken);
var requestResponse = await userClient.PostJsonAsync("/api/activation/request", new { comment = "Please activate me" });
var requestResponse = await userClient.PostJsonAsync(
"/api/activation/request",
new { comment = "Please activate me" }
);
Assert.Equal(HttpStatusCode.OK, requestResponse.StatusCode);
var request = await requestResponse.ReadAsAsync<ActivationRequestResponse>();
Assert.NotNull(request);
@@ -27,7 +34,10 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory)
var adminToken = await LoginAsAdminAsync(adminClient);
adminClient.UseBearerToken(adminToken);
var approveResponse = await adminClient.PostAsync($"/api/admin/activation-requests/{request!.Id}/approve", content: null);
var approveResponse = await adminClient.PostAsync(
$"/api/admin/activation-requests/{request!.Id}/approve",
content: null
);
Assert.Equal(HttpStatusCode.NoContent, approveResponse.StatusCode);
var meResponse = await userClient.GetAsync("/api/auth/me");
@@ -44,14 +54,20 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory)
var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123");
userClient.UseBearerToken(userToken);
var requestResponse = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null });
var requestResponse = await userClient.PostJsonAsync(
"/api/activation/request",
new { comment = (string?)null }
);
var request = await requestResponse.ReadAsAsync<ActivationRequestResponse>();
using var adminClient = factory.CreateClient();
var adminToken = await LoginAsAdminAsync(adminClient);
adminClient.UseBearerToken(adminToken);
var rejectResponse = await adminClient.PostJsonAsync($"/api/admin/activation-requests/{request!.Id}/reject", new { reason = "not enough info" });
var rejectResponse = await adminClient.PostJsonAsync(
$"/api/admin/activation-requests/{request!.Id}/reject",
new { reason = "not enough info" }
);
Assert.Equal(HttpStatusCode.NoContent, rejectResponse.StatusCode);
var meResponse = await userClient.GetAsync("/api/auth/me");
@@ -67,10 +83,16 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory)
var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123");
userClient.UseBearerToken(userToken);
var first = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null });
var first = await userClient.PostJsonAsync(
"/api/activation/request",
new { comment = (string?)null }
);
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
var second = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null });
var second = await userClient.PostJsonAsync(
"/api/activation/request",
new { comment = (string?)null }
);
Assert.Equal(HttpStatusCode.Conflict, second.StatusCode);
}
}
@@ -8,11 +8,28 @@ namespace PnvPanel.IntegrationTests.Admin;
[Collection(IntegrationTestCollection.Name)]
public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory)
{
private sealed record NodeResponse(Guid Id, string Name, string BaseAddress, string Username, string? Location, string Status, bool IsEnabled);
private sealed record NodeResponse(
Guid Id,
string Name,
string BaseAddress,
string Username,
string? Location,
string Status,
bool IsEnabled
);
private sealed record InboundResponse(
Guid Id, Guid NodeId, string RemoteInboundId, string Protocol, string Remark, int Port,
bool IsPublished, string? DisplayName, int? MaxClients, IReadOnlyList<Guid> AllowedRoleIds);
Guid Id,
Guid NodeId,
string RemoteInboundId,
string Protocol,
string Remark,
int Port,
bool IsPublished,
string? DisplayName,
int? MaxClients,
IReadOnlyList<Guid> AllowedRoleIds
);
private sealed record SyncNodeResponse(int InboundsSynced, string Status);
@@ -23,14 +40,17 @@ public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory)
var adminToken = await LoginAsAdminAsync(adminClient);
adminClient.UseBearerToken(adminToken);
var registerResponse = await adminClient.PostJsonAsync("/api/admin/nodes", new
{
name = $"Node-{Guid.NewGuid():N}"[..20],
baseAddress = "https://node.example.com:2053",
username = "admin",
password = "node-panel-password",
location = "Germany",
});
var registerResponse = await adminClient.PostJsonAsync(
"/api/admin/nodes",
new
{
name = $"Node-{Guid.NewGuid():N}"[..20],
baseAddress = "https://node.example.com:2053",
username = "admin",
password = "node-panel-password",
location = "Germany",
}
);
Assert.Equal(HttpStatusCode.OK, registerResponse.StatusCode);
var node = await registerResponse.ReadAsAsync<NodeResponse>();
Assert.NotNull(node);
@@ -40,24 +60,32 @@ public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory)
var nodes = await listNodesResponse.ReadAsAsync<List<NodeResponse>>();
Assert.Contains(nodes!, n => n.Id == node!.Id);
var syncResponse = await adminClient.PostAsync($"/api/admin/nodes/{node!.Id}/sync", content: null);
var syncResponse = await adminClient.PostAsync(
$"/api/admin/nodes/{node!.Id}/sync",
content: null
);
Assert.Equal(HttpStatusCode.OK, syncResponse.StatusCode);
var sync = await syncResponse.ReadAsAsync<SyncNodeResponse>();
Assert.Equal(1, sync!.InboundsSynced);
var listInboundsResponse = await adminClient.GetAsync($"/api/admin/inbounds?nodeId={node.Id}");
var listInboundsResponse = await adminClient.GetAsync(
$"/api/admin/inbounds?nodeId={node.Id}"
);
Assert.Equal(HttpStatusCode.OK, listInboundsResponse.StatusCode);
var inbounds = await listInboundsResponse.ReadAsAsync<List<InboundResponse>>();
var inbound = Assert.Single(inbounds!);
Assert.False(inbound.IsPublished);
var publishResponse = await adminClient.SendPutJsonAsync($"/api/admin/inbounds/{inbound.Id}/publish", new
{
isPublished = true,
displayName = "Germany (VLESS)",
allowedRoleIds = Array.Empty<Guid>(),
maxClients = (int?)null,
});
var publishResponse = await adminClient.SendPutJsonAsync(
$"/api/admin/inbounds/{inbound.Id}/publish",
new
{
isPublished = true,
displayName = "Germany (VLESS)",
allowedRoleIds = Array.Empty<Guid>(),
maxClients = (int?)null,
}
);
Assert.Equal(HttpStatusCode.OK, publishResponse.StatusCode);
var published = await publishResponse.ReadAsAsync<InboundResponse>();
Assert.True(published!.IsPublished);
@@ -72,10 +100,17 @@ public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory)
var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123");
userClient.UseBearerToken(userToken);
var response = await userClient.PostJsonAsync("/api/admin/nodes", new
{
name = "Node", baseAddress = "https://node.example.com", username = "admin", password = "pw", location = (string?)null,
});
var response = await userClient.PostJsonAsync(
"/api/admin/nodes",
new
{
name = "Node",
baseAddress = "https://node.example.com",
username = "admin",
password = "pw",
location = (string?)null,
}
);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
@@ -10,9 +10,19 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory)
{
private sealed record RegisterResponse(Guid Id, string UserName);
private sealed record CurrentUserResponse(Guid Id, string UserName, string Role, bool IsActivated, bool TelegramLinked);
private sealed record CurrentUserResponse(
Guid Id,
string UserName,
string Role,
bool IsActivated,
bool TelegramLinked
);
private sealed record LoginResponse(string AccessToken, DateTimeOffset ExpiresAt, CurrentUserResponse User);
private sealed record LoginResponse(
string AccessToken,
DateTimeOffset ExpiresAt,
CurrentUserResponse User
);
[Fact]
public async Task RegisterLoginMeRefreshLogout_FullFlow_Succeeds()
@@ -21,13 +31,19 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory)
var userName = $"alice_{Guid.NewGuid():N}"[..20];
const string password = "P@ssw0rd123";
var registerResponse = await client.PostJsonAsync("/api/auth/register", new { userName, password });
var registerResponse = await client.PostJsonAsync(
"/api/auth/register",
new { userName, password }
);
Assert.Equal(HttpStatusCode.OK, registerResponse.StatusCode);
var registered = await registerResponse.ReadAsAsync<RegisterResponse>();
Assert.NotNull(registered);
Assert.Equal(userName, registered!.UserName);
var loginResponse = await client.PostJsonAsync("/api/auth/login", new { userName, password });
var loginResponse = await client.PostJsonAsync(
"/api/auth/login",
new { userName, password }
);
Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
var login = await loginResponse.ReadAsAsync<LoginResponse>();
Assert.NotNull(login);
@@ -61,9 +77,15 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory)
using var client = factory.CreateClient();
var userName = $"bob_{Guid.NewGuid():N}"[..20];
await client.PostJsonAsync("/api/auth/register", new { userName, password = "CorrectPassword123" });
await client.PostJsonAsync(
"/api/auth/register",
new { userName, password = "CorrectPassword123" }
);
var loginResponse = await client.PostJsonAsync("/api/auth/login", new { userName, password = "WrongPassword123" });
var loginResponse = await client.PostJsonAsync(
"/api/auth/login",
new { userName, password = "WrongPassword123" }
);
Assert.Equal(HttpStatusCode.Unauthorized, loginResponse.StatusCode);
}
@@ -74,10 +96,16 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory)
using var client = factory.CreateClient();
var userName = $"carol_{Guid.NewGuid():N}"[..20];
var first = await client.PostJsonAsync("/api/auth/register", new { userName, password = "P@ssw0rd123" });
var first = await client.PostJsonAsync(
"/api/auth/register",
new { userName, password = "P@ssw0rd123" }
);
Assert.Equal(HttpStatusCode.OK, first.StatusCode);
var second = await client.PostJsonAsync("/api/auth/register", new { userName, password = "AnotherPass123!" });
var second = await client.PostJsonAsync(
"/api/auth/register",
new { userName, password = "AnotherPass123!" }
);
Assert.Equal(HttpStatusCode.Conflict, second.StatusCode);
}
@@ -17,9 +17,21 @@ public class ConfigQuotaTests(PnvPanelWebApplicationFactory factory)
private sealed record SyncNodeResponse(int InboundsSynced, string Status);
private sealed record InboundResponse(Guid Id, Guid NodeId, string RemoteInboundId, string Protocol, string Remark, int Port, bool IsPublished);
private sealed record InboundResponse(
Guid Id,
Guid NodeId,
string RemoteInboundId,
string Protocol,
string Remark,
int Port,
bool IsPublished
);
private sealed record ActivationRequestResponse(Guid Id, string? Comment, DateTimeOffset CreatedAt);
private sealed record ActivationRequestResponse(
Guid Id,
string? Comment,
DateTimeOffset CreatedAt
);
private sealed record MyConfigsResponse(List<object> Configs, int MaxConfigs);
@@ -41,33 +53,44 @@ public class ConfigQuotaTests(PnvPanelWebApplicationFactory factory)
var userRole = roles!.Single(r => r.Name == "user");
var updateRoleResponse = await adminClient.SendPutJsonAsync(
$"/api/admin/roles/{userRole.Id}", new { maxConfigs = Quota, maxIpLimit = -1 });
$"/api/admin/roles/{userRole.Id}",
new { maxConfigs = Quota, maxIpLimit = -1 }
);
Assert.Equal(HttpStatusCode.OK, updateRoleResponse.StatusCode);
var registerNodeResponse = await adminClient.PostJsonAsync("/api/admin/nodes", new
{
name = $"QuotaNode-{Guid.NewGuid():N}"[..24],
baseAddress = "https://quota-node.example.com",
username = "admin",
password = "node-panel-password",
location = (string?)null,
});
var registerNodeResponse = await adminClient.PostJsonAsync(
"/api/admin/nodes",
new
{
name = $"QuotaNode-{Guid.NewGuid():N}"[..24],
baseAddress = "https://quota-node.example.com",
username = "admin",
password = "node-panel-password",
location = (string?)null,
}
);
var node = await registerNodeResponse.ReadAsAsync<NodeResponse>();
var syncResponse = await adminClient.PostAsync($"/api/admin/nodes/{node!.Id}/sync", content: null);
var syncResponse = await adminClient.PostAsync(
$"/api/admin/nodes/{node!.Id}/sync",
content: null
);
var sync = await syncResponse.ReadAsAsync<SyncNodeResponse>();
Assert.Equal(1, sync!.InboundsSynced);
var inboundsResponse = await adminClient.GetAsync($"/api/admin/inbounds?nodeId={node.Id}");
var inbound = (await inboundsResponse.ReadAsAsync<List<InboundResponse>>())!.Single();
var publishResponse = await adminClient.SendPutJsonAsync($"/api/admin/inbounds/{inbound.Id}/publish", new
{
isPublished = true,
displayName = "Quota inbound",
allowedRoleIds = new[] { userRole.Id },
maxClients = (int?)null,
});
var publishResponse = await adminClient.SendPutJsonAsync(
$"/api/admin/inbounds/{inbound.Id}/publish",
new
{
isPublished = true,
displayName = "Quota inbound",
allowedRoleIds = new[] { userRole.Id },
maxClients = (int?)null,
}
);
Assert.Equal(HttpStatusCode.OK, publishResponse.StatusCode);
using var userClient = factory.CreateClient();
@@ -75,21 +98,29 @@ public class ConfigQuotaTests(PnvPanelWebApplicationFactory factory)
var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123");
userClient.UseBearerToken(userToken);
var activationRequestResponse = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null });
var activationRequest = await activationRequestResponse.ReadAsAsync<ActivationRequestResponse>();
var approveResponse = await adminClient.PostAsync($"/api/admin/activation-requests/{activationRequest!.Id}/approve", content: null);
var activationRequestResponse = await userClient.PostJsonAsync(
"/api/activation/request",
new { comment = (string?)null }
);
var activationRequest =
await activationRequestResponse.ReadAsAsync<ActivationRequestResponse>();
var approveResponse = await adminClient.PostAsync(
$"/api/admin/activation-requests/{activationRequest!.Id}/approve",
content: null
);
Assert.Equal(HttpStatusCode.NoContent, approveResponse.StatusCode);
var tasks = Enumerable.Range(0, ConcurrentAttempts).Select(async i =>
{
using var attemptClient = factory.CreateClient();
attemptClient.UseBearerToken(userToken);
return await attemptClient.PostJsonAsync("/api/configs", new
var tasks = Enumerable
.Range(0, ConcurrentAttempts)
.Select(async i =>
{
inboundId = inbound.Id,
label = $"device-{i}",
using var attemptClient = factory.CreateClient();
attemptClient.UseBearerToken(userToken);
return await attemptClient.PostJsonAsync(
"/api/configs",
new { inboundId = inbound.Id, label = $"device-{i}" }
);
});
});
var responses = await Task.WhenAll(tasks);
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
@@ -24,5 +23,4 @@
<ItemGroup>
<ProjectReference Include="..\..\src\PnvPanel.Api\PnvPanel.Api.csproj" />
</ItemGroup>
</Project>
@@ -4,22 +4,46 @@ namespace PnvPanel.IntegrationTests.TestSupport;
public static class AuthTestHelper
{
public sealed record CurrentUserResponse(Guid Id, string UserName, string Role, bool IsActivated, bool TelegramLinked);
public sealed record CurrentUserResponse(
Guid Id,
string UserName,
string Role,
bool IsActivated,
bool TelegramLinked
);
public sealed record LoginResponse(string AccessToken, DateTimeOffset ExpiresAt, CurrentUserResponse User);
public sealed record LoginResponse(
string AccessToken,
DateTimeOffset ExpiresAt,
CurrentUserResponse User
);
public static async Task<(Guid Id, string AccessToken)> RegisterAndLoginAsync(HttpClient client, string userName, string password)
public static async Task<(Guid Id, string AccessToken)> RegisterAndLoginAsync(
HttpClient client,
string userName,
string password
)
{
var registerResponse = await client.PostJsonAsync("/api/auth/register", new { userName, password });
var registerResponse = await client.PostJsonAsync(
"/api/auth/register",
new { userName, password }
);
registerResponse.EnsureSuccessStatusCode();
var (id, accessToken) = await LoginAsync(client, userName, password);
return (id, accessToken);
}
public static async Task<(Guid Id, string AccessToken)> LoginAsync(HttpClient client, string userName, string password)
public static async Task<(Guid Id, string AccessToken)> LoginAsync(
HttpClient client,
string userName,
string password
)
{
var loginResponse = await client.PostJsonAsync("/api/auth/login", new { userName, password });
var loginResponse = await client.PostJsonAsync(
"/api/auth/login",
new { userName, password }
);
loginResponse.EnsureSuccessStatusCode();
var login = await loginResponse.ReadAsAsync<LoginResponse>();
return (login!.User.Id, login.AccessToken);
@@ -27,7 +51,11 @@ public static class AuthTestHelper
public static async Task<string> LoginAsAdminAsync(HttpClient client)
{
var (_, accessToken) = await LoginAsync(client, PnvPanelWebApplicationFactory.AdminUserName, PnvPanelWebApplicationFactory.AdminPassword);
var (_, accessToken) = await LoginAsync(
client,
PnvPanelWebApplicationFactory.AdminUserName,
PnvPanelWebApplicationFactory.AdminPassword
);
return accessToken;
}
}
@@ -13,10 +13,13 @@ public sealed class FakeXuiPanelGateway : IXuiPanelGateway
{
public Result ValidateBaseAddress(Uri baseAddress) => Result.Success();
public Task<NodeProbeResult> ProbeAsync(Node node, CancellationToken cancellationToken)
=> Task.FromResult(new NodeProbeResult(true, null));
public Task<NodeProbeResult> ProbeAsync(Node node, CancellationToken cancellationToken) =>
Task.FromResult(new NodeProbeResult(true, null));
public Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(Node node, CancellationToken cancellationToken)
public Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(
Node node,
CancellationToken cancellationToken
)
{
IReadOnlyList<RemoteInboundInfo> inbounds =
[
@@ -26,32 +29,53 @@ public sealed class FakeXuiPanelGateway : IXuiPanelGateway
return Task.FromResult(Result.Success(inbounds));
}
public void InvalidateClient(Guid nodeId)
{
}
public void InvalidateClient(Guid nodeId) { }
public Task<Result<string>> AddClientAsync(
Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName, int limitIp,
CancellationToken cancellationToken)
=> Task.FromResult(Result.Success(Guid.NewGuid().ToString()));
Node node,
string inboundRemoteId,
VpnProtocol protocol,
string clientEmail,
string clientName,
int limitIp,
CancellationToken cancellationToken
) => Task.FromResult(Result.Success(Guid.NewGuid().ToString()));
public Task<Result> RemoveClientAsync(
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol, CancellationToken cancellationToken)
=> Task.FromResult(Result.Success());
Node node,
string inboundRemoteId,
string clientExternalId,
VpnProtocol protocol,
CancellationToken cancellationToken
) => Task.FromResult(Result.Success());
public Task<Result> UpdateClientAsync(
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
string name, bool enable, CancellationToken cancellationToken)
=> Task.FromResult(Result.Success());
Node node,
string inboundRemoteId,
string clientExternalId,
VpnProtocol protocol,
string name,
bool enable,
CancellationToken cancellationToken
) => Task.FromResult(Result.Success());
public Task<Result<string>> BuildConnectionStringAsync(
Node node, Inbound inbound, string clientExternalId, string clientName, string publicHost, CancellationToken cancellationToken)
=> Task.FromResult(Result.Success("vless://fake-connection-string"));
Node node,
Inbound inbound,
string clientExternalId,
string clientName,
string publicHost,
CancellationToken cancellationToken
) => Task.FromResult(Result.Success("vless://fake-connection-string"));
public Task<Result<IReadOnlyDictionary<string, ClientTrafficInfo>>> GetClientTrafficAsync(
Node node, string inboundRemoteId, CancellationToken cancellationToken)
Node node,
string inboundRemoteId,
CancellationToken cancellationToken
)
{
IReadOnlyDictionary<string, ClientTrafficInfo> traffic = new Dictionary<string, ClientTrafficInfo>();
IReadOnlyDictionary<string, ClientTrafficInfo> traffic =
new Dictionary<string, ClientTrafficInfo>();
return Task.FromResult(Result.Success(traffic));
}
}
@@ -8,15 +8,24 @@ public static class HttpClientJsonExtensions
{
public static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public static void UseBearerToken(this HttpClient client, string accessToken)
=> client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
public static void UseBearerToken(this HttpClient client, string accessToken) =>
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
accessToken
);
public static async Task<T?> ReadAsAsync<T>(this HttpResponseMessage response)
=> await response.Content.ReadFromJsonAsync<T>(JsonOptions);
public static async Task<T?> ReadAsAsync<T>(this HttpResponseMessage response) =>
await response.Content.ReadFromJsonAsync<T>(JsonOptions);
public static Task<HttpResponseMessage> PostJsonAsync(this HttpClient client, string url, object body)
=> client.PostAsJsonAsync(url, body, JsonOptions);
public static Task<HttpResponseMessage> PostJsonAsync(
this HttpClient client,
string url,
object body
) => client.PostAsJsonAsync(url, body, JsonOptions);
public static Task<HttpResponseMessage> SendPutJsonAsync(this HttpClient client, string url, object body)
=> client.PutAsJsonAsync(url, body, JsonOptions);
public static Task<HttpResponseMessage> SendPutJsonAsync(
this HttpClient client,
string url,
object body
) => client.PutAsJsonAsync(url, body, JsonOptions);
}
@@ -42,20 +42,24 @@ public sealed class PnvPanelWebApplicationFactory : WebApplicationFactory<Progra
{
builder.UseEnvironment("Development");
builder.ConfigureAppConfiguration((_, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>
builder.ConfigureAppConfiguration(
(_, config) =>
{
["ConnectionStrings:Default"] = _postgres.GetConnectionString(),
["AdminSeed:Username"] = AdminUserName,
["AdminSeed:Password"] = AdminPassword,
// Пусто — TelegramBotHostedService при пустом токене не стартует (см. Api/Telegram/TelegramBotHostedService.cs).
["Telegram:BotToken"] = "",
// Весь collection делит один TestServer/host — все запросы идут от одного "клиента",
// дефолтный лимит 20/мин быстро исчерпывается. Поднимаем для тестового окружения.
["RateLimiting:AuthPermitLimit"] = "10000",
});
});
config.AddInMemoryCollection(
new Dictionary<string, string?>
{
["ConnectionStrings:Default"] = _postgres.GetConnectionString(),
["AdminSeed:Username"] = AdminUserName,
["AdminSeed:Password"] = AdminPassword,
// Пусто — TelegramBotHostedService при пустом токене не стартует (см. Api/Telegram/TelegramBotHostedService.cs).
["Telegram:BotToken"] = "",
// Весь collection делит один TestServer/host — все запросы идут от одного "клиента",
// дефолтный лимит 20/мин быстро исчерпывается. Поднимаем для тестового окружения.
["RateLimiting:AuthPermitLimit"] = "10000",
}
);
}
);
builder.ConfigureServices(services =>
{