feat: implement database backup and restore functionality with JSON support
All checks were successful
Publish Container / publish (push) Successful in 3m53s
All checks were successful
Publish Container / publish (push) Successful in 3m53s
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
e872fe200b
commit
e8bbae0496
8 changed files with 657 additions and 0 deletions
167
tests/WorkTracker.Tests/JsonDatabaseBackupServiceTests.cs
Normal file
167
tests/WorkTracker.Tests/JsonDatabaseBackupServiceTests.cs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Couchbase.Lite;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using WorkTracker.Configuration;
|
||||
using WorkTracker.Domain;
|
||||
using WorkTracker.Services.Settings;
|
||||
using WorkTracker.Services.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace WorkTracker.Tests;
|
||||
|
||||
public sealed class JsonDatabaseBackupServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExportAndImport_RoundTripsManagedCollectionsAndVersions()
|
||||
{
|
||||
using var sourceDatabase = new TestDatabaseHandle();
|
||||
await SeedAsync(sourceDatabase.Provider);
|
||||
|
||||
var backupService = new JsonDatabaseBackupService(sourceDatabase.Provider);
|
||||
var backupFile = await backupService.ExportAsync();
|
||||
|
||||
using var backupJson = JsonDocument.Parse(backupFile.Content);
|
||||
Assert.Equal(1, backupJson.RootElement.GetProperty("backupFormatVersion").GetInt32());
|
||||
Assert.Equal(CouchbaseLiteDatabaseProvider.CurrentDatabaseSchemaVersion, backupJson.RootElement.GetProperty("databaseSchemaVersion").GetInt32());
|
||||
Assert.True(backupJson.RootElement.GetProperty("collections").TryGetProperty("app_settings", out _));
|
||||
Assert.True(backupJson.RootElement.GetProperty("collections").TryGetProperty("users", out _));
|
||||
Assert.True(backupJson.RootElement.GetProperty("collections").TryGetProperty("workdays", out _));
|
||||
|
||||
using var targetDatabase = new TestDatabaseHandle();
|
||||
var importService = new JsonDatabaseBackupService(targetDatabase.Provider);
|
||||
|
||||
await using (var stream = new MemoryStream(backupFile.Content, writable: false))
|
||||
{
|
||||
await importService.ImportAsync(stream);
|
||||
}
|
||||
|
||||
var appSettingsService = new CouchbaseLiteAppSettingsService(targetDatabase.Provider);
|
||||
var importedSettings = await appSettingsService.GetAsync();
|
||||
|
||||
Assert.Equal(AppThemeMode.Dark, importedSettings.ThemeMode);
|
||||
Assert.Equal("USD", importedSettings.Currency);
|
||||
Assert.Equal("en-US", importedSettings.Locale);
|
||||
Assert.Equal(CouchbaseLiteDatabaseProvider.CurrentDatabaseSchemaVersion, targetDatabase.Provider.DatabaseSchemaVersion);
|
||||
|
||||
var importedUser = targetDatabase.Provider.Users.GetDocument("USER1");
|
||||
Assert.NotNull(importedUser);
|
||||
Assert.Equal("admin", importedUser!.GetString("username"));
|
||||
|
||||
var importedWorkDay = targetDatabase.Provider.WorkDays.GetDocument("2026-04-24");
|
||||
Assert.NotNull(importedWorkDay);
|
||||
Assert.Equal("2026-04-24", importedWorkDay!.GetString("date"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_WhenSchemaVersionDoesNotMatch_RejectsWithoutOverwritingDatabase()
|
||||
{
|
||||
using var sourceDatabase = new TestDatabaseHandle();
|
||||
await SeedAsync(sourceDatabase.Provider);
|
||||
|
||||
var backupService = new JsonDatabaseBackupService(sourceDatabase.Provider);
|
||||
var backupFile = await backupService.ExportAsync();
|
||||
|
||||
var modifiedBackup = JsonNode.Parse(backupFile.Content)!;
|
||||
modifiedBackup["databaseSchemaVersion"] = CouchbaseLiteDatabaseProvider.CurrentDatabaseSchemaVersion + 1;
|
||||
var modifiedContent = Encoding.UTF8.GetBytes(modifiedBackup.ToJsonString(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
|
||||
|
||||
using var targetDatabase = new TestDatabaseHandle();
|
||||
SeedExistingTargetData(targetDatabase.Provider);
|
||||
var importService = new JsonDatabaseBackupService(targetDatabase.Provider);
|
||||
|
||||
await using var stream = new MemoryStream(modifiedContent, writable: false);
|
||||
var exception = await Assert.ThrowsAsync<DatabaseBackupException>(() => importService.ImportAsync(stream));
|
||||
|
||||
Assert.Contains("not supported", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotNull(targetDatabase.Provider.Users.GetDocument("KEEP"));
|
||||
Assert.Null(targetDatabase.Provider.Users.GetDocument("USER1"));
|
||||
}
|
||||
|
||||
private static async Task SeedAsync(CouchbaseLiteDatabaseProvider provider)
|
||||
{
|
||||
var appSettingsService = new CouchbaseLiteAppSettingsService(provider);
|
||||
await appSettingsService.SaveAsync(new AppSettingsDocument
|
||||
{
|
||||
ThemeMode = AppThemeMode.Dark,
|
||||
Currency = "USD",
|
||||
Locale = "en-US",
|
||||
StandardWorkHoursPerDay = 7.5m,
|
||||
HourlyGrossRate = 30m,
|
||||
ProfitabilityCoefficient = 0.8m,
|
||||
InpsRate = 0.2m,
|
||||
SubstituteTaxRate = 0.1m
|
||||
});
|
||||
|
||||
var userDocument = new MutableDocument("USER1");
|
||||
userDocument.SetString("username", "admin");
|
||||
userDocument.SetString("usernameNormalized", "ADMIN");
|
||||
userDocument.SetString("passwordHash", "hash");
|
||||
userDocument.SetBoolean("mustChangePassword", false);
|
||||
provider.Users.Save(userDocument);
|
||||
|
||||
var workDayDocument = new MutableDocument("2026-04-24");
|
||||
workDayDocument.SetString("date", "2026-04-24");
|
||||
workDayDocument.SetBoolean("isWeekend", false);
|
||||
workDayDocument.SetBoolean("isItalianFestivity", false);
|
||||
workDayDocument.SetArray("workUnits", new MutableArrayObject());
|
||||
workDayDocument.SetArray("calendarEvents", new MutableArrayObject());
|
||||
workDayDocument.SetString("createdAtUtc", "2026-04-24T08:00:00.0000000+00:00");
|
||||
workDayDocument.SetString("updatedAtUtc", "2026-04-24T08:00:00.0000000+00:00");
|
||||
provider.WorkDays.Save(workDayDocument);
|
||||
}
|
||||
|
||||
private static void SeedExistingTargetData(CouchbaseLiteDatabaseProvider provider)
|
||||
{
|
||||
var existingUser = new MutableDocument("KEEP");
|
||||
existingUser.SetString("username", "keep-me");
|
||||
existingUser.SetString("usernameNormalized", "KEEP-ME");
|
||||
existingUser.SetString("passwordHash", "keep-hash");
|
||||
existingUser.SetBoolean("mustChangePassword", false);
|
||||
provider.Users.Save(existingUser);
|
||||
}
|
||||
|
||||
private sealed class TestDatabaseHandle : IDisposable
|
||||
{
|
||||
private readonly string directoryPath;
|
||||
|
||||
public TestDatabaseHandle()
|
||||
{
|
||||
directoryPath = Path.Combine(Path.GetTempPath(), "WorkTracker.Tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
|
||||
Provider = new CouchbaseLiteDatabaseProvider(
|
||||
Options.Create(new CouchbaseLiteOptions
|
||||
{
|
||||
Directory = directoryPath,
|
||||
DatabaseName = $"worktracker-tests-{Guid.NewGuid():N}"
|
||||
}),
|
||||
new TestHostEnvironment());
|
||||
}
|
||||
|
||||
public CouchbaseLiteDatabaseProvider Provider { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Provider.Dispose();
|
||||
|
||||
if (Directory.Exists(directoryPath))
|
||||
{
|
||||
Directory.Delete(directoryPath, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestHostEnvironment : IHostEnvironment
|
||||
{
|
||||
public string EnvironmentName { get; set; } = "Development";
|
||||
|
||||
public string ApplicationName { get; set; } = "WorkTracker.Tests";
|
||||
|
||||
public string ContentRootPath { get; set; } = Directory.GetCurrentDirectory();
|
||||
|
||||
public Microsoft.Extensions.FileProviders.IFileProvider ContentRootFileProvider { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue