using JobTrackerApi.Services; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace JobTrackerApi.Tests; public sealed class DatabaseBackupRunnerTests : IDisposable { private readonly string _root; public DatabaseBackupRunnerTests() { _root = Path.Combine(Path.GetTempPath(), $"jt-backup-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(_root); } public void Dispose() { SqliteConnection.ClearAllPools(); try { Directory.Delete(_root, recursive: true); } catch (IOException) { } } private string CreateSourceDb(out string connectionString) { var dbPath = Path.Combine(_root, "source.db"); connectionString = $"Data Source={dbPath}"; using var connection = new SqliteConnection(connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "CREATE TABLE Sample (Id INTEGER PRIMARY KEY, Name TEXT); INSERT INTO Sample (Name) VALUES ('alpha'), ('beta');"; command.ExecuteNonQuery(); return dbPath; } private SqliteDatabaseBackupRunner CreateRunner(string connectionString, int retainCount = 14) => new(connectionString, Path.Combine(_root, "backups"), retainCount, NullLogger.Instance); [Fact] public async Task RunOnce_creates_a_restorable_backup_file() { CreateSourceDb(out var connectionString); var runner = CreateRunner(connectionString); var backupPath = await runner.RunOnceAsync(CancellationToken.None); Assert.NotNull(backupPath); Assert.True(File.Exists(backupPath)); await using var verify = new SqliteConnection($"Data Source={backupPath}"); await verify.OpenAsync(); await using var count = verify.CreateCommand(); count.CommandText = "SELECT COUNT(*) FROM Sample"; Assert.Equal(2L, (long)(await count.ExecuteScalarAsync())!); } [Fact] public async Task RunOnce_prunes_backups_beyond_retention() { CreateSourceDb(out var connectionString); var runner = CreateRunner(connectionString, retainCount: 2); var backupsRoot = runner.BackupsRoot; Directory.CreateDirectory(backupsRoot); for (var i = 0; i < 3; i++) { var stale = Path.Combine(backupsRoot, $"{SqliteDatabaseBackupRunner.BackupFilePrefix}stale{i}.db"); File.WriteAllText(stale, "stale"); File.SetLastWriteTimeUtc(stale, DateTime.UtcNow.AddDays(-10 - i)); } await runner.RunOnceAsync(CancellationToken.None); var remaining = Directory.GetFiles(backupsRoot, $"{SqliteDatabaseBackupRunner.BackupFilePrefix}*.db"); Assert.Equal(2, remaining.Length); Assert.Contains(remaining, f => Path.GetFileName(f).Contains("stale0")); } [Fact] public void Latest_backup_timestamp_reflects_newest_file() { CreateSourceDb(out var connectionString); var runner = CreateRunner(connectionString); Assert.Null(runner.GetLatestBackupUtc()); Directory.CreateDirectory(runner.BackupsRoot); var file = Path.Combine(runner.BackupsRoot, $"{SqliteDatabaseBackupRunner.BackupFilePrefix}x.db"); File.WriteAllText(file, "x"); var stamp = DateTime.UtcNow.AddHours(-3); File.SetLastWriteTimeUtc(file, stamp); var latest = runner.GetLatestBackupUtc(); Assert.NotNull(latest); Assert.True(Math.Abs((latest!.Value - stamp).TotalSeconds) < 2); } }