From 6979ab445047f57deaf26c439e47578c1c65f362 Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Tue, 25 Aug 2020 14:21:14 -0400 Subject: [PATCH] Libation 4.0 prep: account management complete --- InternalUtilities/AccountsSettings.cs | 56 +++---- .../UNTESTED/AudibleApiStorage.cs | 4 +- LibationLauncher/LibationLauncher.csproj | 2 +- .../Dialogs/AccountsDialog.Designer.cs | 12 -- .../UNTESTED/Dialogs/AccountsDialog.cs | 110 ++++++++------ .../Dialogs/AccountsDialog.Designer.cs | 12 -- .../InternalUtilities.Tests/AccountTests.cs | 138 ++++++++++++------ 7 files changed, 198 insertions(+), 136 deletions(-) diff --git a/InternalUtilities/AccountsSettings.cs b/InternalUtilities/AccountsSettings.cs index 29d8bac6..2d57d640 100644 --- a/InternalUtilities/AccountsSettings.cs +++ b/InternalUtilities/AccountsSettings.cs @@ -51,37 +51,17 @@ namespace InternalUtilities public IReadOnlyList Accounts => _accounts_json.AsReadOnly(); #endregion + #region de/serialize public static AccountsSettings FromJson(string json) => JsonConvert.DeserializeObject(json, Identity.GetJsonSerializerSettings()); public string ToJson(Formatting formatting = Formatting.Indented) => JsonConvert.SerializeObject(this, formatting, Identity.GetJsonSerializerSettings()); - - public void Add(Account account) - { - _add(account); - update_no_validate(); - } - - public void _add(Account account) - { - validate(account); - - _accounts_backing.Add(account); - account.Updated += update; - } + #endregion // more common naming convention alias for internal collection public IReadOnlyList GetAll() => Accounts; - public Account GetAccount(string accountId, string locale) - { - if (locale is null) - return null; - - return Accounts.SingleOrDefault(a => a.AccountId == accountId && a.IdentityTokens.Locale.Name == locale); - } - public Account Upsert(string accountId, string locale) { var acct = GetAccount(accountId, locale); @@ -97,13 +77,26 @@ namespace InternalUtilities return account; } - public bool Delete(Account account) + public void Add(Account account) { - if (!_accounts_backing.Contains(account)) - return false; + _add(account); + update_no_validate(); + } - account.Updated -= update; - return _accounts_backing.Remove(account); + public void _add(Account account) + { + validate(account); + + _accounts_backing.Add(account); + account.Updated += update; + } + + public Account GetAccount(string accountId, string locale) + { + if (locale is null) + return null; + + return Accounts.SingleOrDefault(a => a.AccountId == accountId && a.IdentityTokens.Locale.Name == locale); } public bool Delete(string accountId, string locale) @@ -114,6 +107,15 @@ namespace InternalUtilities return Delete(acct); } + public bool Delete(Account account) + { + if (!_accounts_backing.Contains(account)) + return false; + + account.Updated -= update; + return _accounts_backing.Remove(account); + } + private void validate(Account account) { ArgumentValidator.EnsureNotNull(account, nameof(account)); diff --git a/InternalUtilities/UNTESTED/AudibleApiStorage.cs b/InternalUtilities/UNTESTED/AudibleApiStorage.cs index ffd68e62..3afd17fa 100644 --- a/InternalUtilities/UNTESTED/AudibleApiStorage.cs +++ b/InternalUtilities/UNTESTED/AudibleApiStorage.cs @@ -26,8 +26,8 @@ namespace InternalUtilities public static Account TEST_GetFirstAccount() => GetPersistentAccountsSettings().GetAll().FirstOrDefault(); - public static AccountsSettings GetPersistentAccountsSettings() - => new AccountsSettingsPersister(AccountsSettingsFile).AccountsSettings; + public static AccountsSettings GetPersistentAccountsSettings() => GetAccountsSettingsPersister().AccountsSettings; + public static AccountsSettingsPersister GetAccountsSettingsPersister() => new AccountsSettingsPersister(AccountsSettingsFile); public static string GetIdentityTokensJsonPath(this Account account) => GetIdentityTokensJsonPath(account.AccountId, account.Locale?.Name); diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index af452c58..7780f682 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 3.1.12.260 + 3.1.12.268 diff --git a/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.Designer.cs b/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.Designer.cs index f4d90064..ed64f90e 100644 --- a/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.Designer.cs +++ b/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.Designer.cs @@ -31,7 +31,6 @@ this.cancelBtn = new System.Windows.Forms.Button(); this.saveBtn = new System.Windows.Forms.Button(); this.dataGridView1 = new System.Windows.Forms.DataGridView(); - this.Original = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn(); this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn(); @@ -71,7 +70,6 @@ this.dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells; this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { - this.Original, this.DeleteAccount, this.LibraryScan, this.AccountId, @@ -85,15 +83,6 @@ this.dataGridView1.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView1_CellContentClick); this.dataGridView1.DefaultValuesNeeded += new System.Windows.Forms.DataGridViewRowEventHandler(this.dataGridView1_DefaultValuesNeeded); // - // Original - // - this.Original.HeaderText = "Original"; - this.Original.Name = "Original"; - this.Original.ReadOnly = true; - this.Original.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable; - this.Original.Visible = false; - this.Original.Width = 48; - // // DeleteAccount // this.DeleteAccount.HeaderText = "Delete"; @@ -148,7 +137,6 @@ private System.Windows.Forms.Button cancelBtn; private System.Windows.Forms.Button saveBtn; private System.Windows.Forms.DataGridView dataGridView1; - private System.Windows.Forms.DataGridViewTextBoxColumn Original; private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount; private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan; private System.Windows.Forms.DataGridViewTextBoxColumn AccountId; diff --git a/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs b/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs index 4b617e2a..3e534a9a 100644 --- a/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs +++ b/LibationWinForms/UNTESTED/Dialogs/AccountsDialog.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Windows.Forms; using AudibleApi; @@ -8,9 +9,6 @@ namespace LibationWinForms.Dialogs { public partial class AccountsDialog : Form { - const string NON_BREAKING_SPACE = "\u00a0"; - - const string COL_Original = nameof(Original); const string COL_Delete = nameof(DeleteAccount); const string COL_LibraryScan = nameof(LibraryScan); const string COL_AccountId = nameof(AccountId); @@ -28,12 +26,6 @@ namespace LibationWinForms.Dialogs populateGridValues(); } - struct OriginalValue - { - public string AccountId { get; set; } - public string LocaleName { get; set; } - } - private void populateDropDown() => (dataGridView1.Columns[COL_Locale] as DataGridViewComboBoxColumn).DataSource = Localization.Locales @@ -51,7 +43,6 @@ namespace LibationWinForms.Dialogs foreach (var account in accounts) dataGridView1.Rows.Add( - new OriginalValue { AccountId = account.AccountId, LocaleName = account.Locale.Name }, "X", account.LibraryScan, account.AccountId, @@ -61,7 +52,6 @@ namespace LibationWinForms.Dialogs private void dataGridView1_DefaultValuesNeeded(object sender, DataGridViewRowEventArgs e) { - e.Row.Cells[COL_Original].Value = new OriginalValue(); e.Row.Cells[COL_Delete].Value = "X"; e.Row.Cells[COL_LibraryScan].Value = true; } @@ -101,44 +91,82 @@ namespace LibationWinForms.Dialogs private void cancelBtn_Click(object sender, EventArgs e) => this.Close(); + class AccountDto + { + public string AccountId { get; set; } + public string AccountName { get; set; } + public string LocaleName { get; set; } + public bool LibraryScan { get; set; } + } + private void saveBtn_Click(object sender, EventArgs e) { - foreach (DataGridViewRow row in this.dataGridView1.Rows) + try { - if (row.IsNewRow) - continue; + // without transaction, accounts persister will write ANY EDIT immediately to file. + var persister = AudibleApiStorage.GetAccountsSettingsPersister(); + persister.BeginTransation(); - var original = (OriginalValue)row.Cells[COL_Original].Value; - var originalAccountId = original.AccountId; - var originalLocaleName = original.LocaleName; + persist(persister.AccountsSettings); - var libraryScan = (bool)row.Cells[COL_LibraryScan].Value; - var accountId = (string)row.Cells[COL_AccountId].Value; - var localeName = (string)row.Cells[COL_Locale].Value; - var accountName = (string)row.Cells[COL_AccountName].Value; + persister.CommitTransation(); + + this.Close(); } - - // WARNING: accounts persister will write ANY EDIT immediately to file. - // Take NO action on persistent objects until the end - var accountsSettings = AudibleApiStorage.GetPersistentAccountsSettings(); - - var existingAccounts = accountsSettings.Accounts; - - foreach (var account in existingAccounts) + catch (Exception ex) { + MessageBox.Show($"Error: {ex.Message}", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error); } - - // editing account id is a special case - // an account is defined by its account id, therefore this is really a different account. the user won't care about this distinction though - - // added: find and validate - - // edited: find and validate - - // deleted: find - - // persist - } + + private void persist(AccountsSettings accountsSettings) + { + var existingAccounts = accountsSettings.Accounts; + var dtos = getRowDtos(); + + // editing account id is a special case. an account is defined by its account id, therefore this is really a different account. the user won't care about this distinction though. + // these will be caught below by normal means and re-created minus the convenience of persisting identity tokens + + // delete + for (var i = existingAccounts.Count - 1; i >= 0; i--) + { + var existing = existingAccounts[i]; + if (!dtos.Any(dto => + dto.AccountId?.ToLower().Trim() == existing.AccountId.ToLower() + && dto.LocaleName == existing.Locale?.Name)) + { + accountsSettings.Delete(existing); + } + } + + // upsert each. validation occurs through Account and AccountsSettings + foreach (var dto in dtos) + { + if (string.IsNullOrWhiteSpace(dto.AccountId)) + throw new Exception("Please enter an account id for all accounts"); + if (string.IsNullOrWhiteSpace(dto.LocaleName)) + throw new Exception("Please select a local name for all accounts"); + + var acct = accountsSettings.Upsert(dto.AccountId, dto.LocaleName); + acct.LibraryScan = dto.LibraryScan; + acct.AccountName + = string.IsNullOrWhiteSpace(dto.AccountName) + ? $"{dto.AccountId} - {dto.LocaleName}" + : dto.AccountName.Trim(); + } + } + + private List getRowDtos() + => dataGridView1.Rows + .Cast() + .Where(r => !r.IsNewRow) + .Select(r => new AccountDto + { + AccountId = (string)r.Cells[COL_AccountId].Value, + AccountName = (string)r.Cells[COL_AccountName].Value, + LocaleName = (string)r.Cells[COL_Locale].Value, + LibraryScan = (bool)r.Cells[COL_LibraryScan].Value + }) + .ToList(); } } diff --git a/WinFormsDesigner/Dialogs/AccountsDialog.Designer.cs b/WinFormsDesigner/Dialogs/AccountsDialog.Designer.cs index bf3c6d57..b4c50276 100644 --- a/WinFormsDesigner/Dialogs/AccountsDialog.Designer.cs +++ b/WinFormsDesigner/Dialogs/AccountsDialog.Designer.cs @@ -31,7 +31,6 @@ this.cancelBtn = new System.Windows.Forms.Button(); this.saveBtn = new System.Windows.Forms.Button(); this.dataGridView1 = new System.Windows.Forms.DataGridView(); - this.Original = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn(); this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn(); @@ -69,7 +68,6 @@ this.dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells; this.dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { - this.Original, this.DeleteAccount, this.LibraryScan, this.AccountId, @@ -81,15 +79,6 @@ this.dataGridView1.Size = new System.Drawing.Size(776, 397); this.dataGridView1.TabIndex = 0; // - // Original - // - this.Original.HeaderText = "Original"; - this.Original.Name = "Original"; - this.Original.ReadOnly = true; - this.Original.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable; - this.Original.Visible = false; - this.Original.Width = 48; - // // DeleteAccount // this.DeleteAccount.HeaderText = "Delete"; @@ -144,7 +133,6 @@ private System.Windows.Forms.Button cancelBtn; private System.Windows.Forms.Button saveBtn; private System.Windows.Forms.DataGridView dataGridView1; - private System.Windows.Forms.DataGridViewTextBoxColumn Original; private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount; private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan; private System.Windows.Forms.DataGridViewTextBoxColumn AccountId; diff --git a/_Tests/InternalUtilities.Tests/AccountTests.cs b/_Tests/InternalUtilities.Tests/AccountTests.cs index 76ced71d..ad555c5c 100644 --- a/_Tests/InternalUtilities.Tests/AccountTests.cs +++ b/_Tests/InternalUtilities.Tests/AccountTests.cs @@ -26,18 +26,36 @@ using static TestAudibleApiCommon.ComputedTestValues; namespace AccountsTests { + public class AccountsTestBase + { + protected const string EMPTY_FILE = "{\r\n \"Accounts\": []\r\n}"; + + protected string TestFile; + protected Locale usLocale => Localization.Get("us"); + protected Locale ukLocale => Localization.Get("uk"); + + protected void WriteToTestFile(string contents) + => File.WriteAllText(TestFile, contents); + + [TestInitialize] + public void TestInit() + => TestFile = Guid.NewGuid() + ".txt"; + + [TestCleanup] + public void TestCleanup() + { + if (File.Exists(TestFile)) + File.Delete(TestFile); + } + } + [TestClass] - public class FromJson + public class FromJson : AccountsTestBase { [TestMethod] public void _0_accounts() { - var json = @" -{ - ""Accounts"": [] -} -".Trim(); - var accountsSettings = AccountsSettings.FromJson(json); + var accountsSettings = AccountsSettings.FromJson(EMPTY_FILE); accountsSettings.Accounts.Count.Should().Be(0); } @@ -121,7 +139,7 @@ namespace AccountsTests ""LocaleName"": ""[empty]"", ""ExistingAccessToken"": { ""TokenValue"": ""Atna|"", - ""Expires"": ""9999-12-31T23:59:59.9999999"" + ""Expires"": ""0001-01-01T00:00:00"" }, ""PrivateKey"": null, ""AdpToken"": null, @@ -135,27 +153,6 @@ namespace AccountsTests } } - public class AccountsTestBase - { - protected string TestFile; - protected Locale usLocale => Localization.Get("us"); - protected Locale ukLocale => Localization.Get("uk"); - - protected void WriteToTestFile(string contents) - => File.WriteAllText(TestFile, contents); - - [TestInitialize] - public void TestInit() - => TestFile = Guid.NewGuid() + ".txt"; - - [TestCleanup] - public void TestCleanup() - { - if (File.Exists(TestFile)) - File.Delete(TestFile); - } - } - [TestClass] public class ctor : AccountsTestBase { @@ -166,11 +163,7 @@ namespace AccountsTests var accountsSettings = new AccountsSettings(); _ = new AccountsSettingsPersister(accountsSettings, TestFile); File.Exists(TestFile).Should().BeTrue(); - File.ReadAllText(TestFile).Should().Be(@" -{ - ""Accounts"": [] -} -".Trim()); + File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); } [TestMethod] @@ -183,11 +176,7 @@ namespace AccountsTests var accountsSettings = new AccountsSettings(); _ = new AccountsSettingsPersister(accountsSettings, TestFile); File.Exists(TestFile).Should().BeTrue(); - File.ReadAllText(TestFile).Should().Be(@" -{ - ""Accounts"": [] -} -".Trim()); + File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); } [TestMethod] @@ -566,11 +555,11 @@ namespace AccountsTests using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { - File.ReadAllText(TestFile).Should().Be("{\r\n \"Accounts\": []\r\n}".Trim()); + File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); acct.AccountName = "new"; - File.ReadAllText(TestFile).Should().Be("{\r\n \"Accounts\": []\r\n}".Trim()); + File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); } } } @@ -612,4 +601,71 @@ namespace AccountsTests Assert.ThrowsException(() => a2.IdentityTokens = idIn); } } + + [TestClass] + public class transactions : AccountsTestBase + { + [TestMethod] + public void atomic_update_at_end() + { + var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile); + p.BeginTransation(); + + // upserted account will not persist until CommitTransation + var acct = p.AccountsSettings.Upsert("cng", "us"); + acct.AccountName = "foo"; + + File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); + p.IsInTransaction.Should().BeTrue(); + + p.CommitTransation(); + p.IsInTransaction.Should().BeFalse(); + + + var jsonOut = File.ReadAllText(TestFile);//.Should().Be(EMPTY_FILE); + jsonOut.Should().Be(@" +{ + ""Accounts"": [ + { + ""AccountId"": ""cng"", + ""AccountName"": ""foo"", + ""LibraryScan"": true, + ""DecryptKey"": """", + ""IdentityTokens"": { + ""LocaleName"": ""us"", + ""ExistingAccessToken"": { + ""TokenValue"": ""Atna|"", + ""Expires"": ""0001-01-01T00:00:00"" + }, + ""PrivateKey"": null, + ""AdpToken"": null, + ""RefreshToken"": null, + ""Cookies"": [] + } + } + ] +} +".Trim()); + } + + [TestMethod] + public void abandoned_transaction() + { + var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile); + try + { + p.BeginTransation(); + + var acct = p.AccountsSettings.Upsert("cng", "us"); + acct.AccountName = "foo"; + throw new Exception(); + } + catch { } + finally + { + File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); + p.IsInTransaction.Should().BeTrue(); + } + } + } }