diff --git a/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs b/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs index 6d968673c9..fba25e0108 100644 --- a/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs +++ b/Source/Csla.AspNetCore/Blazor/State/SessionManager.cs @@ -79,8 +79,8 @@ public void PurgeSessions(TimeSpan expiration) } // wasm client-side methods - Task ISessionManager.RetrieveSession() => throw new NotImplementedException(); + Task ISessionManager.RetrieveSession(TimeSpan timeout) => throw new NotImplementedException(); Session ISessionManager.GetCachedSession() => throw new NotImplementedException(); - Task ISessionManager.SendSession() => throw new NotImplementedException(); + Task ISessionManager.SendSession(TimeSpan timeout) => throw new NotImplementedException(); } } diff --git a/Source/Csla.Blazor.WebAssembly.Tests/Csla.Blazor.WebAssembly.Tests.csproj b/Source/Csla.Blazor.WebAssembly.Tests/Csla.Blazor.WebAssembly.Tests.csproj new file mode 100644 index 0000000000..8af20c9d4a --- /dev/null +++ b/Source/Csla.Blazor.WebAssembly.Tests/Csla.Blazor.WebAssembly.Tests.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + false + + Library + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs b/Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs new file mode 100644 index 0000000000..8788766da0 --- /dev/null +++ b/Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs @@ -0,0 +1,201 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading; +using System.Threading.Tasks; +using Csla.Blazor.WebAssembly.State; +using Csla.State; +using System.Net.Http; +using Csla.Blazor.WebAssembly.Configuration; +using Csla.Core; +using Csla.Runtime; +using System.Net; +using Csla.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using Csla.Serialization.Mobile; +using Microsoft.AspNetCore.Components.Authorization; +using NSubstitute; + +namespace Csla.Test.State +{ + [TestClass] + public class SessionManagerTests + { + private SessionManager _sessionManager; + private SessionMessage _sessionValue; + + [TestInitialize] + public void Initialize() + { + var mockServiceProvider = Substitute.For(); + + // Mock AuthenticationStateProvider + var mockAuthStateProvider = Substitute.For(); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(AuthenticationStateProvider)).Returns(mockAuthStateProvider); + + _sessionValue = new SessionMessage + { + // Set properties here + // For example: + Principal = new Security.CslaClaimsPrincipal() { }, + Session = [] + }; + + // Mock ISerializationFormatter + var mockFormatter = Substitute.For(); + mockFormatter.Serialize(Arg.Any(), Arg.Any()); + mockFormatter.Deserialize(Arg.Any()).Returns(_sessionValue); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(Csla.Serialization.Mobile.MobileFormatter)).Returns(mockFormatter); + + var mockActivator = Substitute.For(); + mockActivator.CreateInstance(Arg.Is(t => t == typeof(Csla.Serialization.Mobile.MobileFormatter))).Returns(mockFormatter); + mockActivator.InitializeInstance(Arg.Any()); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(Csla.Server.IDataPortalActivator)).Returns(mockActivator); + + // Mock IContextManager + var mockContextManager = Substitute.For(); + mockContextManager.IsValid.Returns(true); + + // Mock IContextManagerLocal + var mockLocalContextManager = Substitute.For(); + + // Mock IServiceProvider + mockServiceProvider.GetService(typeof(IRuntimeInfo)).Returns(new RuntimeInfo()); + + // Mock IEnumerable + var mockContextManagerList = new List { mockContextManager }; + + // Mock ApplicationContextAccessor + var mockApplicationContextAccessor = Substitute.For(mockContextManagerList, mockLocalContextManager, mockServiceProvider); + + var _applicationContext = new ApplicationContext(mockApplicationContextAccessor); + + _sessionManager = new SessionManager(_applicationContext, GetHttpClient(_sessionValue, _applicationContext), new BlazorWebAssemblyConfigurationOptions { SyncContextWithServer = true }); + } + + public class TestHttpMessageHandler(SessionMessage session, ApplicationContext _applicationContext) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var response = new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("{\"ResultStatus\":0, \"SessionData\":\"" + Convert.ToBase64String(GetSession(session, _applicationContext)) + "\"}"), + }; + return Task.FromResult(response); + } + } + private static HttpClient GetHttpClient(SessionMessage session, ApplicationContext _applicationContext) + { + var handlerMock = new TestHttpMessageHandler(session,_applicationContext); + // use real http client with mocked handler here + var httpClient = new HttpClient(handlerMock) + { + BaseAddress = new Uri("http://test.com/"), + }; + return httpClient; + } + + private static byte[] GetSession(SessionMessage session, ApplicationContext _applicationContext) + { + var info = new MobileFormatter(_applicationContext).SerializeToDTO(session); + var ms = new MemoryStream(); + new CslaBinaryWriter(_applicationContext).Write(ms, info); + ms.Position = 0; + var array = ms.ToArray(); + return array; + } + + [TestMethod] + public async Task RetrieveSession_WithTimeoutValue_ShouldNotThrowException() + { + var timeout = TimeSpan.FromHours(1); + var session = await _sessionManager.RetrieveSession(timeout); + Assert.AreEqual(session, _sessionValue.Session); + } + + [TestMethod] + public async Task RetrieveSession_WithZeroTimeout_ShouldThrowTimeoutException() + { + var timeout = TimeSpan.Zero; + await Assert.ThrowsExceptionAsync(() => _sessionManager.RetrieveSession(timeout)); + } + + [TestMethod] + public async Task RetrieveSession_WithValidCancellationToken_ShouldNotThrowException() + { + var cts = new CancellationTokenSource(); + var session = await _sessionManager.RetrieveSession(cts.Token); + Assert.AreEqual(session, _sessionValue.Session); + } + + [TestMethod] + public async Task RetrieveSession_WithCancelledCancellationToken_ShouldThrowTaskCanceledException() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsExceptionAsync(() => _sessionManager.RetrieveSession(cts.Token)); + } + + [TestMethod] + public async Task SendSession_WithTimeoutValue_ShouldNotThrowException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var timeout = TimeSpan.FromHours(1); + await _sessionManager.SendSession(timeout); + Assert.IsTrue(true); + } + + [TestMethod] + public async Task SendSession_WithZeroTimeout_ShouldThrowTimeoutException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var timeout = TimeSpan.Zero; + await Assert.ThrowsExceptionAsync(() => _sessionManager.SendSession(timeout)); + } + + [TestMethod] + public async Task SendSession_WithValidCancellationToken_ShouldNotThrowException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var cts = new CancellationTokenSource(); + await _sessionManager.SendSession(cts.Token); + Assert.IsTrue(true); + } + + [TestMethod] + public async Task SendSession_WithCancelledCancellationToken_ShouldThrowTaskCanceledException() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsExceptionAsync(() => _sessionManager.SendSession(cts.Token)); + } + + + [TestMethod] + public async Task RetrieveCachedSessionSession() + { + await _sessionManager.RetrieveSession(TimeSpan.FromHours(1)); + + var session = _sessionManager.GetCachedSession(); + Assert.IsNotNull(session); + } + [TestMethod] + public void RetrieveNullCachedSessionSession() + { + var session = _sessionManager.GetCachedSession(); + Assert.IsNull(session); + } + } +} diff --git a/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs b/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs index b8c61c0328..36458f3ab4 100644 --- a/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs +++ b/Source/Csla.Blazor.WebAssembly/State/SessionManager.cs @@ -13,6 +13,8 @@ using Csla.State; using Microsoft.AspNetCore.Components.Authorization; using Csla.Blazor.State.Messages; +using System.Reflection; +using System.Threading; namespace Csla.Blazor.WebAssembly.State { @@ -39,84 +41,132 @@ public Session GetCachedSession() return _session; } - /// - /// Retrieves the current user's session from - /// the web server to the wasm client - /// if SyncContextWithServer is true. - /// - public async Task RetrieveSession() - { - if (_options.SyncContextWithServer) - { - long lastTouched = 0; - if (_session != null) - lastTouched = _session.LastTouched; - var url = $"{_options.StateControllerName}?lastTouched={lastTouched}"; + /// + /// Retrieves the current user's session from + /// the web server to the wasm client + /// if SyncContextWithServer is true. + /// + /// The timeout duration for the operation. + /// The retrieved session. + public async Task RetrieveSession(TimeSpan timeout) + { + try + { + return await RetrieveSession(GetCancellationToken(timeout)); + } + catch (TaskCanceledException tcex) + { + throw new TimeoutException($"{this.GetType().FullName}.{nameof(RetrieveSession)}.", tcex); + } + } - var stateResult = await client.GetFromJsonAsync(url); - if (stateResult.ResultStatus == ResultStatuses.Success) + /// + /// Retrieves the current user's session from + /// the web server to the wasm client + /// if SyncContextWithServer is true. + /// + /// The cancellation token. + /// The retrieved session. + public async Task RetrieveSession(CancellationToken ct) { - var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); - var buffer = new MemoryStream(stateResult.SessionData) - { - Position = 0 - }; - var message = (SessionMessage)formatter.Deserialize(buffer); - _session = message.Session; - if (message.Principal is not null && - ApplicationContext.GetRequiredService() is CslaAuthenticationStateProvider provider) - { - provider.SetPrincipal(message.Principal); - } + if (_options.SyncContextWithServer) + { + long lastTouched = 0; + if (_session != null) + lastTouched = _session.LastTouched; + var url = $"{_options.StateControllerName}?lastTouched={lastTouched}"; + var stateResult = await client.GetFromJsonAsync(url, ct); + if (stateResult.ResultStatus == ResultStatuses.Success) + { + var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); + var buffer = new MemoryStream(stateResult.SessionData) + { + Position = 0 + }; + var message = (SessionMessage)formatter.Deserialize(buffer); + _session = message.Session; + if (message.Principal is not null && + ApplicationContext.GetRequiredService() is CslaAuthenticationStateProvider provider) + { + provider.SetPrincipal(message.Principal); + } + } + else // NoUpdates + { + _session = GetSession(); + } + } + else + { + _session = GetSession(); + } + return _session; } - else // NoUpdates + + /// + /// Sends the current user's session from + /// the wasm client to the web server + /// if SyncContextWithServer is true. + /// + /// The timeout duration for the operation. + /// A task representing the asynchronous operation. + public async Task SendSession(TimeSpan timeout) { - _session = GetSession(); + try + { + await SendSession(GetCancellationToken(timeout)); + } + catch (TaskCanceledException tcex) + { + throw new TimeoutException($"{this.GetType().FullName}.{nameof(SendSession)}.", tcex); + } } - } - else - { - _session = GetSession(); - } - return _session; - } - /// - /// Sends the current user's session from - /// the wasm client to the web server - /// if SyncContextWithServer is true. - /// - public async Task SendSession() + private static CancellationToken GetCancellationToken(TimeSpan timeout) { - _session.Touch(); - if (_options.SyncContextWithServer) - { - var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); - var buffer = new MemoryStream(); - formatter.Serialize(buffer, _session); - buffer.Position = 0; - await client.PutAsJsonAsync(_options.StateControllerName, buffer.ToArray()); - } + var cts = new CancellationTokenSource(timeout); + return cts.Token; } + /// + /// Sends the current user's session from + /// the wasm client to the web server + /// if SyncContextWithServer is true. + /// + /// The cancellation token. + /// A task representing the asynchronous operation. + public async Task SendSession(CancellationToken ct) + { + _session.Touch(); + if (_options.SyncContextWithServer) + { + var formatter = SerializationFormatterFactory.GetFormatter(ApplicationContext); + var buffer = new MemoryStream(); + formatter.Serialize(buffer, _session); + buffer.Position = 0; + await client.PutAsJsonAsync(_options.StateControllerName, buffer.ToArray(), ct); + } + } - /// - /// Gets or creates the session data. - /// - private Session GetSession() - { - Session result; - if (_session != null) - { - result = _session; - } - else - { - result = []; - result.Touch(); - } - return result; - } + + /// + /// Gets or creates the session data. + /// + /// The session data. + private Session GetSession() + { + Session result; + if (_session != null) + { + result = _session; + } + else + { + result = []; + result.Touch(); + } + return result; + } // server-side methods Session ISessionManager.GetSession() => throw new NotImplementedException(); diff --git a/Source/Csla.Blazor/State/StateManager.cs b/Source/Csla.Blazor/State/StateManager.cs index b9a58c50d9..8631eeb92c 100644 --- a/Source/Csla.Blazor/State/StateManager.cs +++ b/Source/Csla.Blazor/State/StateManager.cs @@ -41,10 +41,9 @@ public Task InitializeAsync(TimeSpan timeout) /// Time to wait before timing out private async Task GetState(TimeSpan timeout) { - Session session; var isBrowser = OperatingSystem.IsBrowser(); if (isBrowser) - session = await _sessionManager.RetrieveSession(); + _ = await _sessionManager.RetrieveSession(timeout); } /// @@ -57,12 +56,12 @@ private async Task GetState(TimeSpan timeout) /// at which you know the user is navigating to another /// page. /// - public async Task SaveState() + public async Task SaveState(TimeSpan timeout) { var isBrowser = OperatingSystem.IsBrowser(); if (isBrowser) { - await _sessionManager.SendSession(); + await _sessionManager.SendSession(timeout); } } } diff --git a/Source/Csla/State/ISessionManager.cs b/Source/Csla/State/ISessionManager.cs index 6ce61594d0..c089102e62 100644 --- a/Source/Csla/State/ISessionManager.cs +++ b/Source/Csla/State/ISessionManager.cs @@ -18,7 +18,7 @@ public interface ISessionManager /// Retrieves the current user's session from /// the web server to the wasm client. /// - Task RetrieveSession(); + Task RetrieveSession(TimeSpan timeout); /// /// Gets the current user's session from the cache. /// @@ -27,7 +27,7 @@ public interface ISessionManager /// Sends the current user's session from /// the wasm client to the web server. /// - Task SendSession(); + Task SendSession(TimeSpan timeout); #endregion #region Server diff --git a/Source/csla.test.sln b/Source/csla.test.sln index 8eddba99cf..1036a141cc 100644 --- a/Source/csla.test.sln +++ b/Source/csla.test.sln @@ -60,6 +60,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphMergerTest.Business", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphMergerTest.BusinessTests", "GraphMergerTest\GraphMergerTest.BusinessTests\GraphMergerTest.BusinessTests.csproj", "{FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Csla.Blazor.WebAssembly.Tests", "Csla.Blazor.WebAssembly.Tests\Csla.Blazor.WebAssembly.Tests.csproj", "{430DDCF1-1570-4193-9B1E-774303F6FC34}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -964,6 +966,46 @@ Global {FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}.Testing|x64.Build.0 = Debug|Any CPU {FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}.Testing|x86.ActiveCfg = Debug|Any CPU {FE652EBA-94E7-4BB2-99C8-18B0382E0D9A}.Testing|x86.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|ARM.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|ARM.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x64.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x64.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x86.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Debug|x86.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|Any CPU.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|Any CPU.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|ARM.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|ARM.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x64.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x64.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x86.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Debug|x86.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|Any CPU.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|Any CPU.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|ARM.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|ARM.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x64.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x64.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x86.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Mono Release|x86.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|Any CPU.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|ARM.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|ARM.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x64.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x64.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x86.ActiveCfg = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Release|x86.Build.0 = Release|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|Any CPU.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|Any CPU.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|ARM.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|ARM.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x64.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x64.Build.0 = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x86.ActiveCfg = Debug|Any CPU + {430DDCF1-1570-4193-9B1E-774303F6FC34}.Testing|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE