using NSubstitute; using PnvPanel.Application.Admin.Maintenance; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Application.Common.Models; using PnvPanel.Application.Tests.TestSupport; using PnvPanel.Domain.Audit; using PnvPanel.Domain.Configs; using PnvPanel.Domain.Inbounds; using PnvPanel.Domain.News; using PnvPanel.Domain.Nodes; using PnvPanel.Domain.Support; using Xunit; namespace PnvPanel.Application.Tests.Admin.Maintenance; public class FactoryResetCommandHandlerTests { private readonly IIdentityService _identityService = Substitute.For(); private readonly IRoleService _roleService = Substitute.For(); private readonly IXuiPanelGateway _gateway = Substitute.For(); private readonly IFileStorage _fileStorage = Substitute.For(); private readonly IClientAppCatalogSeeder _catalogSeeder = Substitute.For(); private readonly IInstructionIntroSeeder _instructionIntroSeeder = Substitute.For(); [Fact] public async Task Handle_WipesEverythingExceptCurrentAdminAndSystemRoles() { using var dbContext = InMemoryDbContextFactory.Create(); var adminId = Guid.NewGuid(); var otherUserId = Guid.NewGuid(); var node = Node.Register( "node-1", new Uri("https://node.example.com"), new NodeCredentials("u", "p"), null ); var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443); var config = VpnConfig.Create(otherUserId, inbound.Id, VpnProtocol.Vless, "label"); dbContext.Nodes.Add(node); dbContext.Inbounds.Add(inbound); dbContext.VpnConfigs.Add(config); var ticket = SupportTicket.CreateBugReport(otherUserId); dbContext.SupportTickets.Add(ticket); var comment = TicketComment.Create(ticket.Id, otherUserId, "текст"); dbContext.TicketComments.Add(comment); var attachment = TicketAttachment.Create( comment.Id, "shot.png", "stored-name", "image/png", 10 ); dbContext.TicketAttachments.Add(attachment); dbContext.NewsPosts.Add(NewsPost.Create("Заголовок", "Текст")); dbContext.AuditLogs.Add( AuditLog.Create(adminId, "SomeOldAction", "Test", "1", null, AuditSource.Web) ); await dbContext.SaveChangesAsync(CancellationToken.None); var adminRoleId = Guid.NewGuid(); var customRoleId = Guid.NewGuid(); _roleService .ListRolesAsync(Arg.Any()) .Returns( new List { new(adminRoleId, "admin", -1, -1, IsSystem: true), new(customRoleId, "premium", 10, 5, IsSystem: false), } ); _roleService .DeleteRoleAsync(customRoleId, Arg.Any()) .Returns(Result.Success()); _identityService .ListAllUserIdsExceptAsync(adminId, Arg.Any()) .Returns(new List { otherUserId }); _identityService .DeleteUserAsync(otherUserId, Arg.Any()) .Returns(Result.Success()); _gateway .RemoveClientAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() ) .Returns(Result.Success()); var currentUser = FakeCurrentUser.Authenticated(adminId, "admin"); var handler = new FactoryResetCommandHandler( dbContext, _identityService, _roleService, _gateway, _fileStorage, _catalogSeeder, _instructionIntroSeeder, currentUser ); var result = await handler.Handle(new FactoryResetCommand(), CancellationToken.None); // Хендлер сам коммитит несколько раз (в проде это делает он же, а не диспетчер) — фиксируем финально. await dbContext.SaveChangesAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.False(dbContext.VpnConfigs.Any()); Assert.False(dbContext.Inbounds.Any()); Assert.False(dbContext.Nodes.Any()); Assert.False(dbContext.SupportTickets.Any()); Assert.False(dbContext.TicketComments.Any()); Assert.False(dbContext.TicketAttachments.Any()); Assert.False(dbContext.NewsPosts.Any()); // Аудит очищен, но остаётся ровно одна собственная запись о факте сброса. var auditLog = Assert.Single(dbContext.AuditLogs); Assert.Equal("FactoryReset", auditLog.Action); await _identityService .Received(1) .DeleteUserAsync(otherUserId, Arg.Any()); await _roleService.Received(1).DeleteRoleAsync(customRoleId, Arg.Any()); await _roleService .DidNotReceive() .DeleteRoleAsync(adminRoleId, Arg.Any()); await _gateway .Received(1) .RemoveClientAsync( Arg.Any(), inbound.RemoteInboundId, config.ClientExternalId, config.Protocol, Arg.Any() ); await _fileStorage.Received(1).DeleteAsync("stored-name", Arg.Any()); await _catalogSeeder.Received(1).SeedIfEmptyAsync(Arg.Any()); await _instructionIntroSeeder.Received(1).SeedIfEmptyAsync(Arg.Any()); } }