Files
PnvPanel/backend/tests/PnvPanel.Application.Tests/Admin/Support/ApproveRoleRequestCommandHandlerTests.cs
T
Leonid Pershin 5ff5224935
CI / Backend (build + test) (push) Successful in 1m19s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s
Refactor payment request handling to support role change top-ups
- Updated the `PaymentRequest` model to include a new `Kind` property, distinguishing between `Subscription` and `RoleChangeTopUp` requests.
- Modified the `TelegramNotifier` to accommodate the new request type, ensuring accurate notifications for role change top-ups.
- Enhanced the `ConfirmPaymentRequestCommandHandler` to handle role change top-ups without extending the billing period, reflecting the new payment logic.
- Updated various application components and tests to support the new payment request structure and ensure proper functionality.
- Revised API documentation to clarify the behavior of role change top-ups and their impact on billing.
2026-07-19 16:45:17 +03:00

266 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using NSubstitute;
using PnvPanel.Application.Admin.Roles;
using PnvPanel.Application.Admin.Support;
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Application.Common.Messaging;
using PnvPanel.Application.Common.Models;
using PnvPanel.Application.Support;
using PnvPanel.Application.Tests.TestSupport;
using PnvPanel.Domain.Billing;
using PnvPanel.Domain.Support;
using Xunit;
namespace PnvPanel.Application.Tests.Admin.Support;
public class ApproveRoleRequestCommandHandlerTests
{
private readonly IRoleService _roleService = Substitute.For<IRoleService>();
private readonly IIdentityService _identityService = Substitute.For<IIdentityService>();
private readonly IRealtimeNotifier _notifier = Substitute.For<IRealtimeNotifier>();
private readonly ITelegramNotifier _telegramNotifier = Substitute.For<ITelegramNotifier>();
private ApproveRoleRequestCommandHandler CreateHandler(IAppDbContext dbContext, ICurrentUser currentUser) =>
new(dbContext, _roleService, _identityService, _notifier, _telegramNotifier, currentUser);
private static CurrentUserProfile Profile(
Guid userId,
int maxConfigs,
DateTimeOffset? billingPaidUntil = null
) =>
new(
userId,
"alice",
Guid.NewGuid(),
"old-role",
IsActivated: true,
IsBlocked: false,
MaxConfigs: maxConfigs,
MaxIpLimit: 3,
SubscriptionToken: "sub-token",
BillingEnabled: billingPaidUntil != null,
BillingPaidUntil: billingPaidUntil,
BillingSuspended: false
);
[Fact]
public async Task Handle_ForNewRoleRequest_CreatesRoleAssignsAndResolves()
{
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var ticket = SupportTicket.CreateRoleRequestForNewRole(userId, "premium", 10, 5);
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
var newRoleId = Guid.NewGuid();
_roleService
.CreateRoleAsync("premium", 10, 5, false, Arg.Any<CancellationToken>())
.Returns(Result.Success(new RoleDto(newRoleId, "premium", 10, 5, false, false)));
_roleService
.ChangeUserRoleAsync(userId, newRoleId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin");
var handler = CreateHandler(dbContext, currentUser);
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, false, Arg.Any<CancellationToken>());
await _roleService
.Received(1)
.ChangeUserRoleAsync(userId, newRoleId, Arg.Any<CancellationToken>());
await _telegramNotifier
.Received(1)
.NotifyUserAsync(
userId,
Arg.Any<string>(),
Arg.Any<string?>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
public async Task Handle_ForExistingRoleRequest_SkipsRoleCreation()
{
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var ticket = SupportTicket.CreateRoleRequestForExistingRole(userId, roleId);
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(new List<RoleDto> { new(roleId, "premium", 10, 5, false, false) });
_roleService
.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
var handler = CreateHandler(dbContext, currentUser);
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<bool>(),
Arg.Any<CancellationToken>()
);
}
[Fact]
public async Task Handle_WhenNotRoleRequestType_ReturnsError()
{
using var dbContext = InMemoryDbContextFactory.Create();
var ticket = SupportTicket.CreateBugReport(Guid.NewGuid());
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid());
var handler = CreateHandler(dbContext, currentUser);
var result = await handler.Handle(
new ApproveRoleRequestCommand(ticket.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(SupportErrors.NotRoleRequest, result.Error);
}
[Fact]
public async Task Handle_WhenTicketIsOwnedByAdmin_StillApproves()
{
// Одобрить свою же заявку можно — единственный реальный риск (снять admin с последнего
// администратора) ловит RoleService.ChangeUserRoleAsync, а не этот хендлер (см. соседний тест).
using var dbContext = InMemoryDbContextFactory.Create();
var adminId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var ticket = SupportTicket.CreateRoleRequestForExistingRole(adminId, roleId);
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(new List<RoleDto> { new(roleId, "premium", 10, 5, false, false) });
_roleService
.ChangeUserRoleAsync(adminId, roleId, Arg.Any<CancellationToken>())
.Returns(Result.Success());
var currentUser = FakeCurrentUser.Authenticated(adminId, "admin");
var handler = CreateHandler(dbContext, currentUser);
var result = await handler.Handle(
new ApproveRoleRequestCommand(ticket.Id),
CancellationToken.None
);
Assert.True(result.IsSuccess);
Assert.Equal(TicketStatus.Resolved, ticket.Status);
}
[Fact]
public async Task Handle_WhenRoleServiceRefusesLastAdminDowngrade_PropagatesFailure()
{
using var dbContext = InMemoryDbContextFactory.Create();
var adminId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var ticket = SupportTicket.CreateRoleRequestForExistingRole(adminId, roleId);
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(new List<RoleDto> { new(roleId, "premium", 10, 5, false, false) });
_roleService
.ChangeUserRoleAsync(adminId, roleId, Arg.Any<CancellationToken>())
.Returns(Result.Failure(RoleErrors.CannotRemoveLastAdmin));
var currentUser = FakeCurrentUser.Authenticated(adminId, "admin");
var handler = CreateHandler(dbContext, currentUser);
var result = await handler.Handle(
new ApproveRoleRequestCommand(ticket.Id),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(RoleErrors.CannotRemoveLastAdmin, result.Error);
// Тикет остаётся Open — можно повторить попытку после назначения второго админа.
Assert.Equal(TicketStatus.Open, ticket.Status);
}
[Fact]
public async Task Handle_WhenNewRoleMoreExpensiveWithActivePaidPeriod_CreatesRoleChangeTopUp()
{
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var ticket = SupportTicket.CreateRoleRequestForExistingRole(userId, roleId);
dbContext.SupportTickets.Add(ticket);
var pricing = PnvPanel.Domain.Pricing.PricingSettings.CreateDefault();
pricing.Update(500, 450, 400);
dbContext.PricingSettings.Add(pricing);
await dbContext.SaveChangesAsync(CancellationToken.None);
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(new List<RoleDto> { new(roleId, "premium", MaxConfigs: 10, 5, false, BillingEnabled: true) });
_roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(Profile(userId, maxConfigs: 3, billingPaidUntil: DateTimeOffset.UtcNow.AddDays(30)));
var handler = CreateHandler(dbContext, FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin"));
var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None);
Assert.True(result.IsSuccess);
var topUp = Assert.Single(dbContext.PaymentRequests.Local);
Assert.Equal(PaymentRequestKind.RoleChangeTopUp, topUp.Kind);
Assert.Null(topUp.Period);
Assert.True(topUp.AmountSnapshot > 0);
}
[Fact]
public async Task Handle_WhenNoActivePaidPeriod_DoesNotCreateTopUp()
{
using var dbContext = InMemoryDbContextFactory.Create();
var userId = Guid.NewGuid();
var roleId = Guid.NewGuid();
var ticket = SupportTicket.CreateRoleRequestForExistingRole(userId, roleId);
dbContext.SupportTickets.Add(ticket);
await dbContext.SaveChangesAsync(CancellationToken.None);
_roleService
.ListRolesAsync(Arg.Any<CancellationToken>())
.Returns(new List<RoleDto> { new(roleId, "premium", MaxConfigs: 10, 5, false, BillingEnabled: true) });
_roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any<CancellationToken>()).Returns(Result.Success());
_identityService
.GetProfileAsync(userId, Arg.Any<CancellationToken>())
.Returns(Profile(userId, maxConfigs: 3, billingPaidUntil: null));
var handler = CreateHandler(dbContext, FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin"));
var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Empty(dbContext.PaymentRequests.Local);
}
}