using Microsoft.Extensions.Configuration; using PLib.Application.Library; using PLib.Desktop.Services; using PLib.Desktop.Settings; using PLib.Infrastructure.Storage; using Shouldly; namespace PLib.Tests.Settings; /// /// The settings file is not just storage — it is a live configuration source. These tests /// close the loop: what the store writes has to be what the options binder reads back. /// public sealed class AppSettingsStoreTests : IDisposable { private readonly TempPaths _paths = new(); public void Dispose() => _paths.Dispose(); [Fact] public async Task Saved_settings_are_readable_by_the_configuration_binder() { var settings = new AppSettings { Folders = [@"C:\videos", @"D:\more videos"], ThumbnailWidth = 640, ThumbnailPositionRatio = 0.25, MaxIndexingConcurrency = 8, MinimumFileSizeInBytes = 2_097_152, Theme = ThemeMode.Light, }; await new JsonAppSettingsStore(_paths).SaveAsync(settings, Token); var (library, appearance) = Reload(); library.Folders.ShouldBe(settings.Folders); library.ThumbnailWidth.ShouldBe(640); library.ThumbnailPositionRatio.ShouldBe(0.25); library.MaxIndexingConcurrency.ShouldBe(8); library.MinimumFileSizeInBytes.ShouldBe(2_097_152); appearance.Theme.ShouldBe(ThemeMode.Light); } [Fact] public async Task Removing_a_folder_actually_shortens_the_stored_list() { var store = new JsonAppSettingsStore(_paths); var settings = Sample with { Folders = [@"C:\a", @"C:\b", @"C:\c"] }; await store.SaveAsync(settings, Token); await store.SaveAsync(settings with { Folders = [@"C:\a", @"C:\c"] }, Token); // Configuration merges arrays by index, so a shorter list is the case most likely // to leave a stale entry behind. Reload().Library.Folders.ShouldBe([@"C:\a", @"C:\c"]); } [Fact] public async Task Keys_the_settings_screen_does_not_know_about_survive_a_save() { await File.WriteAllTextAsync( Path.Combine(_paths.DataDirectory, "settings.json"), """{ "Library": { "VideoExtensions": [ ".mp4" ] }, "Experimental": { "Flag": true } }""", Token); await new JsonAppSettingsStore(_paths).SaveAsync(Sample, Token); var configuration = Build(); configuration["Experimental:Flag"].ShouldBe("True"); configuration["Library:VideoExtensions:0"].ShouldBe(".mp4"); } [Theory] [InlineData(true)] [InlineData(false)] public void Only_changes_that_affect_the_contents_of_the_library_ask_for_a_rescan(bool cosmeticOnly) { var changed = cosmeticOnly ? Sample with { ThumbnailWidth = 999, Theme = ThemeMode.Dark } : Sample with { Folders = [@"C:\elsewhere"] }; changed.RequiresRescanComparedTo(Sample).ShouldBe(!cosmeticOnly); } private static CancellationToken Token => TestContext.Current.CancellationToken; private static AppSettings Sample => new() { Folders = [@"C:\videos"], ThumbnailWidth = 480, ThumbnailPositionRatio = 0.15, MaxIndexingConcurrency = 4, MinimumFileSizeInBytes = 65_536, Theme = ThemeMode.System, }; private IConfigurationRoot Build() => new ConfigurationBuilder() .AddJsonFile(Path.Combine(_paths.DataDirectory, "settings.json"), optional: false) .Build(); private (LibraryOptions Library, AppearanceOptions Appearance) Reload() { var configuration = Build(); var library = new LibraryOptions(); configuration.GetSection(LibraryOptions.SectionName).Bind(library); var appearance = new AppearanceOptions(); configuration.GetSection(AppearanceOptions.SectionName).Bind(appearance); return (library, appearance); } private sealed class TempPaths : IAppPaths, IDisposable { public TempPaths() { DataDirectory = Path.Combine(Path.GetTempPath(), $"plib-tests-{Guid.CreateVersion7()}"); ThumbnailDirectory = Path.Combine(DataDirectory, "thumbnails"); DatabaseFile = Path.Combine(DataDirectory, "library.db"); Directory.CreateDirectory(ThumbnailDirectory); } public string DataDirectory { get; } public string ThumbnailDirectory { get; } public string DatabaseFile { get; } public void Dispose() => Directory.Delete(DataDirectory, recursive: true); } }