using ErsatzTV.Infrastructure;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Sqlite.Data;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;
namespace ErsatzTV.Tests.Support;
///
/// A multi-connection in-memory SQLite harness (shared cache) so several
/// instances on DISTINCT connections hit the same database and real unique-constraint enforcement
/// applies across connections. Unlike (a single shared connection),
/// this can reproduce a genuine cross-connection write race — needed to exercise the #308 composite-PK
/// collision through the real handler. A keep-alive connection holds the shared in-memory DB open for
/// the harness lifetime.
///
public sealed class SharedCacheTvContext : IAsyncDisposable
{
private readonly string _connectionString;
private readonly SqliteConnection _keepAlive;
private SharedCacheTvContext(string connectionString, SqliteConnection keepAlive)
{
_connectionString = connectionString;
_keepAlive = keepAlive;
}
public static async Task CreateAsync(string name)
{
TvContext.IsSqlite = true;
TvContext.IsUniqueConstraintViolation = SqliteErrorClassifier.IsUniqueConstraintViolation;
// Foreign keys OFF (connection-string keyword, inherited by every connection) so partial object
// graphs can be seeded without satisfying every FK — matches InMemoryTvContext. The composite-PK /
// UNIQUE constraint under test is enforced regardless of this pragma.
var connectionString = $"Data Source={name};Mode=Memory;Cache=Shared;Foreign Keys=False";
var keepAlive = new SqliteConnection(connectionString);
await keepAlive.OpenAsync();
await using (TvContext context = Create(BuildOptions(connectionString, null)))
{
await context.Database.EnsureCreatedAsync();
}
return new SharedCacheTvContext(connectionString, keepAlive);
}
/// A factory whose contexts each open their own connection to the shared DB.
public IDbContextFactory Factory(IInterceptor? interceptor = null) =>
new PerConnectionFactory(_connectionString, interceptor);
public TvContext CreateContext() => Create(BuildOptions(_connectionString, null));
/// A freshly opened raw connection to the shared DB (used to stage a concurrent writer).
public SqliteConnection OpenConnection()
{
var connection = new SqliteConnection(_connectionString);
connection.Open();
return connection;
}
public async ValueTask DisposeAsync() => await _keepAlive.DisposeAsync();
private static DbContextOptions BuildOptions(string connectionString, IInterceptor? interceptor)
{
DbContextOptionsBuilder builder = new DbContextOptionsBuilder()
.UseSqlite(connectionString);
if (interceptor is not null)
{
builder.AddInterceptors(interceptor);
}
return builder.Options;
}
private static TvContext Create(DbContextOptions options) =>
new(
options,
NullLoggerFactory.Instance,
new SlowQueryInterceptor(NullLogger.Instance));
private sealed class PerConnectionFactory(string connectionString, IInterceptor? interceptor)
: IDbContextFactory
{
public TvContext CreateDbContext() => Create(BuildOptions(connectionString, interceptor));
}
}