Skip to content

Commit

Permalink
Breaking change: Blazor StateManager does not use the provided timeou…
Browse files Browse the repository at this point in the history
…t parameter (#3910)

* Updated ISessionManager and SessionManager with timeout and cancellation support

Updated the `ISessionManager` interface and `SessionManager` class to include a `TimeSpan` parameter in the `RetrieveSession` and `SendSession` methods for timeout specification. Overloads of these methods have been added to accept a `CancellationToken` parameter for operation cancellation. The `StateManager` class's `GetState` and `SaveState` methods have also been updated to accept a `TimeSpan` parameter for timeout. Added a new project `Csla.Blazor.WebAssembly.Tests` for unit tests of the `SessionManager` class, including tests for timeout and cancellation scenarios. The solution file and a new project settings file have been updated accordingly. The `SessionManager` class now handles `OperationCanceledException` by rethrowing the exception.

* `Refactor SessionManager and StateManager for improved session handling`

Refactored `SessionManagerTests.cs` and `SessionManager.cs` to improve session handling. The `SessionMessage` object has been changed from a local variable to a class-level variable, allowing it to be accessed across different methods. The `RetrieveSession` and `SendSession` methods have been updated to return the session and assert that the returned session is equal to `SessionValue.Session`. The exception type expected in these methods with zero timeout and cancelled cancellation token has been changed from `TaskCanceledException` to `TimeoutException`. The `GetCancellationToken` method has been simplified to create a `CancellationTokenSource` with a timeout directly. In `StateManager.cs`, the `GetState` method has been updated to not return a `Session` object, instead, it just retrieves the session without assigning it to a variable.

* Refactor SessionManagerTests and remove certain test methods

This commit includes a significant refactoring of the `SessionManagerTests.cs` file. The `GetHttpClient` method has been simplified by removing the creation of a new `MemoryStream` instance and the use of `CslaBinaryWriter`. The stream's position is no longer reset to 0, and the array is directly obtained from the stream.

Several test methods have been removed, including `RetrieveSession_WithTimeoutValue_ShouldNotThrowException`, `RetrieveSession_WithValidCancellationToken_ShouldNotThrowException`, and `SendSession_WithZeroTimeout_ShouldThrowTimeoutException`. These tests were checking for specific exceptions or session values.

The `SendSession_WithTimeoutValue_ShouldNotThrowException` test method has been modified by removing the assertion that was previously checking if the operation is successful.

Lastly, the `RetrieveCachedSessionSession` method has been modified by removing the call to `GetCachedSession` method of `_sessionManager`.

* Refactor SessionManager and update related tests

Updated `SessionManager.cs` methods `RetrieveSession` and `SendSession` to handle `TaskCanceledException` internally and rethrow as `TimeoutException`. Simplified `SendSession` by removing exception handling and refactored `RetrieveSession` to move `stateResult` handling outside of try-catch block. Renamed test methods in `SessionManagerTests.cs` to reflect these changes and updated expected exception type.

* `Convert SaveState to async in StateManager.cs`

` `

`The SaveState method in StateManager.cs has been converted from a synchronous method to an asynchronous one. This is indicated by the addition of the async keyword and the change in return type from void to Task. Additionally, the call to _sessionManager.SendSession(timeout) within the SaveState method has been updated to use the await keyword, making this method call awaited, in line with the change to make SaveState an asynchronous method.`

* Updated SessionManager and its tests for async operations and better error handling

In this commit, several updates were made to the `SessionManager.cs` and `SessionManagerTests.cs` files. The variable `_sessionManager` was renamed to `_SessionManager` in `SessionManagerTests.cs`. The `Initialize` method was converted to an asynchronous method, and the `RetrieveSession` method call in it was updated to use `await`.

XML comments were added to the `RetrieveSession`, `SendSession`, and `GetSession` methods in `SessionManager.cs` for better code documentation. The `RetrieveSession` and `SendSession` methods were updated to handle `TaskCanceledException` and throw a `TimeoutException` with a custom message.

The `GetSession` method was updated to handle the case where `_session` is `null`, creating and returning a new `Session` object in this case. The `SendSession` method was updated to serialize the `_session` object and send it to the server if `SyncContextWithServer` is `true`.

Finally, the `RetrieveSession` method was updated to retrieve the session from the server if `SyncContextWithServer` is `true`, deserializing and storing the retrieved session in `_session` or calling `GetSession` to get or create a new session if the retrieval is unsuccessful.

* Subject: Refactored test methods and updated session retrieval

Refactored the `Initialize` method in `SessionManagerTests.cs` to be synchronous and removed the `RetrieveSession` call. The `RetrieveSession` call has been added to four test methods to ensure session retrieval before each test. Renamed and converted `RetrieveCAchedSessionSession` to an asynchronous method, adding a `RetrieveSession` call and an assertion for non-null cached sessions. Added a new test method `RetrieveNullCachedSessionSession` to assert null cached sessions.

* Refactor variable names to follow C# naming convention

Updated variable names `_SessionManager` and `SessionValue` to `_sessionManager` and `_sessionValue` respectively, to adhere to the common C# naming convention for private fields. All instances of these variables in the code, including in the `Initialize()`, `Deserialize()`, `RetrieveSession()`, `SendSession()`, and `GetCachedSession()` methods, as well as in test assertions, have been updated accordingly.

* Switch from Moq to NSubstitute in Csla.Blazor.WebAssembly.Tests

This commit represents a significant shift in the mocking framework used for unit testing in the `Csla.Blazor.WebAssembly.Tests.csproj` project. The `Moq` package has been replaced with `NSubstitute` in the project file and throughout the `SessionManagerTests.cs` file. This includes changes in the way mocks are created, set up, and how return values are specified for mocked methods and properties.

Additionally, a new `TestHttpMessageHandler` class has been added to `SessionManagerTests.cs` to mock the behavior of an `HttpClient`. The `GetHttpClient` method has been updated to use this new class, aligning with the switch from `Moq` to `NSubstitute`.

---------

Co-authored-by: Luiz Fernando Bicalho <[email protected]>
Co-authored-by: Rockford Lhotka <[email protected]>
  • Loading branch information
3 people authored May 9, 2024
1 parent 579c84d commit a60f4da
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 76 deletions.
4 changes: 2 additions & 2 deletions Source/Csla.AspNetCore/Blazor/State/SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ public void PurgeSessions(TimeSpan expiration)
}

// wasm client-side methods
Task<Session> ISessionManager.RetrieveSession() => throw new NotImplementedException();
Task<Session> 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<ApplicationIcon />
<OutputType>Library</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NSubstitute" Version="5.1.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Csla.AspNetCore\Csla.AspNetCore.csproj" />
<ProjectReference Include="..\Csla.Blazor.WebAssembly\Csla.Blazor.WebAssembly.csproj" />
<ProjectReference Include="..\Csla.Blazor\Csla.Blazor.csproj" />
<ProjectReference Include="..\Csla.TestHelpers\Csla.TestHelpers.csproj" />
<ProjectReference Include="..\Csla\Csla.csproj" />
</ItemGroup>

</Project>
201 changes: 201 additions & 0 deletions Source/Csla.Blazor.WebAssembly.Tests/SessionManagerTests.cs
Original file line number Diff line number Diff line change
@@ -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<IServiceProvider>();

// Mock AuthenticationStateProvider
var mockAuthStateProvider = Substitute.For<AuthenticationStateProvider>();

// 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<ISerializationFormatter>();
mockFormatter.Serialize(Arg.Any<Stream>(), Arg.Any<object>());
mockFormatter.Deserialize(Arg.Any<Stream>()).Returns(_sessionValue);

// Mock IServiceProvider
mockServiceProvider.GetService(typeof(Csla.Serialization.Mobile.MobileFormatter)).Returns(mockFormatter);

var mockActivator = Substitute.For<Csla.Server.IDataPortalActivator>();
mockActivator.CreateInstance(Arg.Is<Type>(t => t == typeof(Csla.Serialization.Mobile.MobileFormatter))).Returns(mockFormatter);
mockActivator.InitializeInstance(Arg.Any<object>());

// Mock IServiceProvider
mockServiceProvider.GetService(typeof(Csla.Server.IDataPortalActivator)).Returns(mockActivator);

// Mock IContextManager
var mockContextManager = Substitute.For<IContextManager>();
mockContextManager.IsValid.Returns(true);

// Mock IContextManagerLocal
var mockLocalContextManager = Substitute.For<IContextManagerLocal>();

// Mock IServiceProvider
mockServiceProvider.GetService(typeof(IRuntimeInfo)).Returns(new RuntimeInfo());

// Mock IEnumerable<IContextManager>
var mockContextManagerList = new List<IContextManager> { mockContextManager };

// Mock ApplicationContextAccessor
var mockApplicationContextAccessor = Substitute.For<ApplicationContextAccessor>(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<HttpResponseMessage> 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<TimeoutException>(() => _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<TaskCanceledException>(() => _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<TimeoutException>(() => _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<TaskCanceledException>(() => _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);
}
}
}
Loading

0 comments on commit a60f4da

Please sign in to comment.