using Duempelkas.Domain.Enums; using Duempelkas.Infrastructure; using Duempelkas.Infrastructure.Persistence; using Duempelkas.Infrastructure.Services; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Duempelkas.Tests; public sealed class BackupServiceTests : IDisposable { private readonly string folder = Path.Combine(Path.GetTempPath(), "duempelkas-tests-" + Guid.NewGuid().ToString("N")); private readonly ServiceProvider services; private readonly BackupService backup; private readonly EntryService entries; private readonly int accountId; public BackupServiceTests() { Directory.CreateDirectory(folder); services = new ServiceCollection().AddInfrastructure($"Data Source={Path.Combine(folder, "active.db")};Pooling=False").BuildServiceProvider(); using var db = services.GetRequiredService>().CreateDbContext(); db.Database.Migrate(); db.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;"); db.Accounts.Add(new() { Name = "Isolierte Testkasse" }); db.SaveChanges(); accountId = db.Accounts.Single().Id; backup = new BackupService(services.GetRequiredService()); entries = (EntryService)services.GetRequiredService(); } [Fact] public async Task WalSnapshot_RestoresExactState_AndKeepsFallback() { var first = await entries.CreateEntryAsync(accountId, EntryType.Income, new(2026, 1, 1), "Gesichert", 12.50m); var path = Path.Combine(folder, "snapshot.bak"); await backup.CreateBackupToAsync(path); await entries.CreateEntryAsync(accountId, EntryType.Expense, new(2026, 1, 2), "Später", 4.20m); var fallback = await backup.RestoreFromAsync(path); var restored = await entries.GetEntriesAsync(accountId, (int?)null); Assert.Single(restored); Assert.Equal(first, restored[0]); using var old = new SqliteConnection($"Data Source={fallback};Mode=ReadOnly;Pooling=False"); old.Open(); using var command = old.CreateCommand(); command.CommandText = "SELECT COUNT(*) FROM Entries"; Assert.Equal(2L, command.ExecuteScalar()); } [Theory] [InlineData(false)] [InlineData(true)] public async Task InvalidBackup_DoesNotChangeActiveData(bool sqlite) { await entries.CreateEntryAsync(accountId, EntryType.Income, new(2026, 1, 1), "Bleibt", 10m); var path = Path.Combine(folder, "invalid.bak"); if (sqlite) { using var connection = new SqliteConnection($"Data Source={path};Pooling=False"); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "CREATE TABLE Unrelated (Id INTEGER)"; command.ExecuteNonQuery(); } else await File.WriteAllTextAsync(path, "not a database"); await Assert.ThrowsAnyAsync(() => backup.RestoreFromAsync(path)); Assert.Single(await entries.GetEntriesAsync(accountId, (int?)null)); } [Fact] public async Task MissingSource_AndActiveDestination_AreRejected() { await Assert.ThrowsAnyAsync(() => backup.RestoreFromAsync(Path.Combine(folder, "missing.bak"))); await Assert.ThrowsAnyAsync(() => backup.CreateBackupToAsync(Path.Combine(folder, "active.db"))); } public void Dispose() { services.Dispose(); Directory.Delete(folder, true); } }