Files
jobtrackingapp/JobTrackerApi.Tests/DatabaseBackupRunnerTests.cs
T

131 lines
5.2 KiB
C#

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<SqliteDatabaseBackupRunner>.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_includes_committed_WAL_rows_in_an_independent_restorable_snapshot()
{
CreateSourceDb(out var connectionString);
await using var source = new SqliteConnection(connectionString);
await source.OpenAsync();
await using var write = source.CreateCommand();
write.CommandText = "PRAGMA journal_mode=WAL;";
Assert.Equal("wal", await write.ExecuteScalarAsync());
write.CommandText = "INSERT INTO Sample (Name) VALUES ('committed-in-wal');";
await write.ExecuteNonQueryAsync();
Assert.True(new FileInfo(Path.Combine(_root, "source.db-wal")).Length > 0);
var backupPath = await CreateRunner(connectionString).RunOnceAsync(CancellationToken.None);
Assert.NotNull(backupPath);
await using var backup = new SqliteConnection($"Data Source={backupPath};Mode=ReadOnly;Pooling=False");
await backup.OpenAsync();
await using var query = backup.CreateCommand();
query.CommandText = "PRAGMA integrity_check;";
Assert.Equal("ok", await query.ExecuteScalarAsync());
query.CommandText = "SELECT COUNT(*) FROM Sample;";
Assert.Equal(3L, await query.ExecuteScalarAsync());
query.CommandText = "SELECT Name FROM Sample WHERE Id=3;";
Assert.Equal("committed-in-wal", await query.ExecuteScalarAsync());
write.CommandText = "INSERT INTO Sample (Name) VALUES ('after-snapshot');";
await write.ExecuteNonQueryAsync();
query.CommandText = "SELECT COUNT(*) FROM Sample;";
Assert.Equal(3L, await query.ExecuteScalarAsync());
write.CommandText = "SELECT COUNT(*) FROM Sample;";
Assert.Equal(4L, await write.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);
}
}