diff --git a/InternalUtilities/Accounts.cs b/InternalUtilities/Accounts.cs index 9fb5862a..fcb32a9a 100644 --- a/InternalUtilities/Accounts.cs +++ b/InternalUtilities/Accounts.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using AudibleApi; using AudibleApi.Authorization; using Dinah.Core; @@ -28,7 +29,12 @@ namespace InternalUtilities { public event EventHandler Updated; private void update(object sender = null, EventArgs e = null) - => Updated?.Invoke(this, new EventArgs()); + { + foreach (var account in AccountsSettings) + validate(account); + update_no_validate(); + } + private void update_no_validate() => Updated?.Invoke(this, new EventArgs()); public Accounts() { } @@ -45,15 +51,13 @@ namespace InternalUtilities // 'set' is only used by json deser set { - _accountsSettings_backing = value; - - if (_accountsSettings_backing is null) + if (value is null) return; - foreach (var acct in _accountsSettings_backing) - acct.Updated += update; + foreach (var account in value) + _add(account); - update(); + update_no_validate(); } } [JsonIgnore] @@ -66,29 +70,78 @@ namespace InternalUtilities public string ToJson(Formatting formatting = Formatting.Indented) => JsonConvert.SerializeObject(this, formatting, Identity.GetJsonSerializerSettings()); -public void UNITTEST_Seed(Account account) + public void Add(Account account) { - _accountsSettings_backing.Add(account); - update(); + _add(account); + update_no_validate(); } - // replace UNITTEST_Seed + public void _add(Account account) + { + validate(account); - // when creating Account object (get, update, insert), subscribe to it's update. including all new ones on initial load - // removing: unsubscribe + _accountsSettings_backing.Add(account); + account.Updated += update; + } - // IEnumerable GetAllAccounts + // more common naming convention alias for internal collection + public IReadOnlyList GetAll() => AccountsSettings; - // void UpsertAccount (id, locale) - // if not exists - // create account w/null identity - // save in file - // return Account? - // return bool/enum of whether is newly created? + public Account GetAccount(string accountId, string locale) + { + if (locale is null) + return null; - // how to persist edits to [Account] obj? - // account name, decryptkey, id tokens, ... - // persistence happens in [Accounts], not [Account]. an [Account] accidentally created directly shouldn't mess up expected workflow ??? + return AccountsSettings.SingleOrDefault(a => a.AccountId == accountId && a.IdentityTokens.Locale.Name == locale); + } + + public Account Upsert(string accountId, string locale) + { + var acct = GetAccount(accountId, locale); + + if (acct != null) + return acct; + + var l = Localization.Locales.Single(l => l.Name == locale); + var id = new Identity(l); + + var account = new Account(accountId) { IdentityTokens = id }; + Add(account); + return account; + } + + public bool Delete(Account account) + { + if (!_accountsSettings_backing.Contains(account)) + return false; + + account.Updated -= update; + return _accountsSettings_backing.Remove(account); + } + + public bool Delete(string accountId, string locale) + { + var acct = GetAccount(accountId, locale); + if (acct is null) + return false; + return Delete(acct); + } + + private void validate(Account account) + { + ArgumentValidator.EnsureNotNull(account, nameof(account)); + + var accountId = account.AccountId; + var locale = account?.IdentityTokens?.Locale?.Name; + + var acct = GetAccount(accountId, locale); + + if (acct is null || account is null) + return; + + if (acct != account) + throw new InvalidOperationException("Cannot add an account with the same account Id and Locale"); + } } public class Account : Updatable { diff --git a/_Tests/InternalUtilities.Tests/AccountTests.cs b/_Tests/InternalUtilities.Tests/AccountTests.cs index 85ea943d..5e5cba53 100644 --- a/_Tests/InternalUtilities.Tests/AccountTests.cs +++ b/_Tests/InternalUtilities.Tests/AccountTests.cs @@ -10,6 +10,7 @@ using AudibleApi; using AudibleApi.Authorization; using Dinah.Core; using FluentAssertions; +using FluentAssertions.Common; using InternalUtilities; using Microsoft.VisualStudio.TestPlatform.Common.Filtering; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -133,7 +134,7 @@ namespace AccountsTests } } - public class AccountsPersisterTestBase + public class AccountsTestBase { protected string TestFile; @@ -153,7 +154,7 @@ namespace AccountsTests } [TestClass] - public class ctor : AccountsPersisterTestBase + public class ctor : AccountsTestBase { [TestMethod] public void create_file() @@ -190,8 +191,8 @@ namespace AccountsTests public void save_multiple_children() { var accounts = new Accounts(); - accounts.UNITTEST_Seed(new Account("a0") { AccountName = "n0" }); - accounts.UNITTEST_Seed(new Account("a1") { AccountName = "n1" }); + accounts.Add(new Account("a0") { AccountName = "n0" }); + accounts.Add(new Account("a1") { AccountName = "n1" }); // dispose to cease auto-updates using (var p = new AccountsPersister(accounts, TestFile)) { } @@ -209,7 +210,7 @@ namespace AccountsTests var idJson = JsonConvert.SerializeObject(id, Identity.GetJsonSerializerSettings()); var accounts = new Accounts(); - accounts.UNITTEST_Seed(new Account("a0") { AccountName = "n0", IdentityTokens = id }); + accounts.Add(new Account("a0") { AccountName = "n0", IdentityTokens = id }); // dispose to cease auto-updates using (var p = new AccountsPersister(accounts, TestFile)) { } @@ -222,7 +223,7 @@ namespace AccountsTests } [TestClass] - public class save : AccountsPersisterTestBase + public class save : AccountsTestBase { // add/save account after file creation [TestMethod] @@ -238,7 +239,7 @@ namespace AccountsTests var idIn = new Identity(localeIn); var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn }; - p.Accounts.UNITTEST_Seed(acctIn); + p.Accounts.Add(acctIn); } // re-load file. ensure account still exists @@ -266,7 +267,7 @@ namespace AccountsTests var idIn = new Identity(localeIn); var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn }; - p.Accounts.UNITTEST_Seed(acctIn); + p.Accounts.Add(acctIn); } // re-load file. ensure account still exists @@ -286,7 +287,7 @@ namespace AccountsTests var idIn = new Identity(localeIn); var acctIn = new Account("a1") { AccountName = "n1", IdentityTokens = idIn }; - p.Accounts.UNITTEST_Seed(acctIn); + p.Accounts.Add(acctIn); } // re-load file. ensure both accounts still exist @@ -304,6 +305,32 @@ namespace AccountsTests } } + [TestMethod] + public void update_Account_field_just_added() + { + // create initial file + using (var p = new AccountsPersister(new Accounts(), TestFile)) { } + + // load file. create 2 accounts + using (var p = new AccountsPersister(TestFile)) + { + var locale1 = Localization.Locales.Single(l => l.Name == "us"); + var id1 = new Identity(locale1); + var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; + p.Accounts.Add(acct1); + + // update just-added item. note: this is different than the subscription which happens on initial collection load. ensure this works also + acct1.AccountName = "new"; + } + + // verify save property + using (var p = new AccountsPersister(TestFile)) + { + var acct0 = p.Accounts.AccountsSettings[0]; + acct0.AccountName.Should().Be("new"); + } + } + // update Account property. must be non-destructive to all other data [TestMethod] public void update_Account_field() @@ -317,13 +344,13 @@ namespace AccountsTests var locale1 = Localization.Locales.Single(l => l.Name == "us"); var id1 = new Identity(locale1); var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; - p.Accounts.UNITTEST_Seed(acct1); + p.Accounts.Add(acct1); var locale2 = Localization.Locales.Single(l => l.Name == "uk"); var id2 = new Identity(locale2); var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 }; - p.Accounts.UNITTEST_Seed(acct2); + p.Accounts.Add(acct2); } // update AccountName on existing file @@ -363,13 +390,13 @@ namespace AccountsTests var locale1 = Localization.Locales.Single(l => l.Name == "us"); var id1 = new Identity(locale1); var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; - p.Accounts.UNITTEST_Seed(acct1); + p.Accounts.Add(acct1); var locale2 = Localization.Locales.Single(l => l.Name == "uk"); var id2 = new Identity(locale2); var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 }; - p.Accounts.UNITTEST_Seed(acct2); + p.Accounts.Add(acct2); } // update identity on existing file @@ -413,13 +440,13 @@ namespace AccountsTests var locale1 = Localization.Locales.Single(l => l.Name == "us"); var id1 = new Identity(locale1); var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; - p.Accounts.UNITTEST_Seed(acct1); + p.Accounts.Add(acct1); var locale2 = Localization.Locales.Single(l => l.Name == "uk"); var id2 = new Identity(locale2); var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 }; - p.Accounts.UNITTEST_Seed(acct2); + p.Accounts.Add(acct2); } // update identity on existing file @@ -448,4 +475,154 @@ namespace AccountsTests } } } + + [TestClass] + public class retrieve : AccountsTestBase + { + [TestMethod] + public void get_where() + { + var us = Localization.Locales.Single(l => l.Name == "us"); + var idUs = new Identity(us); + var acct1 = new Account("cng") { IdentityTokens = idUs, AccountName = "foo" }; + + var uk = Localization.Locales.Single(l => l.Name == "uk"); + var idUk = new Identity(uk); + var acct2 = new Account("cng") { IdentityTokens = idUk, AccountName = "bar" }; + + var accounts = new Accounts(); + accounts.Add(acct1); + accounts.Add(acct2); + + accounts.GetAccount("cng", "uk").AccountName.Should().Be("bar"); + } + } + + [TestClass] + public class upsert : AccountsTestBase + { + [TestMethod] + public void upsert_new() + { + var accounts = new Accounts(); + accounts.AccountsSettings.Count.Should().Be(0); + + accounts.Upsert("cng", "us"); + + accounts.AccountsSettings.Count.Should().Be(1); + accounts.GetAccount("cng", "us").AccountId.Should().Be("cng"); + } + + [TestMethod] + public void upsert_exists() + { + var accounts = new Accounts(); + var orig = accounts.Upsert("cng", "us"); + orig.AccountName = "foo"; + + var exists = accounts.Upsert("cng", "us"); + exists.AccountName.Should().Be("foo"); + + orig.Should().IsSameOrEqualTo(exists); + } + } + + [TestClass] + public class delete : AccountsTestBase + { + [TestMethod] + public void delete_account() + { + var accounts = new Accounts(); + var acct = accounts.Upsert("cng", "us"); + accounts.AccountsSettings.Count.Should().Be(1); + + var removed = accounts.Delete(acct); + removed.Should().BeTrue(); + + accounts.AccountsSettings.Count.Should().Be(0); + } + + [TestMethod] + public void delete_where() + { + var accounts = new Accounts(); + var acct = accounts.Upsert("cng", "us"); + accounts.AccountsSettings.Count.Should().Be(1); + + accounts.Delete("baz", "baz").Should().BeFalse(); + accounts.AccountsSettings.Count.Should().Be(1); + + accounts.Delete("cng", "us").Should().BeTrue(); + accounts.AccountsSettings.Count.Should().Be(0); + } + + [TestMethod] + public void deleted_account_should_not_persist_file() + { + Account acct; + + using (var p = new AccountsPersister(new Accounts(), TestFile)) + { + acct = p.Accounts.Upsert("foo", "us"); + p.Accounts.AccountsSettings.Count.Should().Be(1); + acct.AccountName = "old"; + } + + using (var p = new AccountsPersister(new Accounts(), TestFile)) + { + p.Accounts.Delete(acct); + p.Accounts.AccountsSettings.Count.Should().Be(0); + } + + using (var p = new AccountsPersister(new Accounts(), TestFile)) + { + File.ReadAllText(TestFile).Should().Be("{\r\n \"AccountsSettings\": []\r\n}".Trim()); + + acct.AccountName = "new"; + + File.ReadAllText(TestFile).Should().Be("{\r\n \"AccountsSettings\": []\r\n}".Trim()); + } + } + } + + // account.Id + Locale.Name -- must be unique + [TestClass] + public class validate : AccountsTestBase + { + [TestMethod] + public void violate_validation() + { + var accounts = new Accounts(); + + var localeIn = Localization.Locales.Single(l => l.Name == "us"); + var idIn = new Identity(localeIn); + + var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn }; + accounts.Add(a1); + + var a2 = new Account("a") { AccountName = "two", IdentityTokens = idIn }; + + // violation: validate() + Assert.ThrowsException(() => accounts.Add(a2)); + } + + [TestMethod] + public void identity_violate_validation() + { + var accounts = new Accounts(); + + var localeIn = Localization.Locales.Single(l => l.Name == "us"); + var idIn = new Identity(localeIn); + + var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn }; + accounts.Add(a1); + + var a2 = new Account("a") { AccountName = "two" }; + accounts.Add(a2); + + // violation: GetAccount.SingleOrDefault + Assert.ThrowsException(() => a2.IdentityTokens = idIn); + } + } }