using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using AudibleApi; using AudibleApi.Authorization; using AudibleUtilities; using Dinah.Core; using FluentAssertions; using FluentAssertions.Common; using Microsoft.VisualStudio.TestPlatform.Common.Filtering; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Moq.Protected; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace AccountsTests { #pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. public class AccountsTestBase { protected string EMPTY_FILE { get; } = "{\r\n \"Accounts\": []\r\n}".Replace("\r\n", Environment.NewLine); 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 : AccountsTestBase { [TestMethod] public void _0_accounts() { var accountsSettings = AccountsSettings.FromJson(EMPTY_FILE); accountsSettings.Accounts.Count.Should().Be(0); } [TestMethod] public void _1_account_new() { var json = @" { ""Accounts"": [ { ""AccountId"": ""cng"", ""AccountName"": ""my main login"", ""DecryptKey"": ""asdfasdf"", ""IdentityTokens"": null } ] } ".Trim(); var accountsSettings = AccountsSettings.FromJson(json); accountsSettings.Accounts.Count.Should().Be(1); accountsSettings.Accounts[0].AccountId.Should().Be("cng"); accountsSettings.Accounts[0].IdentityTokens.Should().BeNull(); } } [TestClass] public class ctor : AccountsTestBase { [TestMethod] public void create_file() { File.Exists(TestFile).Should().BeFalse(); var accountsSettings = new AccountsSettings(); _ = new AccountsSettingsPersister(accountsSettings, TestFile); File.Exists(TestFile).Should().BeTrue(); File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); } [TestMethod] public void overwrite_existing_file() { File.Exists(TestFile).Should().BeFalse(); WriteToTestFile("foo"); File.Exists(TestFile).Should().BeTrue(); var accountsSettings = new AccountsSettings(); _ = new AccountsSettingsPersister(accountsSettings, TestFile); File.Exists(TestFile).Should().BeTrue(); File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); } [TestMethod] public void save_multiple_children() { var accountsSettings = new AccountsSettings(); accountsSettings.Add(new Account("a0") { AccountName = "n0" }); accountsSettings.Add(new Account("a1") { AccountName = "n1" }); // dispose to cease auto-updates using (var p = new AccountsSettingsPersister(accountsSettings, TestFile)) { } var persister = new AccountsSettingsPersister(TestFile); persister.AccountsSettings.Accounts.Count.Should().Be(2); persister.AccountsSettings.Accounts[1].AccountName.Should().Be("n1"); } [TestMethod] public void save_with_identity() { var id = new Identity(usLocale); var idJson = JsonConvert.SerializeObject(id, Identity.GetJsonSerializerSettings()); var accountsSettings = new AccountsSettings(); accountsSettings.Add(new Account("a0") { AccountName = "n0", IdentityTokens = id }); // dispose to cease auto-updates using (var p = new AccountsSettingsPersister(accountsSettings, TestFile)) { } var persister = new AccountsSettingsPersister(TestFile); var acct = persister.AccountsSettings.Accounts[0]; acct.AccountName.Should().Be("n0"); acct.Locale.CountryCode.Should().Be("us"); } } [TestClass] public class save : AccountsTestBase { // add/save account after file creation [TestMethod] public void save_1_account() { // create initial file using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { } // load file. create account using (var p = new AccountsSettingsPersister(TestFile)) { var idIn = new Identity(usLocale); var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn }; p.AccountsSettings.Add(acctIn); } // re-load file. ensure account still exists using (var p = new AccountsSettingsPersister(TestFile)) { p.AccountsSettings.Accounts.Count.Should().Be(1); var acct0 = p.AccountsSettings.Accounts[0]; acct0.AccountName.Should().Be("n0"); acct0.Locale.CountryCode.Should().Be("us"); } } // add/save mult accounts after file creation // separately create 2 accounts. ensure both still exist in the end [TestMethod] public void save_2_accounts() { // create initial file using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { } // load file. create account 0 using (var p = new AccountsSettingsPersister(TestFile)) { var idIn = new Identity(usLocale); var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn }; p.AccountsSettings.Add(acctIn); } // re-load file. ensure account still exists using (var p = new AccountsSettingsPersister(TestFile)) { p.AccountsSettings.Accounts.Count.Should().Be(1); var acct0 = p.AccountsSettings.Accounts[0]; acct0.AccountName.Should().Be("n0"); acct0.Locale.CountryCode.Should().Be("us"); } // load file. create account 1 using (var p = new AccountsSettingsPersister(TestFile)) { var idIn = new Identity(ukLocale); var acctIn = new Account("a1") { AccountName = "n1", IdentityTokens = idIn }; p.AccountsSettings.Add(acctIn); } // re-load file. ensure both accounts still exist using (var p = new AccountsSettingsPersister(TestFile)) { p.AccountsSettings.Accounts.Count.Should().Be(2); var acct0 = p.AccountsSettings.Accounts[0]; acct0.AccountName.Should().Be("n0"); acct0.Locale.CountryCode.Should().Be("us"); var acct1 = p.AccountsSettings.Accounts[1]; acct1.AccountName.Should().Be("n1"); acct1.Locale.CountryCode.Should().Be("uk"); } } [TestMethod] public void update_Account_field_just_added() { // create initial file using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { } // load file. create 2 accounts using (var p = new AccountsSettingsPersister(TestFile)) { var id1 = new Identity(usLocale); var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; p.AccountsSettings.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 AccountsSettingsPersister(TestFile)) { var acct0 = p.AccountsSettings.Accounts[0]; acct0.AccountName.Should().Be("new"); } } // update Account property. must be non-destructive to all other data [TestMethod] public void update_Account_field() { // create initial file using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { } // load file. create 2 accounts using (var p = new AccountsSettingsPersister(TestFile)) { var id1 = new Identity(usLocale); var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; p.AccountsSettings.Add(acct1); var id2 = new Identity(ukLocale); var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 }; p.AccountsSettings.Add(acct2); } // update AccountName on existing file using (var p = new AccountsSettingsPersister(TestFile)) { var acct0 = p.AccountsSettings.Accounts[0]; acct0.AccountName = "new"; } // re-load file. ensure both accounts still exist using (var p = new AccountsSettingsPersister(TestFile)) { p.AccountsSettings.Accounts.Count.Should().Be(2); var acct0 = p.AccountsSettings.Accounts[0]; // new acct0.AccountName.Should().Be("new"); // still here acct0.Locale.CountryCode.Should().Be("us"); var acct1 = p.AccountsSettings.Accounts[1]; acct1.AccountName.Should().Be("n1"); acct1.Locale.CountryCode.Should().Be("uk"); } } // update identity. must be non-destructive to all other data [TestMethod] public void replace_identity() { // create initial file using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { } // load file. create 2 accounts using (var p = new AccountsSettingsPersister(TestFile)) { var id1 = new Identity(usLocale); var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; p.AccountsSettings.Add(acct1); var id2 = new Identity(ukLocale); var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 }; p.AccountsSettings.Add(acct2); } // update identity on existing file using (var p = new AccountsSettingsPersister(TestFile)) { var id = new Identity(ukLocale); var acct0 = p.AccountsSettings.Accounts[0]; acct0.IdentityTokens = id; } // re-load file. ensure both accounts still exist using (var p = new AccountsSettingsPersister(TestFile)) { p.AccountsSettings.Accounts.Count.Should().Be(2); var acct0 = p.AccountsSettings.Accounts[0]; // new acct0.Locale.CountryCode.Should().Be("uk"); // still here acct0.AccountName.Should().Be("n0"); var acct1 = p.AccountsSettings.Accounts[1]; acct1.AccountName.Should().Be("n1"); acct1.Locale.CountryCode.Should().Be("uk"); } } // multi-level subscribe => update // edit field of existing identity. must be non-destructive to all other data [TestMethod] public void update_identity_field() { // create initial file using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { } // load file. create 2 accounts using (var p = new AccountsSettingsPersister(TestFile)) { var id1 = new Identity(usLocale); var acct1 = new Account("a0") { AccountName = "n0", IdentityTokens = id1 }; p.AccountsSettings.Add(acct1); var id2 = new Identity(ukLocale); var acct2 = new Account("a1") { AccountName = "n1", IdentityTokens = id2 }; p.AccountsSettings.Add(acct2); } // update identity on existing file using (var p = new AccountsSettingsPersister(TestFile)) { p.AccountsSettings.Accounts[0] .IdentityTokens .Update(new AccessToken("Atna|_NEW_", DateTime.Now.AddDays(1))); } // re-load file. ensure both accounts still exist using (var p = new AccountsSettingsPersister(TestFile)) { p.AccountsSettings.Accounts.Count.Should().Be(2); var acct0 = p.AccountsSettings.Accounts[0]; // new acct0.IdentityTokens.ExistingAccessToken.TokenValue.Should().Be("Atna|_NEW_"); // still here acct0.AccountName.Should().Be("n0"); acct0.Locale.CountryCode.Should().Be("us"); var acct1 = p.AccountsSettings.Accounts[1]; acct1.AccountName.Should().Be("n1"); acct1.Locale.CountryCode.Should().Be("uk"); } } } [TestClass] public class retrieve : AccountsTestBase { [TestMethod] public void get_where() { var idUs = new Identity(usLocale); var acct1 = new Account("cng") { IdentityTokens = idUs, AccountName = "foo" }; var idUk = new Identity(ukLocale); var acct2 = new Account("cng") { IdentityTokens = idUk, AccountName = "bar" }; var accountsSettings = new AccountsSettings(); accountsSettings.Add(acct1); accountsSettings.Add(acct2); accountsSettings.GetAccount("cng", "uk").AccountName.Should().Be("bar"); } } [TestClass] public class upsert : AccountsTestBase { [TestMethod] public void upsert_new() { var accountsSettings = new AccountsSettings(); accountsSettings.Accounts.Count.Should().Be(0); accountsSettings.Upsert("cng", "us"); accountsSettings.Accounts.Count.Should().Be(1); accountsSettings.GetAccount("cng", "us").AccountId.Should().Be("cng"); } [TestMethod] public void upsert_exists() { var accountsSettings = new AccountsSettings(); var orig = accountsSettings.Upsert("cng", "us"); orig.AccountName = "foo"; var exists = accountsSettings.Upsert("cng", "us"); exists.AccountName.Should().Be("foo"); orig.Should().BeSameAs(exists); } } [TestClass] public class delete : AccountsTestBase { [TestMethod] public void delete_account() { var accountsSettings = new AccountsSettings(); var acct = accountsSettings.Upsert("cng", "us"); accountsSettings.Accounts.Count.Should().Be(1); var removed = accountsSettings.Delete(acct); removed.Should().BeTrue(); accountsSettings.Accounts.Count.Should().Be(0); } [TestMethod] public void delete_where() { var accountsSettings = new AccountsSettings(); _ = accountsSettings.Upsert("cng", "us"); accountsSettings.Accounts.Count.Should().Be(1); accountsSettings.Delete("baz", "baz").Should().BeFalse(); accountsSettings.Accounts.Count.Should().Be(1); accountsSettings.Delete("cng", "us").Should().BeTrue(); accountsSettings.Accounts.Count.Should().Be(0); } [TestMethod] public void delete_updates() { var i = 0; void update(object sender, EventArgs e) => i++; var accountsSettings = new AccountsSettings(); accountsSettings.Updated += update; accountsSettings.Accounts.Count.Should().Be(0); i.Should().Be(0); _ = accountsSettings.Upsert("cng", "us"); accountsSettings.Accounts.Count.Should().Be(1); i.Should().Be(1); accountsSettings.Delete("baz", "baz").Should().BeFalse(); accountsSettings.Accounts.Count.Should().Be(1); i.Should().Be(1); accountsSettings.Delete("cng", "us").Should().BeTrue(); accountsSettings.Accounts.Count.Should().Be(0); i.Should().Be(2); // <== this is the one being tested } [TestMethod] public void deleted_account_should_not_persist_file() { Account acct; using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { acct = p.AccountsSettings.Upsert("foo", "us"); p.AccountsSettings.Accounts.Count.Should().Be(1); acct.AccountName = "old"; } using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { p.AccountsSettings.Delete(acct); p.AccountsSettings.Accounts.Count.Should().Be(0); } using (var p = new AccountsSettingsPersister(new AccountsSettings(), TestFile)) { File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); acct.AccountName = "new"; File.ReadAllText(TestFile).Should().Be(EMPTY_FILE); } } } // account.Id + Locale.Name -- must be unique [TestClass] public class validate : AccountsTestBase { [TestMethod] public void violate_validation() { var accountsSettings = new AccountsSettings(); var idIn = new Identity(usLocale); var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn }; accountsSettings.Add(a1); var a2 = new Account("a") { AccountName = "two", IdentityTokens = idIn }; // violation: validate() Assert.ThrowsException(() => accountsSettings.Add(a2)); } [TestMethod] public void identity_violate_validation() { var accountsSettings = new AccountsSettings(); var idIn = new Identity(usLocale); var a1 = new Account("a") { AccountName = "one", IdentityTokens = idIn }; accountsSettings.Add(a1); var a2 = new Account("a") { AccountName = "two" }; accountsSettings.Add(a2); // violation: GetAccount.SingleOrDefault 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(); } [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(); } } } #pragma warning restore CS8981 }