From caebcf550dc43c914c55dcde7c59b9840404cf50 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Mon, 31 Aug 2026 17:04:44 +0300 Subject: [PATCH] Add a shared SQLite test database helper (#754) --- .../Persistence/SeedParityTests.cs | 20 ++----- .../Persistence/TimelineParityTests.cs | 24 +++----- .../TestHelpers/SqliteTestDatabase.cs | 60 +++++++++++++++++++ 3 files changed, 72 insertions(+), 32 deletions(-) create mode 100644 code/backend/Cleanuparr.Infrastructure.Tests/TestHelpers/SqliteTestDatabase.cs diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/SeedParityTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/SeedParityTests.cs index 34a4df92..35f77b99 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/SeedParityTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/SeedParityTests.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Cleanuparr.Domain.Enums; +using Cleanuparr.Infrastructure.Tests.TestHelpers; using Cleanuparr.Persistence; using Cleanuparr.Persistence.Models.Configuration.Arr; using Cleanuparr.Persistence.Models.Configuration.BlacklistSync; @@ -43,11 +44,11 @@ public class SeedParityTests throw new SkipException($"Docker is unavailable, skipping seed parity test: {exception.Message}"); } - string sqlitePath = Path.Combine(Path.GetTempPath(), $"cleanuparr-seed-parity-{Guid.NewGuid():N}.db"); + using SqliteTestDatabase sqlite = SqliteTestDatabase.Create("seed-parity"); try { - SeedSnapshot sqliteSnapshot = await LoadSqliteSeedAsync(sqlitePath); + SeedSnapshot sqliteSnapshot = await LoadSqliteSeedAsync(sqlite); SeedSnapshot postgresSnapshot = await LoadPostgresSeedAsync(postgresContainer); AssertSingletonMatches("general_configs", sqliteSnapshot.GeneralConfig, postgresSnapshot.GeneralConfig); @@ -62,23 +63,12 @@ public class SeedParityTests finally { await postgresContainer.DisposeAsync(); - - if (File.Exists(sqlitePath)) - { - File.Delete(sqlitePath); - } } } - private static async Task LoadSqliteSeedAsync(string sqlitePath) + private static async Task LoadSqliteSeedAsync(SqliteTestDatabase sqlite) { - DbContextOptionsBuilder optionsBuilder = new(); - optionsBuilder - .UseSqlite($"Data Source={sqlitePath}", options => options.MigrationsAssembly("Cleanuparr.Persistence.Sqlite")) - .UseLowerCaseNamingConvention() - .UseSnakeCaseNamingConvention(); - - await using DataContext context = new(optionsBuilder.Options, new SqliteDatabaseProvider()); + await using DataContext context = sqlite.CreateContext(); await context.Database.MigrateAsync(); return await LoadSnapshotAsync(context); diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/TimelineParityTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/TimelineParityTests.cs index 6fc549cf..c27db03a 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/TimelineParityTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Persistence/TimelineParityTests.cs @@ -1,4 +1,5 @@ using Cleanuparr.Domain.Enums; +using Cleanuparr.Infrastructure.Tests.TestHelpers; using Cleanuparr.Persistence; using Cleanuparr.Persistence.Models.Events; using Cleanuparr.Persistence.Providers; @@ -42,11 +43,11 @@ public class TimelineParityTests throw new SkipException($"Docker is unavailable, skipping timeline parity test: {exception.Message}"); } - string sqlitePath = Path.Combine(Path.GetTempPath(), $"cleanuparr-timeline-parity-{Guid.NewGuid():N}.db"); + using SqliteTestDatabase sqlite = SqliteTestDatabase.Create("timeline-parity"); try { - List sqliteLabels = await BucketLabelsSqliteAsync(sqlitePath, size); + List sqliteLabels = await BucketLabelsSqliteAsync(sqlite, size); List postgresLabels = await BucketLabelsPostgresAsync(postgresContainer, size); postgresLabels.ShouldBe(sqliteLabels); @@ -54,28 +55,17 @@ public class TimelineParityTests finally { await postgresContainer.DisposeAsync(); - - if (File.Exists(sqlitePath)) - { - File.Delete(sqlitePath); - } } } - private static async Task> BucketLabelsSqliteAsync(string sqlitePath, TimelineBucketSize size) + private static async Task> BucketLabelsSqliteAsync( + SqliteTestDatabase sqlite, TimelineBucketSize size) { - SqliteDatabaseProvider provider = new(); - DbContextOptionsBuilder builder = new(); - builder - .UseSqlite($"Data Source={sqlitePath}", options => options.MigrationsAssembly("Cleanuparr.Persistence.Sqlite")) - .UseLowerCaseNamingConvention() - .UseSnakeCaseNamingConvention(); - - await using EventsContext context = new(builder.Options, provider); + await using EventsContext context = sqlite.CreateContext(); await context.Database.MigrateAsync(); await SeedAsync(context); - return await BucketLabelsAsync(context, provider, size); + return await BucketLabelsAsync(context, sqlite.Provider, size); } private static async Task> BucketLabelsPostgresAsync(PostgreSqlContainer container, TimelineBucketSize size) diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/TestHelpers/SqliteTestDatabase.cs b/code/backend/Cleanuparr.Infrastructure.Tests/TestHelpers/SqliteTestDatabase.cs new file mode 100644 index 00000000..d95382e4 --- /dev/null +++ b/code/backend/Cleanuparr.Infrastructure.Tests/TestHelpers/SqliteTestDatabase.cs @@ -0,0 +1,60 @@ +using Cleanuparr.Persistence.Providers; +using Microsoft.EntityFrameworkCore; + +namespace Cleanuparr.Infrastructure.Tests.TestHelpers; + +/// +/// A throwaway file-backed SQLite database, wired the way the app wires one. +/// Dispose deletes the file and its journals. +/// +public sealed class SqliteTestDatabase : IDisposable +{ + private readonly string _path; + + private SqliteTestDatabase(string path) + { + _path = path; + } + + /// + /// The provider the contexts were built with. + /// + public SqliteDatabaseProvider Provider { get; } = new(); + + /// + /// Names the temp file after the caller. + /// + public static SqliteTestDatabase Create(string name) => + new(Path.Combine(Path.GetTempPath(), $"cleanuparr-{name}-{Guid.NewGuid():N}.db")); + + /// + /// A context pointing at this database, unmigrated. + /// + public TContext CreateContext() + where TContext : DbContext + { + DbContextOptionsBuilder optionsBuilder = new(); + optionsBuilder + .UseSqlite( + $"Data Source={_path}", + options => options.MigrationsAssembly("Cleanuparr.Persistence.Sqlite")) + .UseLowerCaseNamingConvention() + .UseSnakeCaseNamingConvention(); + + return (TContext)Activator.CreateInstance( + typeof(TContext), + optionsBuilder.Options, + Provider)!; + } + + public void Dispose() + { + foreach (string file in new[] { _path, $"{_path}-wal", $"{_path}-shm" }) + { + if (File.Exists(file)) + { + File.Delete(file); + } + } + } +}