167 lines
7.1 KiB
C#
167 lines
7.1 KiB
C#
|
|
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!;
|
||
|
|
}
|
||
|
|
}
|