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); // новая партиция строки ещё не получила
}
}