джоба есть
Tests / frontend-test (pull_request) Successful in 47s
Tests / backend-test (pull_request) Successful in 2m7s

This commit is contained in:
gorbenko.pavel
2026-08-17 14:14:45 +05:00
parent 48ede1420d
commit 2a7130c37f
25 changed files with 1533 additions and 5 deletions
@@ -0,0 +1,120 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Tnsu.ApprovalRoute.Domain.Entities;
using Tnsu.ApprovalRoute.Infrastructure.Persistence;
using Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices;
/// <summary>
/// Блокировка провижининга — единственное, что не даёт двум фоновым задачам создать
/// по списку каждая, поэтому проверяется именно на живом Postgres: смысл здесь несёт
/// атомарность условного UPDATE, а не код на C#.
/// </summary>
public sealed class RequestTypeProvisioningLockTests(PostgresDatabaseFixture fixture)
: IClassFixture<PostgresDatabaseFixture>
{
private static readonly TimeSpan StaleAfter = TimeSpan.FromMinutes(30);
private readonly ServiceProvider _provider = TestServiceProviderFactory.Build(fixture.ConnectionString);
[Fact(DisplayName = "Повторный захват занятой блокировки не удаётся")]
public async Task TryAcquire_WhenAlreadyHeld_ReturnsFalse()
{
// Arrange
var requestTypeId = await CreateRequestTypeAsync();
// Action
var first = await TryAcquireAsync(requestTypeId);
var second = await TryAcquireAsync(requestTypeId);
// Assert
first.ShouldBeTrue();
second.ShouldBeFalse();
}
[Fact(DisplayName = "При одновременном захвате блокировку получает ровно одна задача")]
public async Task TryAcquire_Concurrently_OnlyOneWins()
{
// Arrange
var requestTypeId = await CreateRequestTypeAsync();
// Action — каждый захват из своего scope, то есть своим соединением к БД:
// именно так это и происходит у параллельных задач Hangfire.
var results = await Task.WhenAll(
Enumerable.Range(0, 8).Select(_ => TryAcquireAsync(requestTypeId)));
// Assert
results.Count(x => x).ShouldBe(1);
}
[Fact(DisplayName = "После снятия блокировку можно захватить снова")]
public async Task TryAcquire_AfterRelease_Succeeds()
{
// Arrange
var requestTypeId = await CreateRequestTypeAsync();
(await TryAcquireAsync(requestTypeId)).ShouldBeTrue();
// Action
using (var scope = _provider.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<RequestTypeProvisioningLock>()
.ReleaseAsync(requestTypeId, CancellationToken.None);
}
// Assert
(await TryAcquireAsync(requestTypeId)).ShouldBeTrue();
}
[Fact(DisplayName = "Просроченная блокировка перехватывается, свежая — нет")]
public async Task TryAcquire_StaleLock_IsTakenOver()
{
// Arrange
var requestTypeId = await CreateRequestTypeAsync();
(await TryAcquireAsync(requestTypeId)).ShouldBeTrue();
// Свежую блокировку с тем же порогом перехватить нельзя.
(await TryAcquireAsync(requestTypeId, TimeSpan.FromMinutes(10))).ShouldBeFalse();
// Action — отодвигаем метку в прошлое: так выглядит блокировка, которую
// оставил упавший, не снявший её процесс.
using (var scope = _provider.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ApprovalRouteDbContext>();
await db.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE request_types SET list_provisioning_started_at = now() - interval '1 hour' WHERE id = {requestTypeId}");
}
// Assert
(await TryAcquireAsync(requestTypeId, TimeSpan.FromMinutes(10))).ShouldBeTrue();
}
[Fact(DisplayName = "Захват блокировки для несуществующего типа заявки не удаётся")]
public async Task TryAcquire_UnknownRequestType_ReturnsFalse() =>
(await TryAcquireAsync(999999)).ShouldBeFalse();
private async Task<bool> TryAcquireAsync(int requestTypeId, TimeSpan? staleAfter = null)
{
using var scope = _provider.CreateScope();
return await scope.ServiceProvider.GetRequiredService<RequestTypeProvisioningLock>()
.TryAcquireAsync(requestTypeId, staleAfter ?? StaleAfter, CancellationToken.None);
}
private async Task<int> CreateRequestTypeAsync()
{
using var scope = _provider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApprovalRouteDbContext>();
var entity = new RequestType
{
Name = $"Тип {Guid.NewGuid():N}",
PageUrl = "/test",
Section = "General"
};
db.RequestTypes.Add(entity);
await db.SaveChangesAsync();
return entity.Id;
}
}