diff --git a/InternalUtilities/Accounts.cs b/InternalUtilities/Accounts.cs new file mode 100644 index 00000000..9fb5862a --- /dev/null +++ b/InternalUtilities/Accounts.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using AudibleApi; +using AudibleApi.Authorization; +using Dinah.Core; +using Dinah.Core.IO; +using Newtonsoft.Json; + +namespace InternalUtilities +{ + public class AccountsPersister : JsonFilePersister + { + /// Alias for Target + public Accounts Accounts => Target; + + /// uses path. create file if doesn't yet exist + public AccountsPersister(Accounts target, string path, string jsonPath = null) + : base(target, path, jsonPath) { } + + /// load from existing file + public AccountsPersister(string path, string jsonPath = null) + : base(path, jsonPath) { } + + protected override JsonSerializerSettings GetSerializerSettings() + => Identity.GetJsonSerializerSettings(); + } + public class Accounts : Updatable + { + public event EventHandler Updated; + private void update(object sender = null, EventArgs e = null) + => Updated?.Invoke(this, new EventArgs()); + + public Accounts() { } + + // for some reason this will make the json instantiator use _accountsSettings_json.set() + [JsonConstructor] + protected Accounts(List accounts) { } + + #region AccountsSettings + private List _accountsSettings_backing = new List(); + [JsonProperty(PropertyName = "AccountsSettings")] + private List _accountsSettings_json + { + get => _accountsSettings_backing; + // 'set' is only used by json deser + set + { + _accountsSettings_backing = value; + + if (_accountsSettings_backing is null) + return; + + foreach (var acct in _accountsSettings_backing) + acct.Updated += update; + + update(); + } + } + [JsonIgnore] + public IReadOnlyList AccountsSettings => _accountsSettings_json.AsReadOnly(); + #endregion + + public static Accounts FromJson(string json) + => JsonConvert.DeserializeObject(json, Identity.GetJsonSerializerSettings()); + + public string ToJson(Formatting formatting = Formatting.Indented) + => JsonConvert.SerializeObject(this, formatting, Identity.GetJsonSerializerSettings()); + +public void UNITTEST_Seed(Account account) + { + _accountsSettings_backing.Add(account); + update(); + } + + // replace UNITTEST_Seed + + // when creating Account object (get, update, insert), subscribe to it's update. including all new ones on initial load + // removing: unsubscribe + + // IEnumerable GetAllAccounts + + // 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? + + // 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 ??? + } + public class Account : Updatable + { + public event EventHandler Updated; + private void update(object sender = null, EventArgs e = null) + => Updated?.Invoke(this, new EventArgs()); + + // canonical. immutable. email or phone number + public string AccountId { get; } + + // user-friendly, non-canonical name. mutable + private string _accountName; + public string AccountName + { + get => _accountName; + set + { + if (string.IsNullOrWhiteSpace(value)) + return; + var v = value.Trim(); + if (v == _accountName) + return; + _accountName = v; + update(); + } + } + + private string _decryptKey = ""; + public string DecryptKey + { + get => _decryptKey; + set + { + var v = (value ?? "").Trim(); + if (v == _decryptKey) + return; + _decryptKey = v; + update(); + } + } + + private Identity _identity; + public Identity IdentityTokens + { + get => _identity; + set + { + if (_identity is null && value is null) + return; + + if (_identity != null) + _identity.Updated -= update; + + if (value != null) + value.Updated += update; + + _identity = value; + update(); + } + } + + [JsonIgnore] + public Locale Locale => IdentityTokens?.Locale; + + public Account(string accountId) + { + ArgumentValidator.EnsureNotNullOrWhiteSpace(accountId, nameof(accountId)); + AccountId = accountId.Trim(); + } + } +} diff --git a/InternalUtilities/UNTESTED/AudibleApiActions.cs b/InternalUtilities/UNTESTED/AudibleApiActions.cs index 20331e7c..8ed07999 100644 --- a/InternalUtilities/UNTESTED/AudibleApiActions.cs +++ b/InternalUtilities/UNTESTED/AudibleApiActions.cs @@ -12,11 +12,11 @@ namespace InternalUtilities { public static class AudibleApiActions { - public static async Task GetApiAsyncLegacy30(ILoginCallback loginCallback = null) + public static async Task GetApiAsyncLegacy30Async() { Localization.SetLocale(Configuration.Instance.LocaleCountryCode); - return await EzApiCreator.GetApiAsync(AudibleApiStorage.AccountsSettingsFileLegacy30, null, loginCallback); + return await EzApiCreator.GetApiAsync(AudibleApiStorage.AccountsSettingsFileLegacy30); } /// USE THIS from within Libation. It wraps the call with correct JSONPath diff --git a/InternalUtilities/UNTESTED/AudibleApiStorage.cs b/InternalUtilities/UNTESTED/AudibleApiStorage.cs index 6d9892b9..13fb6a07 100644 --- a/InternalUtilities/UNTESTED/AudibleApiStorage.cs +++ b/InternalUtilities/UNTESTED/AudibleApiStorage.cs @@ -14,6 +14,15 @@ namespace InternalUtilities public static string AccountsSettingsFile => Path.Combine(Configuration.Instance.LibationFiles, "AccountsSettings.json"); + public static void EnsureAccountsSettingsFileExists() + { + if (File.Exists(AccountsSettingsFile)) + return; + + // saves. BEWARE: this will overwrite an existing file + _ = new AccountsPersister(new Accounts(), AccountsSettingsFile); + } + // TEMP public static string GetJsonPath() => null; diff --git a/Libation.sln b/Libation.sln index 13b6c4e3..ece018cc 100644 --- a/Libation.sln +++ b/Libation.sln @@ -82,6 +82,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsDesktopUtilities", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -200,6 +204,10 @@ Global {F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.Build.0 = Release|Any CPU + {8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -233,6 +241,7 @@ Global {059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1} {E7EFD64D-6630-4426-B09C-B6862A92E3FD} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249} {F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14} + {8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9} diff --git a/LibationLauncher/LibationLauncher.csproj b/LibationLauncher/LibationLauncher.csproj index dc8497df..ae4842c1 100644 --- a/LibationLauncher/LibationLauncher.csproj +++ b/LibationLauncher/LibationLauncher.csproj @@ -13,7 +13,7 @@ win-x64 - 3.1.12.95 + 3.1.12.120 diff --git a/LibationLauncher/UNTESTED/Program.cs b/LibationLauncher/UNTESTED/Program.cs index c02ed4af..34ab8313 100644 --- a/LibationLauncher/UNTESTED/Program.cs +++ b/LibationLauncher/UNTESTED/Program.cs @@ -24,7 +24,7 @@ namespace LibationLauncher createSettings(); - ensureIdentityFile(); + AudibleApiStorage.EnsureAccountsSettingsFileExists(); migrateIdentityFile(); updateSettingsFile(); @@ -82,18 +82,6 @@ namespace LibationLauncher Environment.Exit(0); } - private static void ensureIdentityFile() - { - if (File.Exists(AudibleApiStorage.AccountsSettingsFile)) - return; - - var jObj = new JObject { - { "AccountsSettings", new JArray() } - }; - var contents = jObj.ToString(Formatting.Indented); - File.WriteAllText(AudibleApiStorage.AccountsSettingsFile, contents); - } - private static void migrateIdentityFile() { if (!File.Exists(AudibleApiStorage.AccountsSettingsFileLegacy30)) @@ -102,8 +90,9 @@ namespace LibationLauncher try { // - // for all in here: read directly from json file => JObject. A lot of this is legacy; don't rely on applicable POCOs + // in here: read directly from json file => JObject. A lot of this is legacy; don't rely on applicable POCOs // + var legacyContents = File.ReadAllText(AudibleApiStorage.AccountsSettingsFileLegacy30); var legacyJObj = JObject.Parse(legacyContents); @@ -127,6 +116,11 @@ namespace LibationLauncher } } + // create new account stub in new file + var api = AudibleApiActions.GetApiAsyncLegacy30Async().GetAwaiter().GetResult(); + var email = api.GetEmailAsync().GetAwaiter().GetResult(); + var locale = api.GetLocaleAsync(AudibleApi.CustomerOptions.All).GetAwaiter().GetResult(); + // more to do? } diff --git a/_Tests/InternalUtilities.Tests/AccountTests.cs b/_Tests/InternalUtilities.Tests/AccountTests.cs new file mode 100644 index 00000000..85ea943d --- /dev/null +++ b/_Tests/InternalUtilities.Tests/AccountTests.cs @@ -0,0 +1,451 @@ +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 Dinah.Core; +using FluentAssertions; +using InternalUtilities; +using Microsoft.VisualStudio.TestPlatform.Common.Filtering; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Moq.Protected; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using TestAudibleApiCommon; +using TestCommon; +using static AuthorizationShared.Shared; +using static AuthorizationShared.Shared.AccessTokenTemporality; +using static TestAudibleApiCommon.ComputedTestValues; + +namespace AccountsTests +{ + [TestClass] + public class FromJson + { + [TestMethod] + public void _0_accounts() + { + var json = @" +{ + ""AccountsSettings"": [] +} +".Trim(); + var accounts = Accounts.FromJson(json); + accounts.AccountsSettings.Count.Should().Be(0); + } + + [TestMethod] + public void _1_account_new() + { + var json = @" +{ + ""AccountsSettings"": [ + { + ""AccountId"": ""cng"", + ""AccountName"": ""my main login"", + ""DecryptKey"": ""asdfasdf"", + ""IdentityTokens"": null + } + ] +} +".Trim(); + var accounts = Accounts.FromJson(json); + accounts.AccountsSettings.Count.Should().Be(1); + accounts.AccountsSettings[0].AccountId.Should().Be("cng"); + accounts.AccountsSettings[0].IdentityTokens.Should().BeNull(); + } + + [TestMethod] + public void _1_account_populated() + { + var id = GetIdentityJson(Future); + + var json = $@" +{{ + ""AccountsSettings"": [ + {{ + ""AccountId"": ""cng"", + ""AccountName"": ""my main login"", + ""DecryptKey"": ""asdfasdf"", + ""IdentityTokens"": {id} + }} + ] +}} +".Trim(); + var accounts = Accounts.FromJson(json); + accounts.AccountsSettings.Count.Should().Be(1); + accounts.AccountsSettings[0].AccountId.Should().Be("cng"); + accounts.AccountsSettings[0].IdentityTokens.Should().NotBeNull(); + accounts.AccountsSettings[0].IdentityTokens.ExistingAccessToken.TokenValue.Should().Be(AccessTokenValue); + } + } + + [TestClass] + public class ToJson + { + [TestMethod] + public void serialize() + { + var id = JsonConvert.SerializeObject(Identity.Empty, Identity.GetJsonSerializerSettings()); + var jsonIn = $@" +{{ + ""AccountsSettings"": [ + {{ + ""AccountId"": ""cng"", + ""AccountName"": ""my main login"", + ""DecryptKey"": ""asdfasdf"", + ""IdentityTokens"": {id} + }} + ] +}} +".Trim(); + var accounts = Accounts.FromJson(jsonIn); + + var jsonOut = accounts.ToJson(); + jsonOut.Should().Be(@" +{ + ""AccountsSettings"": [ + { + ""AccountId"": ""cng"", + ""AccountName"": ""my main login"", + ""DecryptKey"": ""asdfasdf"", + ""IdentityTokens"": { + ""LocaleName"": ""[empty]"", + ""ExistingAccessToken"": { + ""TokenValue"": ""Atna|"", + ""Expires"": ""9999-12-31T23:59:59.9999999"" + }, + ""PrivateKey"": null, + ""AdpToken"": null, + ""RefreshToken"": null, + ""Cookies"": [] + } + } + ] +} +".Trim()); + } + } + + public class AccountsPersisterTestBase + { + protected string TestFile; + + 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 : AccountsPersisterTestBase + { + [TestMethod] + public void create_file() + { + File.Exists(TestFile).Should().BeFalse(); + var accounts = new Accounts(); + _ = new AccountsPersister(accounts, TestFile); + File.Exists(TestFile).Should().BeTrue(); + File.ReadAllText(TestFile).Should().Be(@" +{ + ""AccountsSettings"": [] +} +".Trim()); + } + + [TestMethod] + public void overwrite_existing_file() + { + File.Exists(TestFile).Should().BeFalse(); + WriteToTestFile("foo"); + File.Exists(TestFile).Should().BeTrue(); + + var accounts = new Accounts(); + _ = new AccountsPersister(accounts, TestFile); + File.Exists(TestFile).Should().BeTrue(); + File.ReadAllText(TestFile).Should().Be(@" +{ + ""AccountsSettings"": [] +} +".Trim()); + } + + [TestMethod] + 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" }); + + // dispose to cease auto-updates + using (var p = new AccountsPersister(accounts, TestFile)) { } + + var persister = new AccountsPersister(TestFile); + persister.Accounts.AccountsSettings.Count.Should().Be(2); + persister.Accounts.AccountsSettings[1].AccountName.Should().Be("n1"); + } + + [TestMethod] + public void save_with_identity() + { + var usLocale = Localization.Locales.Single(l => l.Name == "us"); + var id = new Identity(usLocale); + var idJson = JsonConvert.SerializeObject(id, Identity.GetJsonSerializerSettings()); + + var accounts = new Accounts(); + accounts.UNITTEST_Seed(new Account("a0") { AccountName = "n0", IdentityTokens = id }); + + // dispose to cease auto-updates + using (var p = new AccountsPersister(accounts, TestFile)) { } + + var persister = new AccountsPersister(TestFile); + var acct = persister.Accounts.AccountsSettings[0]; + acct.AccountName.Should().Be("n0"); + acct.Locale.CountryCode.Should().Be("us"); + } + } + + [TestClass] + public class save : AccountsPersisterTestBase + { + // add/save account after file creation + [TestMethod] + public void save_1_account() + { + // create initial file + using (var p = new AccountsPersister(new Accounts(), TestFile)) { } + + // load file. create account + using (var p = new AccountsPersister(TestFile)) + { + var localeIn = Localization.Locales.Single(l => l.Name == "us"); + var idIn = new Identity(localeIn); + var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn }; + + p.Accounts.UNITTEST_Seed(acctIn); + } + + // re-load file. ensure account still exists + using (var p = new AccountsPersister(TestFile)) + { + p.Accounts.AccountsSettings.Count.Should().Be(1); + var acct0 = p.Accounts.AccountsSettings[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 AccountsPersister(new Accounts(), TestFile)) { } + + // load file. create account 0 + using (var p = new AccountsPersister(TestFile)) + { + var localeIn = Localization.Locales.Single(l => l.Name == "us"); + var idIn = new Identity(localeIn); + var acctIn = new Account("a0") { AccountName = "n0", IdentityTokens = idIn }; + + p.Accounts.UNITTEST_Seed(acctIn); + } + + // re-load file. ensure account still exists + using (var p = new AccountsPersister(TestFile)) + { + p.Accounts.AccountsSettings.Count.Should().Be(1); + + var acct0 = p.Accounts.AccountsSettings[0]; + acct0.AccountName.Should().Be("n0"); + acct0.Locale.CountryCode.Should().Be("us"); + } + + // load file. create account 1 + using (var p = new AccountsPersister(TestFile)) + { + var localeIn = Localization.Locales.Single(l => l.Name == "uk"); + var idIn = new Identity(localeIn); + var acctIn = new Account("a1") { AccountName = "n1", IdentityTokens = idIn }; + + p.Accounts.UNITTEST_Seed(acctIn); + } + + // re-load file. ensure both accounts still exist + using (var p = new AccountsPersister(TestFile)) + { + p.Accounts.AccountsSettings.Count.Should().Be(2); + + var acct0 = p.Accounts.AccountsSettings[0]; + acct0.AccountName.Should().Be("n0"); + acct0.Locale.CountryCode.Should().Be("us"); + + var acct1 = p.Accounts.AccountsSettings[1]; + acct1.AccountName.Should().Be("n1"); + acct1.Locale.CountryCode.Should().Be("uk"); + } + } + + // update Account property. must be non-destructive to all other data + [TestMethod] + public void update_Account_field() + { + // 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.UNITTEST_Seed(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); + } + + // update AccountName on existing file + using (var p = new AccountsPersister(TestFile)) + { + var acct0 = p.Accounts.AccountsSettings[0]; + acct0.AccountName = "new"; + } + + // re-load file. ensure both accounts still exist + using (var p = new AccountsPersister(TestFile)) + { + p.Accounts.AccountsSettings.Count.Should().Be(2); + + var acct0 = p.Accounts.AccountsSettings[0]; + // new + acct0.AccountName.Should().Be("new"); + + // still here + acct0.Locale.CountryCode.Should().Be("us"); + var acct1 = p.Accounts.AccountsSettings[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 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.UNITTEST_Seed(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); + } + + // update identity on existing file + using (var p = new AccountsPersister(TestFile)) + { + var locale = Localization.Locales.Single(l => l.Name == "uk"); + var id = new Identity(locale); + + var acct0 = p.Accounts.AccountsSettings[0]; + acct0.IdentityTokens = id; + } + + // re-load file. ensure both accounts still exist + using (var p = new AccountsPersister(TestFile)) + { + p.Accounts.AccountsSettings.Count.Should().Be(2); + + var acct0 = p.Accounts.AccountsSettings[0]; + // new + acct0.Locale.CountryCode.Should().Be("uk"); + + // still here + acct0.AccountName.Should().Be("n0"); + var acct1 = p.Accounts.AccountsSettings[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 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.UNITTEST_Seed(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); + } + + // update identity on existing file + using (var p = new AccountsPersister(TestFile)) + { + p.Accounts.AccountsSettings[0] + .IdentityTokens + .Update(new AccessToken("Atna|_NEW_", DateTime.Now.AddDays(1))); + } + + // re-load file. ensure both accounts still exist + using (var p = new AccountsPersister(TestFile)) + { + p.Accounts.AccountsSettings.Count.Should().Be(2); + + var acct0 = p.Accounts.AccountsSettings[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.Accounts.AccountsSettings[1]; + acct1.AccountName.Should().Be("n1"); + acct1.Locale.CountryCode.Should().Be("uk"); + } + } + } +} diff --git a/_Tests/InternalUtilities.Tests/InternalUtilities.Tests.csproj b/_Tests/InternalUtilities.Tests/InternalUtilities.Tests.csproj new file mode 100644 index 00000000..2da44cc5 --- /dev/null +++ b/_Tests/InternalUtilities.Tests/InternalUtilities.Tests.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp3.1 + + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + +