diff --git a/src/Core/Storage/AppPaths.cs b/src/Core/Storage/AppPaths.cs index 2d1e474..3a69cf2 100644 --- a/src/Core/Storage/AppPaths.cs +++ b/src/Core/Storage/AppPaths.cs @@ -21,6 +21,13 @@ public static class AppPaths /// Explicit override of the data root (e.g. CLI --data-dir). Takes priority over everything. public static string? OverrideDataRoot { get; set; } + // Test seams: redirect the machine-global bootstrap locations (APPDATA pointer + // dir, executable dir, per-user default root) to a sandbox so the resolution + // precedence can be exercised without touching real user data. + internal static string? BootstrapDirOverride { get; set; } + internal static string? PortableBaseOverride { get; set; } + internal static string? DefaultRootOverride { get; set; } + /// /// Default data root, following each platform's convention: /// Windows → %APPDATA%\PalladiumWallet (PascalCase, like Electrum/Bitcoin); @@ -29,6 +36,8 @@ public static class AppPaths /// public static string DefaultDataRoot() { + if (DefaultRootOverride is { } o) + return o; if (OperatingSystem.IsWindows()) return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @@ -41,11 +50,12 @@ public static class AppPaths /// bootstrap location that is always writable and independent of the data root. private static string LocationPointerPath() => Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - AppDirName, "data-location"); + BootstrapDirOverride ?? Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppDirName), + "data-location"); private static string PortableRoot() => - Path.Combine(AppContext.BaseDirectory, PortableDirName); + Path.Combine(PortableBaseOverride ?? AppContext.BaseDirectory, PortableDirName); private static bool HasData(string root) => Directory.Exists(root) && Directory.EnumerateFileSystemEntries(root).Any(); diff --git a/tests/PalladiumWallet.Tests/Storage/AppPathsTests.cs b/tests/PalladiumWallet.Tests/Storage/AppPathsTests.cs index b5e3be1..731a1d6 100644 --- a/tests/PalladiumWallet.Tests/Storage/AppPathsTests.cs +++ b/tests/PalladiumWallet.Tests/Storage/AppPathsTests.cs @@ -7,9 +7,11 @@ namespace PalladiumWallet.Tests.Storage; /// Tests for the data-path resolution (§8), pinned to a temporary root via /// — the same seam the Android head and /// the CLI --data-dir use — so nothing outside the temp folder is touched. -/// The pointer-file and portable-mode branches read machine-global locations -/// and are deliberately not exercised here. +/// The pointer/portable/default precedence has its own sandboxed tests in +/// ; both classes share a collection +/// because AppPaths state is static. /// +[Collection("AppPaths")] public class AppPathsTests : IDisposable { private readonly string _root; @@ -82,3 +84,98 @@ public class AppPathsTests : IDisposable Assert.NotEqual(AppPaths.WalletsDir(NetKind.Testnet), AppPaths.WalletsDir(NetKind.Regtest)); } } + +/// +/// Precedence tests for — override → portable → +/// pointer → default — using the internal bootstrap seams to sandbox the +/// machine-global locations (APPDATA pointer dir, executable dir, default root). +/// +[Collection("AppPaths")] +public sealed class AppPathsResolutionTests : IDisposable +{ + private readonly string _sandbox; + private readonly string? _savedOverride; + + public AppPathsResolutionTests() + { + _sandbox = Path.Combine(Path.GetTempPath(), $"plm-paths-{Guid.NewGuid()}"); + Directory.CreateDirectory(_sandbox); + _savedOverride = AppPaths.OverrideDataRoot; + AppPaths.OverrideDataRoot = null; + AppPaths.BootstrapDirOverride = Path.Combine(_sandbox, "bootstrap"); + AppPaths.PortableBaseOverride = Path.Combine(_sandbox, "exe"); + AppPaths.DefaultRootOverride = Path.Combine(_sandbox, "default-root"); + Directory.CreateDirectory(AppPaths.PortableBaseOverride); + } + + public void Dispose() + { + AppPaths.OverrideDataRoot = _savedOverride; + AppPaths.BootstrapDirOverride = null; + AppPaths.PortableBaseOverride = null; + AppPaths.DefaultRootOverride = null; + if (Directory.Exists(_sandbox)) + Directory.Delete(_sandbox, recursive: true); + } + + private string PortableDir => Path.Combine(_sandbox, "exe", AppPaths.PortableDirName); + + [Fact] + public void Senza_alcuna_configurazione_vince_il_default_e_la_posizione_non_e_configurata() + { + Assert.Equal(Path.Combine(_sandbox, "default-root"), AppPaths.DataRoot()); + Assert.False(AppPaths.IsDataLocationConfigured()); + } + + [Fact] + public void La_cartella_portable_accanto_all_eseguibile_vince_sul_pointer_e_sul_default() + { + AppPaths.ConfigureDataLocation(Path.Combine(_sandbox, "custom")); + Directory.CreateDirectory(PortableDir); + + Assert.Equal(PortableDir, AppPaths.DataRoot()); + Assert.True(AppPaths.IsDataLocationConfigured()); + } + + [Fact] + public void Il_pointer_scritto_da_ConfigureDataLocation_vince_sul_default() + { + var custom = Path.Combine(_sandbox, "custom"); + AppPaths.ConfigureDataLocation($" {custom} "); // trims and creates + + Assert.True(Directory.Exists(custom)); + Assert.Equal(custom, AppPaths.DataRoot()); + Assert.True(AppPaths.IsDataLocationConfigured()); + } + + [Fact] + public void Un_pointer_vuoto_viene_ignorato_e_si_ricade_sul_default() + { + Directory.CreateDirectory(AppPaths.BootstrapDirOverride!); + File.WriteAllText(Path.Combine(AppPaths.BootstrapDirOverride!, "data-location"), " "); + + Assert.Equal(Path.Combine(_sandbox, "default-root"), AppPaths.DataRoot()); + Assert.False(AppPaths.IsDataLocationConfigured()); + } + + [Fact] + public void Dati_gia_presenti_nel_default_contano_come_posizione_configurata() + { + var defaultRoot = Path.Combine(_sandbox, "default-root"); + Directory.CreateDirectory(defaultRoot); + Assert.False(AppPaths.IsDataLocationConfigured()); // exists but empty + + File.WriteAllText(Path.Combine(defaultRoot, "config.json"), "{}"); + Assert.True(AppPaths.IsDataLocationConfigured()); + } + + [Fact] + public void L_override_esplicito_vince_anche_su_portable_e_pointer() + { + Directory.CreateDirectory(PortableDir); + AppPaths.ConfigureDataLocation(Path.Combine(_sandbox, "custom")); + AppPaths.OverrideDataRoot = Path.Combine(_sandbox, "override"); + + Assert.Equal(Path.Combine(_sandbox, "override"), AppPaths.DataRoot()); + } +}