using YKanBan.Storage.AppData; using YKanBan.Tests.TestUtilities; namespace YKanBan.Tests.Storage.AppData; /// /// Tests for app.json round-tripping, kebab-case naming and graceful /// degradation on a missing or corrupt file. /// [TestClass] public class AppConfigStoreTests { [TestMethod] public void LoadMissingFileReturnsDefaults() { using var directory = new TempDirectory(); var store = new AppConfigStore(Path.Combine(directory.FullPath, "app.json")); AppConfig config = store.Load(); Assert.AreEqual(AppConfig.CurrentFormatVersion, config.Version); Assert.AreEqual(AppConfig.DefaultLanguage, config.Language); Assert.AreEqual(ThemeOption.FollowSystem, config.Theme); Assert.AreEqual(SortOption.IdAscending, config.Display.CardSort); Assert.AreEqual(SortOption.IdAscending, config.Display.ColumnSort); Assert.AreEqual(ColumnWidthPreset.Standard, config.Display.ColumnWidth.Preset); Assert.AreEqual(320, config.Display.ColumnWidth.CustomPixels); Assert.IsTrue(config.Confirmations.DeleteCard); Assert.IsTrue(config.Confirmations.DeleteColumn); Assert.IsTrue(config.Confirmations.DeleteTag); Assert.IsTrue(config.Confirmations.DiscardEdit); } [TestMethod] public void LoadCorruptFileBacksUpAndReturnsDefaults() { using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); File.WriteAllText(path, "{ this is not valid json"); var store = new AppConfigStore(path); AppConfig config = store.Load(); Assert.AreEqual(AppConfig.CurrentFormatVersion, config.Version); Assert.IsTrue(File.Exists(path + ".bak")); Assert.AreEqual("{ this is not valid json", File.ReadAllText(path + ".bak")); } [TestMethod] public void LoadJsonNullReturnsDefaults() { using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); File.WriteAllText(path, "null"); var store = new AppConfigStore(path); Assert.AreEqual(AppConfig.CurrentFormatVersion, store.Load().Version); } [TestMethod] public void LoadNumericEnumValueIsRejectedAndBackedUp() { using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); // Integer enum values are not accepted; the file is treated like corrupt. File.WriteAllText(path, """{ "theme": 1 }"""); var store = new AppConfigStore(path); AppConfig config = store.Load(); Assert.AreEqual(ThemeOption.FollowSystem, config.Theme); Assert.IsTrue(File.Exists(path + ".bak")); } [TestMethod] public void LoadUnreadableFileReturnsDefaults() { // File sharing is only reliably enforced on Windows, so this branch is exercised there. if (!OperatingSystem.IsWindows()) { return; } using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); File.WriteAllText(path, """{ "version": 1 }"""); // Hold the file exclusively so the store's read fails with an I/O error. using FileStream hold = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None); var store = new AppConfigStore(path); Assert.AreEqual(AppConfig.CurrentFormatVersion, store.Load().Version); } [TestMethod] public void LoadReadsKebabCaseKeysAndEnumValues() { using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); File.WriteAllText(path, $$""" { "version": {{AppConfig.CurrentFormatVersion}}, "language": "zh-Hans", "theme": "dark", "display": { "column-width": { "preset": "custom", "custom-pixels": 400 }, "card-sort": "title-asc", "column-sort": "id-desc" }, "confirmations": { "delete-card": false, "delete-column": false, "delete-tag": false, "discard-edit": false } } """); var store = new AppConfigStore(path); AppConfig config = store.Load(); Assert.AreEqual("zh-Hans", config.Language); Assert.AreEqual(ThemeOption.Dark, config.Theme); Assert.AreEqual(SortOption.TitleAscending, config.Display.CardSort); Assert.AreEqual(SortOption.IdDescending, config.Display.ColumnSort); Assert.AreEqual(ColumnWidthPreset.Custom, config.Display.ColumnWidth.Preset); Assert.AreEqual(400, config.Display.ColumnWidth.CustomPixels); Assert.IsFalse(config.Confirmations.DeleteCard); Assert.IsFalse(config.Confirmations.DeleteColumn); Assert.IsFalse(config.Confirmations.DeleteTag); Assert.IsFalse(config.Confirmations.DiscardEdit); } [TestMethod] public void LoadMissingSectionsFillsDefaults() { using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); File.WriteAllText(path, $$"""{ "version": {{AppConfig.CurrentFormatVersion}} }"""); var store = new AppConfigStore(path); AppConfig config = store.Load(); Assert.IsNotNull(config.Display); Assert.IsNotNull(config.Display.ColumnWidth); Assert.IsNotNull(config.Confirmations); Assert.AreEqual(ColumnWidthPreset.Standard, config.Display.ColumnWidth.Preset); } [TestMethod] public void SaveWritesKebabCaseKeysAndEnumValues() { using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); var store = new AppConfigStore(path); var config = new AppConfig { Theme = ThemeOption.FollowSystem, Display = new DisplaySettings { CardSort = SortOption.UpdatedDescending, ColumnSort = SortOption.CreatedAscending, ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Custom, CustomPixels = 500 }, }, Confirmations = new ConfirmationSettings { DeleteColumn = false }, }; store.Save(config); string json = File.ReadAllText(path); StringAssert.Contains(json, "\"delete-column\""); StringAssert.Contains(json, "\"discard-edit\""); StringAssert.Contains(json, "\"card-sort\""); StringAssert.Contains(json, "\"column-sort\""); StringAssert.Contains(json, "\"custom-pixels\""); StringAssert.Contains(json, "\"follow-system\""); StringAssert.Contains(json, "\"updated-desc\""); StringAssert.Contains(json, "\"created-asc\""); // No member-name CamelCase must leak into the file. Assert.IsFalse(json.Contains("deleteColumn", StringComparison.Ordinal)); Assert.IsFalse(json.Contains("CustomPixels", StringComparison.Ordinal)); } [TestMethod] public void SaveThenLoadRoundTrips() { using var directory = new TempDirectory(); string path = Path.Combine(directory.FullPath, "app.json"); var store = new AppConfigStore(path); var config = new AppConfig { Language = "zh-Hans", Theme = ThemeOption.Light, Display = new DisplaySettings { CardSort = SortOption.CreatedDescending, ColumnSort = SortOption.TitleAscending, ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Wide }, }, Confirmations = new ConfirmationSettings { DiscardEdit = false }, }; store.Save(config); AppConfig reloaded = store.Load(); Assert.AreEqual(config.Language, reloaded.Language); Assert.AreEqual(config.Theme, reloaded.Theme); Assert.AreEqual(config.Display.CardSort, reloaded.Display.CardSort); Assert.AreEqual(config.Display.ColumnSort, reloaded.Display.ColumnSort); Assert.AreEqual(config.Display.ColumnWidth.Preset, reloaded.Display.ColumnWidth.Preset); Assert.AreEqual(config.Confirmations.DiscardEdit, reloaded.Confirmations.DiscardEdit); } }