Libation 4.0 prep: account management complete

This commit is contained in:
Robert McRackan 2020-08-25 14:21:14 -04:00
parent 4b31207f91
commit 6979ab4450
7 changed files with 198 additions and 136 deletions

View File

@ -51,37 +51,17 @@ namespace InternalUtilities
public IReadOnlyList<Account> Accounts => _accounts_json.AsReadOnly();
#endregion
#region de/serialize
public static AccountsSettings FromJson(string json)
=> JsonConvert.DeserializeObject<AccountsSettings>(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<Account> 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));

View File

@ -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);

View File

@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>3.1.12.260</Version>
<Version>3.1.12.268</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -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;

View File

@ -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();
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}", "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// WARNING: accounts persister will write ANY EDIT immediately to file.
// Take NO action on persistent objects until the end
var accountsSettings = AudibleApiStorage.GetPersistentAccountsSettings();
private void persist(AccountsSettings accountsSettings)
{
var existingAccounts = accountsSettings.Accounts;
var dtos = getRowDtos();
foreach (var account in existingAccounts)
// 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);
}
}
// 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
// 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<AccountDto> getRowDtos()
=> dataGridView1.Rows
.Cast<DataGridViewRow>()
.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();
}
}

View File

@ -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;

View File

@ -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<InvalidOperationException>(() => 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();
}
}
}
}