using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Shouldly; using Tnsu.ApprovalRoute.Application.ListPartitioning; using Tnsu.ApprovalRoute.Domain.Entities; using Tnsu.ApprovalRoute.Infrastructure.Persistence; using Tnsu.ApprovalRoute.Infrastructure.SharePoint; using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure; namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices; /// /// request_list_partitions хранит ТОЛЬКО архив — партиция, которая ни разу не /// архивировалась, не должна получать строку просто от того, что её прочитали /// (см. ListPartitionRegistry). Раньше самозаполняющийся реестр писал строку /// уже на первое чтение, из-за чего один и тот же список оказывался записан /// дважды: и в RequestType.ListId, и строкой в этой таблице. /// public sealed class ListPartitionRegistryTests(PostgresDatabaseFixture fixture) : IClassFixture { private readonly ApprovalRouteDbContext _db = TestServiceProviderFactory.Build(fixture.ConnectionString).GetRequiredService(); private ListPartitionRegistry BuildRegistry() => new(_db, Mock.Of(), NullLogger.Instance); private async Task CreateTypeAsync(string listId) { var type = new RequestType { Name = $"Тест {Guid.NewGuid():N}", PageUrl = $"/test-{Guid.NewGuid():N}", ListId = listId, SiteUrl = "https://tnsukz.sharepoint.com/sites/requests", Section = "General" }; _db.RequestTypes.Add(type); await _db.SaveChangesAsync(); return type; } [Fact(DisplayName = "Партиция, которую ни разу не архивировали, не пишется в request_list_partitions")] public async Task BuildCurrentAsync_NeverArchived_DoesNotWriteRow() { // Arrange var registry = BuildRegistry(); var type = await CreateTypeAsync("/sites/requests/Lists/1"); // Action var archived = await registry.GetArchivedAsync(type, CancellationToken.None); var current = await registry.BuildCurrentAsync(type, archived, CancellationToken.None); // Assert archived.ShouldBeEmpty(); current.Number.ShouldBe(1); current.ListId.ShouldBe(type.ListId); current.IsCurrent.ShouldBeTrue(); var rowsInDb = await _db.RequestListPartitions .Where(x => x.RequestTypeId == type.Id) .CountAsync(); rowsInDb.ShouldBe(0); } [Fact(DisplayName = "ArchiveCurrentAsync пишет ровно одну строку архива под номером 1")] public async Task ArchiveCurrentAsync_WritesExactlyOneRow() { // Arrange var registry = BuildRegistry(); var type = await CreateTypeAsync("/sites/requests/Lists/1"); // Action var archived = await registry.ArchiveCurrentAsync(type, CancellationToken.None); // Assert archived.Number.ShouldBe(1); archived.ListId.ShouldBe("/sites/requests/Lists/1"); var rowsInDb = await _db.RequestListPartitions .Where(x => x.RequestTypeId == type.Id) .ToListAsync(); rowsInDb.Count.ShouldBe(1); rowsInDb[0].ListId.ShouldBe("/sites/requests/Lists/1"); } [Fact(DisplayName = "ArchiveCurrentAsync идемпотентна — повтор не создаёт вторую строку")] public async Task ArchiveCurrentAsync_CalledTwice_DoesNotDuplicate() { // Arrange var registry = BuildRegistry(); var type = await CreateTypeAsync("/sites/requests/Lists/1"); // Action — как повторная попытка Hangfire после частичного сбоя: пока указатель // ещё не переключён, GetArchivedAsync только что записанную строку не увидит // (фильтрует по ListId != RequestType.ListId) — именно поэтому идемпотентность // ArchiveCurrentAsync не должна зависеть от него (см. её doc-комментарий). await registry.ArchiveCurrentAsync(type, CancellationToken.None); await registry.ArchiveCurrentAsync(type, CancellationToken.None); // Assert var rowsInDb = await _db.RequestListPartitions .Where(x => x.RequestTypeId == type.Id) .CountAsync(); rowsInDb.ShouldBe(1); } [Fact(DisplayName = "После архивации и переключения текущая партиция получает следующий номер")] public async Task BuildCurrentAsync_AfterArchiving_ReturnsNextNumber() { // Arrange: тип с одной партицией, которую сейчас архивируем (как // CreateNextListPartitionJob перед переключением на следующую). var registry = BuildRegistry(); var type = await CreateTypeAsync("/sites/requests/Lists/1"); await registry.ArchiveCurrentAsync(type, CancellationToken.None); // Action: переключаем указатель на новую партицию (сама она архивной // строки ещё не получает — см. doc-комментарий ListPartitionRegistry). type.ListId = "/sites/requests/Lists/2"; await _db.SaveChangesAsync(); var archived = await registry.GetArchivedAsync(type, CancellationToken.None); var current = await registry.BuildCurrentAsync(type, archived, CancellationToken.None); // Assert archived.Count.ShouldBe(1); archived[0].ListId.ShouldBe("/sites/requests/Lists/1"); current.Number.ShouldBe(2); current.ListId.ShouldBe("/sites/requests/Lists/2"); var rowsInDb = await _db.RequestListPartitions .Where(x => x.RequestTypeId == type.Id) .CountAsync(); rowsInDb.ShouldBe(1); // новая партиция строки ещё не получила } }