Implement asynchronous bumper rendering and add integration tests: refactor BumperAsset to support async rendering, introduce BumperRenderBackgroundService for processing, and create TeleWave.Integration.Tests with Testcontainers-Postgres for comprehensive testing of scheduling and bumper generation scenarios. Update documentation and ensure tests skip if Docker is not available.

This commit is contained in:
Leonid Pershin
2026-07-25 23:42:40 +03:00
parent 6c18a9da79
commit d6ad8e11a4
22 changed files with 1967 additions and 197 deletions
@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore;
using TeleWave.Infrastructure.Persistence;
using Testcontainers.PostgreSql;
using Xunit;
namespace TeleWave.Integration.Tests;
/// <summary>
/// Поднимает одноразовый Postgres в контейнере и применяет к нему реальные миграции. Даёт свежие
/// экземпляры <see cref="AppDbContext"/> (каждый — своё соединение), чтобы тестировать то, что InMemory
/// не умеет: транзакции, advisory-lock, ExecuteDelete, raw SQL. Требует запущенного Docker.
/// </summary>
public sealed class PostgresFixture : IAsyncLifetime
{
private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
/// <summary>false, если Docker недоступен (напр. CI-раннер без Docker) — тогда тесты пропускаются.</summary>
public bool Available { get; private set; }
public string ConnectionString => _container.GetConnectionString();
public async Task InitializeAsync()
{
try
{
await _container.StartAsync();
await using var db = CreateContext();
await db.Database.MigrateAsync();
Available = true;
}
catch (Exception)
{
// Docker не запущен/недоступен — интеграционные тесты будут пропущены (Skip), а не упадут.
Available = false;
}
}
public AppDbContext CreateContext() =>
new(new DbContextOptionsBuilder<AppDbContext>().UseNpgsql(ConnectionString).Options);
public async Task DisposeAsync() => await _container.DisposeAsync();
}
[CollectionDefinition("postgres")]
public sealed class PostgresCollection : ICollectionFixture<PostgresFixture>;
/// <summary>Детерминированный источник случайности для планировщика в тестах.</summary>
internal sealed class SequenceRandom(params int[] sequence) : Domain.Broadcast.Scheduling.IRandomSource
{
private readonly int[] _sequence = sequence.Length == 0 ? [0] : sequence;
private int _i;
public int Next(int maxExclusive)
{
if (maxExclusive <= 0)
return 0;
var value = _sequence[_i++ % _sequence.Length];
return ((value % maxExclusive) + maxExclusive) % maxExclusive;
}
}