diff --git a/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj b/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj index ecff297..a4a3235 100644 --- a/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj +++ b/CommunityToolkit.Uwp.Graph.Controls/CommunityToolkit.Uwp.Graph.Controls.csproj @@ -36,7 +36,7 @@ - + diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs new file mode 100644 index 0000000..3b034da --- /dev/null +++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/BaseRoamingSettingsDataStore.cs @@ -0,0 +1,242 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp.Helpers; +using Windows.Storage; + +namespace CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings +{ + /// + /// A base class for easily building roaming settings helper implementations. + /// + public abstract class BaseRoamingSettingsDataStore : IRoamingSettingsDataStore + { + /// + public EventHandler SyncCompleted { get; set; } + + /// + public EventHandler SyncFailed { get; set; } + + /// + public bool AutoSync { get; } + + /// + public string Id { get; } + + /// + public string UserId { get; } + + /// + public IDictionary Cache { get; private set; } + + /// + /// Gets an object serializer for converting objects in the data store. + /// + protected IObjectSerializer Serializer { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The id of the target Graph user. + /// A unique id for the data store. + /// An IObjectSerializer used for serializing objects. + /// Determines if the data store should sync for every interaction. + public BaseRoamingSettingsDataStore(string userId, string dataStoreId, IObjectSerializer objectSerializer, bool autoSync = true) + { + AutoSync = autoSync; + Id = dataStoreId; + UserId = userId; + Serializer = objectSerializer; + + Cache = null; + } + + /// + /// Create a new instance of the data storage container. + /// + /// A task. + public abstract Task Create(); + + /// + /// Delete the instance of the data storage container. + /// + /// A task. + public abstract Task Delete(); + + /// + public bool KeyExists(string key) + { + return Cache != null && Cache.ContainsKey(key); + } + + /// + public bool KeyExists(string compositeKey, string key) + { + if (KeyExists(compositeKey)) + { + ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; + if (composite != null) + { + return composite.ContainsKey(key); + } + } + + return false; + } + + /// + public T Read(string key, T @default = default) + { + if (Cache != null && Cache.TryGetValue(key, out object value)) + { + try + { + return Serializer.Deserialize((string)value); + } + catch + { + // Primitive types can't be deserialized. + return (T)Convert.ChangeType(value, typeof(T)); + } + } + + return @default; + } + + /// + public T Read(string compositeKey, string key, T @default = default) + { + if (Cache != null) + { + ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; + if (composite != null) + { + object value = composite[key]; + if (value != null) + { + try + { + return Serializer.Deserialize((string)value); + } + catch + { + // Primitive types can't be deserialized. + return (T)Convert.ChangeType(value, typeof(T)); + } + } + } + } + + return @default; + } + + /// + public void Save(string key, T value) + { + InitCache(); + + // Skip serialization for primitives. + if (typeof(T) == typeof(object) || Type.GetTypeCode(typeof(T)) != TypeCode.Object) + { + // Update the cache + Cache[key] = value; + } + else + { + // Update the cache + Cache[key] = Serializer.Serialize(value); + } + + if (AutoSync) + { + // Update the remote + Task.Run(() => Sync()); + } + } + + /// + public void Save(string compositeKey, IDictionary values) + { + InitCache(); + + if (KeyExists(compositeKey)) + { + ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; + + foreach (KeyValuePair setting in values.ToList()) + { + if (composite.ContainsKey(setting.Key)) + { + composite[setting.Key] = Serializer.Serialize(setting.Value); + } + else + { + composite.Add(setting.Key, Serializer.Serialize(setting.Value)); + } + } + + // Update the cache + Cache[compositeKey] = composite; + + if (AutoSync) + { + // Update the remote + Task.Run(() => Sync()); + } + } + else + { + ApplicationDataCompositeValue composite = new ApplicationDataCompositeValue(); + foreach (KeyValuePair setting in values.ToList()) + { + composite.Add(setting.Key, Serializer.Serialize(setting.Value)); + } + + // Update the cache + Cache[compositeKey] = composite; + + if (AutoSync) + { + // Update the remote + Task.Run(() => Sync()); + } + } + } + + /// + public abstract Task FileExistsAsync(string filePath); + + /// + public abstract Task ReadFileAsync(string filePath, T @default = default); + + /// + public abstract Task SaveFileAsync(string filePath, T value); + + /// + public abstract Task Sync(); + + /// + /// Initialize the internal cache. + /// + protected void InitCache() + { + if (Cache == null) + { + Cache = new Dictionary(); + } + } + + /// + /// Delete the internal cache. + /// + protected void DeleteCache() + { + Cache = null; + } + } +} diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataSource.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataSource.cs new file mode 100644 index 0000000..cbf3cf9 --- /dev/null +++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataSource.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Net.Authentication; +using CommunityToolkit.Net.Graph.Extensions; +using Microsoft.Graph; + +namespace CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings +{ + /// + /// Helpers for interacting with files in the special OneDrive AppRoot folder. + /// + internal static class OneDriveDataSource + { + private static GraphServiceClient Graph => ProviderManager.Instance.GlobalProvider?.Graph(); + + // Create a new file. + // This fails, because OneDrive doesn't like empty files. Use Update instead. + // public static async Task Create(string fileWithExt) + // { + // var driveItem = new DriveItem() + // { + // Name = fileWithExt, + // }; + // await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Request().CreateAsync(driveItem); + // } + + /// + /// Updates or create a new file on the remote with the provided content. + /// + /// The type of object to save. + /// A representing the asynchronous operation. + public static async Task Update(string userId, string fileWithExt, T fileContents) + { + var json = Graph.HttpProvider.Serializer.SerializeObject(fileContents); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + return await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Content.Request().PutAsync(stream); + } + + /// + /// Get a file from the remote. + /// + /// The type of object to return. + /// A representing the asynchronous operation. + public static async Task Retrieve(string userId, string fileWithExt) + { + Stream stream = await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Content.Request().GetAsync(); + + return Graph.HttpProvider.Serializer.DeserializeObject(stream); + } + + /// + /// Delete the file from the remote. + /// + /// A representing the asynchronous operation. + public static async Task Delete(string userId, string fileWithExt) + { + await Graph.Users[userId].Drive.Special.AppRoot.ItemWithPath(fileWithExt).Request().DeleteAsync(); + } + } +} diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataStore.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataStore.cs new file mode 100644 index 0000000..46c433e --- /dev/null +++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/OneDriveDataStore.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp.Helpers; +using Windows.Storage; + +namespace CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings +{ + /// + /// A DataStore for managing roaming settings in OneDrive. + /// + public class OneDriveDataStore : BaseRoamingSettingsDataStore + { + /// + /// Retrieve an object stored in a OneDrive file. + /// + /// The type of object to retrieve. + /// The id of the target Graph user. + /// The name of the file. + /// The deserialized file contents. + public static async Task Get(string userId, string fileName) + { + return await OneDriveDataSource.Retrieve(userId, fileName); + } + + /// + /// Update the contents of a OneDrive file. + /// + /// The type of object being stored. + /// The id of the target Graph user. + /// The name of the file. + /// The object to store. + /// A task. + public static async Task Set(string userId, string fileName, T fileContents) + { + await OneDriveDataSource.Update(userId, fileName, fileContents); + } + + /// + /// Delete a file from OneDrive by name. + /// + /// The id of the target Graph user. + /// The name of the file. + /// A task. + public static async Task Delete(string userId, string fileName) + { + await OneDriveDataSource.Delete(userId, fileName); + } + + /// + /// Initializes a new instance of the class. + /// + public OneDriveDataStore(string userId, string syncDataFileName, IObjectSerializer objectSerializer, bool autoSync = true) + : base(userId, syncDataFileName, objectSerializer, autoSync) + { + } + + /// + public override Task Create() + { + InitCache(); + + return Task.CompletedTask; + } + + /// + public override async Task Delete() + { + // Clear the cache + DeleteCache(); + + // Delete the remote. + await Delete(UserId, Id); + } + + /// + public override async Task FileExistsAsync(string filePath) + { + var roamingSettings = await Get(UserId, Id); + return roamingSettings != null; + } + + /// + public override async Task ReadFileAsync(string filePath, T @default = default) + { + return await Get(UserId, filePath) ?? @default; + } + + /// + public override async Task SaveFileAsync(string filePath, T value) + { + await Set(UserId, filePath, value); + + // Can't convert DriveItem to StorageFile, so we return null instead. + return null; + } + + /// + public override async Task Sync() + { + try + { + // Get the remote + string fileName = Id; + IDictionary remoteData = null; + try + { + remoteData = await Get>(UserId, fileName); + } + catch + { + // If get fails, we know the remote store does not exist. + } + + bool needsUpdate = false; + if (remoteData != null) + { + if (remoteData.Keys.Count > 0) + { + InitCache(); + } + + // Update local cache with additions from remote + foreach (string key in remoteData.Keys.ToList()) + { + // Only insert new values. Existing keys should be overwritten on the remote. + if (!Cache.ContainsKey(key)) + { + Cache.Add(key, remoteData[key]); + needsUpdate = true; + } + } + } + else if (Cache != null && Cache.Count > 0) + { + // The remote does not yet exist, and we have data to save. + needsUpdate = true; + } + + if (needsUpdate) + { + // Send updates for local values, overwriting the remote. + await Set(UserId, fileName, Cache); + } + + SyncCompleted?.Invoke(this, new EventArgs()); + } + catch + { + SyncFailed?.Invoke(this, new EventArgs()); + } + } + } +} diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/RoamingSettingsHelper.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/RoamingSettingsHelper.cs index 9831656..bf14dd4 100644 --- a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/RoamingSettingsHelper.cs +++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/RoamingSettingsHelper.cs @@ -21,6 +21,11 @@ public enum RoamingDataStore /// Store data using open extensions on the Graph User. /// UserExtensions, + + /// + /// Store data in a Graph User's OneDrive. + /// + OneDrive, } /// @@ -95,6 +100,10 @@ public RoamingSettingsHelper(string userId, RoamingDataStore dataStore = Roaming DataStore = new UserExtensionDataStore(userId, dataStoreName, serializer, autoSync); break; + case RoamingDataStore.OneDrive: + DataStore = new OneDriveDataStore(userId, dataStoreName, serializer, autoSync); + break; + default: throw new ArgumentOutOfRangeException(nameof(dataStore)); } diff --git a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/UserExtensionDataStore.cs b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/UserExtensionDataStore.cs index aa3eee1..0fde3df 100644 --- a/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/UserExtensionDataStore.cs +++ b/CommunityToolkit.Uwp.Graph.Controls/Helpers/RoamingSettings/UserExtensionDataStore.cs @@ -15,7 +15,7 @@ namespace CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings /// /// An IObjectStorageHelper implementation using open extensions on the Graph User for storing key/value pairs. /// - public class UserExtensionDataStore : IRoamingSettingsDataStore + public class UserExtensionDataStore : BaseRoamingSettingsDataStore { /// /// Retrieve the value from Graph User extensions and cast the response to the provided type. @@ -93,80 +93,57 @@ public static async Task GetExtensionForUser(string userId, string ex private static readonly IList ReservedKeys = new List { "responseHeaders", "statusCode", "@odata.context" }; - /// - public EventHandler SyncCompleted { get; set; } - - /// - public EventHandler SyncFailed { get; set; } - - /// - public bool AutoSync { get; } - - /// - public string Id { get; } - - /// - public string UserId { get; } - - /// - public IDictionary Cache { get; private set; } - - private readonly IObjectSerializer _serializer; - /// /// Initializes a new instance of the class. /// public UserExtensionDataStore(string userId, string extensionId, IObjectSerializer objectSerializer, bool autoSync = true) + : base(userId, extensionId, objectSerializer, autoSync) { - AutoSync = autoSync; - Id = extensionId; - UserId = userId; - _serializer = objectSerializer; - - Cache = null; } /// /// Creates a new roaming settings extension on the Graph User. /// /// The newly created Extension object. - public async Task Create() + public override async Task Create() { InitCache(); - if (AutoSync) - { - await Create(UserId, Id); - } + await Create(UserId, Id); } /// /// Deletes the roamingSettings extension from the Graph User. /// /// A void task. - public async Task Delete() + public override async Task Delete() { - // Clear the cache - Cache = null; + // Delete the cache + DeleteCache(); - if (AutoSync) - { - // Delete the remote. - await Delete(UserId, Id); - } + // Delete the remote. + await Delete(UserId, Id); } /// /// Update the remote extension to match the local cache and retrieve any new keys. Any existing remote values are replaced. /// /// The freshly synced user extension. - public async Task Sync() + public override async Task Sync() { try { - // Get the remote - Extension extension = await GetExtensionForUser(UserId, Id); - IDictionary remoteData = extension.AdditionalData; + IDictionary remoteData = null; + + try + { + // Get the remote + Extension extension = await GetExtensionForUser(UserId, Id); + remoteData = extension.AdditionalData; + } + catch + { + } if (Cache != null) { @@ -178,24 +155,24 @@ public async Task Sync() continue; } - if (!remoteData.ContainsKey(key) || !EqualityComparer.Default.Equals(remoteData[key], Cache[key])) + if (remoteData == null || !remoteData.ContainsKey(key) || !EqualityComparer.Default.Equals(remoteData[key], Cache[key])) { Save(key, Cache[key]); } } } - if (remoteData.Keys.Count > 0) + if (remoteData != null) { InitCache(); - } - // Update local cache with additions from remote - foreach (string key in remoteData.Keys.ToList()) - { - if (!Cache.ContainsKey(key)) + // Update local cache with additions from remote + foreach (string key in remoteData.Keys.ToList()) { - Cache.Add(key, remoteData[key]); + if (!Cache.ContainsKey(key)) + { + Cache.Add(key, remoteData[key]); + } } } @@ -208,169 +185,21 @@ public async Task Sync() } /// - public virtual bool KeyExists(string key) - { - return Cache != null && Cache.ContainsKey(key); - } - - /// - public bool KeyExists(string compositeKey, string key) - { - if (KeyExists(compositeKey)) - { - ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; - if (composite != null) - { - return composite.ContainsKey(key); - } - } - - return false; - } - - /// - public T Read(string key, T @default = default) - { - if (Cache != null && Cache.TryGetValue(key, out object value)) - { - try - { - return _serializer.Deserialize((string)value); - } - catch - { - // Primitive types can't be deserialized. - return (T)Convert.ChangeType(value, typeof(T)); - } - } - - return @default; - } - - /// - public T Read(string compositeKey, string key, T @default = default) - { - if (Cache != null) - { - ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; - if (composite != null) - { - object value = composite[key]; - if (value != null) - { - try - { - return _serializer.Deserialize((string)value); - } - catch - { - // Primitive types can't be deserialized. - return (T)Convert.ChangeType(value, typeof(T)); - } - } - } - } - - return @default; - } - - /// - public void Save(string key, T value) - { - InitCache(); - - // Skip serialization for primitives. - if (typeof(T) == typeof(object) || Type.GetTypeCode(typeof(T)) != TypeCode.Object) - { - // Update the cache - Cache[key] = value; - } - else - { - // Update the cache - Cache[key] = _serializer.Serialize(value); - } - - if (AutoSync) - { - // Update the remote - Task.Run(() => Set(UserId, Id, key, value)); - } - } - - /// - public void Save(string compositeKey, IDictionary values) - { - InitCache(); - - if (KeyExists(compositeKey)) - { - ApplicationDataCompositeValue composite = (ApplicationDataCompositeValue)Cache[compositeKey]; - - foreach (KeyValuePair setting in values.ToList()) - { - if (composite.ContainsKey(setting.Key)) - { - composite[setting.Key] = _serializer.Serialize(setting.Value); - } - else - { - composite.Add(setting.Key, _serializer.Serialize(setting.Value)); - } - } - - // Update the cache - Cache[compositeKey] = composite; - - if (AutoSync) - { - // Update the remote - Task.Run(() => Set(UserId, Id, compositeKey, composite)); - } - } - else - { - ApplicationDataCompositeValue composite = new ApplicationDataCompositeValue(); - foreach (KeyValuePair setting in values.ToList()) - { - composite.Add(setting.Key, _serializer.Serialize(setting.Value)); - } - - // Update the cache - Cache[compositeKey] = composite; - - if (AutoSync) - { - // Update the remote - Task.Run(() => Set(UserId, Id, compositeKey, composite)); - } - } - } - - /// - public Task FileExistsAsync(string filePath) + public override Task FileExistsAsync(string filePath) { throw new NotImplementedException(); } /// - public Task ReadFileAsync(string filePath, T @default = default) + public override Task ReadFileAsync(string filePath, T @default = default) { throw new NotImplementedException(); } /// - public Task SaveFileAsync(string filePath, T value) + public override Task SaveFileAsync(string filePath, T value) { throw new NotImplementedException(); } - - private void InitCache() - { - if (Cache == null) - { - Cache = new Dictionary(); - } - } } } \ No newline at end of file diff --git a/SampleTest/SampleTest.csproj b/SampleTest/SampleTest.csproj index 4607273..9b47d2f 100644 --- a/SampleTest/SampleTest.csproj +++ b/SampleTest/SampleTest.csproj @@ -159,6 +159,9 @@ + + 4.0.0-preview.1 + 6.2.12 diff --git a/UnitTests/UnitTests.UWP/RoamingSettings/Test_OneDriveDataStore.cs b/UnitTests/UnitTests.UWP/RoamingSettings/Test_OneDriveDataStore.cs new file mode 100644 index 0000000..0e46dc4 --- /dev/null +++ b/UnitTests/UnitTests.UWP/RoamingSettings/Test_OneDriveDataStore.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Net.Authentication; +using CommunityToolkit.Uwp.Authentication; +using CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings; +using Microsoft.Toolkit.Uwp; +using Microsoft.Toolkit.Uwp.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading.Tasks; + +namespace UnitTests.UWP.Helpers +{ + [TestClass] + public class Test_OneDriveDataStore : VisualUITestBase + { + /// + /// Test the dafault state of a new instance of the OneDriveDataStore. + /// + [TestCategory("RoamingSettings")] + [TestMethod] + public async Task Test_Default() + { + var tcs = new TaskCompletionSource(); + + void test() + { + try + { + string userId = "TestUserId"; + string dataStoreId = "RoamingData.json"; + IObjectSerializer serializer = new SystemSerializer(); + + IRoamingSettingsDataStore dataStore = new OneDriveDataStore(userId, dataStoreId, serializer, false); + + // Evaluate the default state is as expected + Assert.IsFalse(dataStore.AutoSync); + Assert.IsNull(dataStore.Cache); + Assert.AreEqual(dataStoreId, dataStore.Id); + Assert.AreEqual(userId, dataStore.UserId); + + tcs.SetResult(true); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }; + + PrepareProvider(test); + + await tcs.Task; + } + + /// + /// Test the dafault state of a new instance of the OneDriveDataStore. + /// + [TestCategory("RoamingSettings")] + [TestMethod] + public async Task Test_Sync() + { + var tcs = new TaskCompletionSource(); + + async void test() + { + try + { + string userId = "TestUserId"; + string dataStoreId = "RoamingData.json"; + IObjectSerializer serializer = new SystemSerializer(); + + IRoamingSettingsDataStore dataStore = new OneDriveDataStore(userId, dataStoreId, serializer, false); + + try + { + // Attempt to delete the remote first. + await dataStore.Delete(); + } + catch + { + } + + dataStore.SyncCompleted += async (s, e) => + { + try + { + // Create a second instance to ensure that the Cache doesn't yield a false positive. + IRoamingSettingsDataStore dataStore2 = new OneDriveDataStore(userId, dataStoreId, serializer, false); + await dataStore2.Sync(); + + var foo = dataStore.Read("foo"); + Assert.AreEqual("bar", foo); + + tcs.SetResult(true); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }; + + dataStore.SyncFailed = (s, e) => + { + try + { + Assert.Fail("Sync Failed"); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }; + + dataStore.Save("foo", "bar"); + await dataStore.Sync(); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + + PrepareProvider(test); + + var result = await tcs.Task; + Assert.IsTrue(result); + } + + /// + /// Create a new instance of IProvider and check that it has the proper default state, then execute the provided action. + /// + private async void PrepareProvider(Action test) + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var provider = new WindowsProvider(new string[] { "User.Read", "Files.ReadWrite" }, autoSignIn: false); + + ProviderManager.Instance.ProviderUpdated += (s, e) => + { + var providerManager = s as ProviderManager; + if (providerManager.GlobalProvider.State == ProviderState.SignedIn) + { + test.Invoke(); + } + }; + + ProviderManager.Instance.GlobalProvider = provider; + + await provider.LoginAsync(); + }); + } + } +} diff --git a/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs b/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs index 11116df..c4160cb 100644 --- a/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs +++ b/UnitTests/UnitTests.UWP/RoamingSettings/Test_UserExtensionDataStore.cs @@ -1,5 +1,11 @@ -using CommunityToolkit.Net.Authentication; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Net.Authentication; +using CommunityToolkit.Uwp.Authentication; using CommunityToolkit.Uwp.Graph.Helpers.RoamingSettings; +using Microsoft.Toolkit.Uwp; using Microsoft.Toolkit.Uwp.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -15,31 +21,79 @@ public class Test_UserExtensionDataStore /// [TestCategory("RoamingSettings")] [TestMethod] - public async Task Test_MockProvider_Default() + public async Task Test_Default() { var tcs = new TaskCompletionSource(); - Action test = async () => + void test() { try { string userId = "TestUserId"; - string dataStoreId = "TestExtensionId"; + string dataStoreId = "RoamingData"; IObjectSerializer serializer = new SystemSerializer(); - UserExtensionDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer); + IRoamingSettingsDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer, false); // Evaluate the default state is as expected - Assert.IsTrue(dataStore.AutoSync); + Assert.IsFalse(dataStore.AutoSync); Assert.IsNull(dataStore.Cache); Assert.AreEqual(dataStoreId, dataStore.Id); Assert.AreEqual(userId, dataStore.UserId); - dataStore.SyncCompleted += (s, e) => + tcs.SetResult(true); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }; + + PrepareProvider(test); + + await tcs.Task; + } + + /// + /// Test the dafault state of a new instance of the UserExtensionDataStore. + /// + [TestCategory("RoamingSettings")] + [TestMethod] + public async Task Test_Sync() + { + var tcs = new TaskCompletionSource(); + + async void test() + { + try + { + string userId = "TestUserId"; + string dataStoreId = "RoamingData"; + IObjectSerializer serializer = new SystemSerializer(); + + IRoamingSettingsDataStore dataStore = new UserExtensionDataStore(userId, dataStoreId, serializer, false); + + try + { + // Attempt to delete the remote first. + await dataStore.Delete(); + } + catch + { + } + + dataStore.SyncCompleted += async (s, e) => { try { - Assert.Fail("Sync should have failed because we are using the MockProvider."); + // Create a second instance to ensure that the Cache doesn't yield a false positive. + IRoamingSettingsDataStore dataStore2 = new OneDriveDataStore(userId, dataStoreId, serializer, false); + await dataStore2.Sync(); + + var foo = dataStore.Read("foo"); + Assert.AreEqual("bar", foo); + + tcs.SetResult(true); } catch (Exception ex) { @@ -51,8 +105,7 @@ public async Task Test_MockProvider_Default() { try { - Assert.IsNull((s as UserExtensionDataStore).Cache); - tcs.SetResult(true); + Assert.Fail("Sync Failed"); } catch (Exception ex) { @@ -60,33 +113,43 @@ public async Task Test_MockProvider_Default() } }; + dataStore.Save("foo", "bar"); await dataStore.Sync(); } catch (Exception ex) { tcs.SetException(ex); } - }; + } - PrepareMockProvider(test); + PrepareProvider(test); - await tcs.Task; + var result = await tcs.Task; + Assert.IsTrue(result); } /// - /// Create a new instance of the MockProvider and check that it has the proper default state, then execute the provided action. + /// Create a new instance of IProvider and check that it has the proper default state, then execute the provided action. /// - private void PrepareMockProvider(Action test) + private async void PrepareProvider(Action test) { - ProviderManager.Instance.ProviderUpdated += (s, e) => + await App.DispatcherQueue.EnqueueAsync(async () => { - var providerManager = s as ProviderManager; - if (providerManager.GlobalProvider.State == ProviderState.SignedIn) + var provider = new WindowsProvider(new string[] { "User.ReadWrite" }, autoSignIn: false); + + ProviderManager.Instance.ProviderUpdated += (s, e) => { - test.Invoke(); - } - }; - ProviderManager.Instance.GlobalProvider = new MockProvider(); + var providerManager = s as ProviderManager; + if (providerManager.GlobalProvider.State == ProviderState.SignedIn) + { + test.Invoke(); + } + }; + + ProviderManager.Instance.GlobalProvider = provider; + + await provider.LoginAsync(); + }); } } } diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index 6e55334..ade8dc8 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -123,6 +123,7 @@ + UnitTestApp.xaml @@ -177,7 +178,7 @@ 2.1.2 - 13.0.1 + 10.0.3