Skip to content

Commit

Permalink
Add unit tests for wasm auth state provider
Browse files Browse the repository at this point in the history
  • Loading branch information
josephdecock committed Aug 27, 2024
1 parent 562c6c3 commit 2849247
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public override async Task<AuthenticationState> GetAuthenticationStateAsync()
_logger.LogInformation("starting background check..");
ITimer? timer = null;

timer = _timeProvider.CreateTimer(async _ =>
async void TimerCallback(object? _)
{
var currentUser = await _getUserService.GetUserAsync(false);
// Always notify that auth state has changed, because the user
Expand All @@ -65,7 +65,9 @@ public override async Task<AuthenticationState> GetAuthenticationStateAsync()
await timer.DisposeAsync();
}
}
},
}

timer = _timeProvider.CreateTimer(TimerCallback,
null,
TimeSpan.FromMilliseconds(_options.StateProviderPollingDelay),
TimeSpan.FromMilliseconds(_options.StateProviderPollingInterval));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Security.Claims;
using Duende.Bff.Blazor.Client.Internals;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Shouldly;

namespace Duende.Bff.Blazor.Client.UnitTests;

public class BffClientAuthenticationStateProviderTests
{
[Fact]
public async Task when_UserService_gives_anonymous_user_GetAuthState_returns_anonymous()
{
var userService = Substitute.For<IGetUserService>();
userService.GetUserAsync().Returns(new ClaimsPrincipal(new ClaimsIdentity()));
var sut = new BffClientAuthenticationStateProvider(
userService,
new FakeTimeProvider(),
TestMocks.MockOptions(),
Substitute.For<ILogger<BffClientAuthenticationStateProvider>>());

var authState = await sut.GetAuthenticationStateAsync();
authState.User.Identity.IsAuthenticated.ShouldBeFalse();
}

[Fact]
public async Task when_UserService_returns_persisted_user_GetAuthState_returns_that_user()
{
var expectedName = "test-user";
var userService = Substitute.For<IGetUserService>();
userService.GetUserAsync().Returns(new ClaimsPrincipal(new ClaimsIdentity(
new []{ new Claim("name", expectedName) },
"pwd", "name", "role")));
var sut = new BffClientAuthenticationStateProvider(
userService,
new FakeTimeProvider(),
TestMocks.MockOptions(),
Substitute.For<ILogger<BffClientAuthenticationStateProvider>>());

var authState = await sut.GetAuthenticationStateAsync();
authState.User.Identity.IsAuthenticated.ShouldBeTrue();
authState.User.Identity.Name.ShouldBe(expectedName);
userService.Received(1).GetUserAsync();
}

[Fact]
public async Task after_configured_delay_UserService_is_called_again_and_state_notification_is_called()
{
var expectedName = "test-user";
var userService = Substitute.For<IGetUserService>();
var time = new FakeTimeProvider();
userService.GetUserAsync().Returns(new ClaimsPrincipal(new ClaimsIdentity(
new []{ new Claim("name", expectedName) },
"pwd", "name", "role")));
var sut = new BffClientAuthenticationStateProvider(
userService,
time,
TestMocks.MockOptions(new BffBlazorOptions
{
StateProviderPollingDelay = 2000,
StateProviderPollingInterval = 10000

}),
Substitute.For<ILogger<BffClientAuthenticationStateProvider>>());

var authState = await sut.GetAuthenticationStateAsync();

// Initially, we have called the user service once to initialize
userService.Received(1).GetUserAsync();

// Advance time within the polling delay, and note that we still haven't made additional calls
time.Advance(TimeSpan.FromSeconds(1)); // t = 1
userService.Received(1).GetUserAsync();

// Advance time past the polling delay, and note that we make an additional call
time.Advance(TimeSpan.FromSeconds(2)); // t = 3
userService.Received(1).GetUserAsync(true);
userService.Received(1).GetUserAsync(false);

// Advance time within the polling interval, but more than the polling delay
// We don't expect additional calls yet
time.Advance(TimeSpan.FromSeconds(3)); // t = 6
userService.Received(1).GetUserAsync(true);
userService.Received(1).GetUserAsync(false);

// Advance time past the polling interval, and note that we make an additional call
time.Advance(TimeSpan.FromSeconds(10)); // t = 16
userService.Received(1).GetUserAsync(true);
userService.Received(2).GetUserAsync(false);
}

[Fact]
public async Task timer_stops_when_user_logs_out()
{
var expectedName = "test-user";
var userService = Substitute.For<IGetUserService>();
var time = new FakeTimeProvider();

var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
anonymousUser.Identity.IsAuthenticated.ShouldBeFalse();

var cachedUser = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim("name", expectedName),
new Claim("source", "cache")
], "pwd", "name", "role"));

var fetchedUser = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim("name", expectedName),
new Claim("source", "fetch")
], "pwd", "name", "role"));

userService.GetUserAsync(true).Returns(cachedUser);
userService.GetUserAsync(false).Returns(fetchedUser, anonymousUser);
var sut = new BffClientAuthenticationStateProvider(
userService,
time,
TestMocks.MockOptions(new BffBlazorOptions
{
StateProviderPollingDelay = 2000,
StateProviderPollingInterval = 10000

}),
Substitute.For<ILogger<BffClientAuthenticationStateProvider>>());

var authState = await sut.GetAuthenticationStateAsync();
time.Advance(TimeSpan.FromSeconds(5));
userService.Received(1).GetUserAsync(true);
userService.Received(1).GetUserAsync(false);

time.Advance(TimeSpan.FromSeconds(10));
userService.Received(1).GetUserAsync(true);
userService.Received(2).GetUserAsync(false);


time.Advance(TimeSpan.FromSeconds(50));
userService.Received(1).GetUserAsync(true);
userService.Received(2).GetUserAsync(false);

}
}
37 changes: 7 additions & 30 deletions test/Duende.Bff.Blazor.Client.UnitTests/GetUserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,16 @@
using System.Text.Json;
using Duende.Bff.Blazor.Client.Internals;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Shouldly;

namespace Duende.Bff.Blazor.Client.UnitTests;



public class GetUserServiceTests
{
record ClaimRecord(string type, object value);

private static IHttpClientFactory MockHttpClientFactory(string response, HttpStatusCode status)
{
var httpClient = new HttpClient(new MockHttpMessageHandler(response, status))
{
// Just have to set something that looks reasonably like a URL so that the HttpClient's internal validation
// doesn't blow up
BaseAddress = new Uri("https://example.com")
};
var factory = Substitute.For<IHttpClientFactory>();
factory.CreateClient(BffClientAuthenticationStateProvider.HttpClientName).Returns(httpClient);
return factory;
}

private static IOptions<BffBlazorOptions> MockOptions(BffBlazorOptions? opt = null)
{
var result = Substitute.For<IOptions<BffBlazorOptions>>();
result.Value.Returns(opt ?? new BffBlazorOptions());
return result;
}

[Fact]
public async Task FetchUser_maps_claims_into_ClaimsPrincipal()
{
Expand All @@ -49,12 +26,12 @@ public async Task FetchUser_maps_claims_into_ClaimsPrincipal()
new("foo", "bar")
};
var json = JsonSerializer.Serialize(claims);
var factory = MockHttpClientFactory(json, HttpStatusCode.OK);
var factory = TestMocks.MockHttpClientFactory(json, HttpStatusCode.OK);
var sut = new GetUserService(
factory,
Substitute.For<IPersistentUserService>(),
new FakeTimeProvider(),
MockOptions(),
TestMocks.MockOptions(),
Substitute.For<ILogger<GetUserService>>());

var result = await sut.FetchUser();
Expand All @@ -70,12 +47,12 @@ public async Task FetchUser_maps_claims_into_ClaimsPrincipal()
[Fact]
public async Task FetchUser_returns_anonymous_when_http_request_fails()
{
var factory = MockHttpClientFactory("Internal Server Error", HttpStatusCode.InternalServerError);
var factory = TestMocks.MockHttpClientFactory("Internal Server Error", HttpStatusCode.InternalServerError);
var sut = new GetUserService(
factory,
Substitute.For<IPersistentUserService>(),
new FakeTimeProvider(),
MockOptions(),
TestMocks.MockOptions(),
Substitute.For<ILogger<GetUserService>>());
var errorResult = await sut.FetchUser();
errorResult.Identity?.IsAuthenticated.ShouldBeFalse();
Expand All @@ -100,7 +77,7 @@ public async Task GetUser_returns_persisted_user_if_refresh_not_required()
Substitute.For<IHttpClientFactory>(),
persistentUserService,
timeProvider,
MockOptions(),
TestMocks.MockOptions(),
Substitute.For<ILogger<GetUserService>>());

timeProvider.SetUtcNow(startTime);
Expand Down Expand Up @@ -137,10 +114,10 @@ public async Task GetUser_fetches_user_if_no_persisted_user()
};
var json = JsonSerializer.Serialize(claims);
var sut = new GetUserService(
MockHttpClientFactory(json, HttpStatusCode.OK),
TestMocks.MockHttpClientFactory(json, HttpStatusCode.OK),
Substitute.For<IPersistentUserService>(),
timeProvider,
MockOptions(),
TestMocks.MockOptions(),
Substitute.For<ILogger<GetUserService>>());

timeProvider.SetUtcNow(startTime);
Expand Down
31 changes: 31 additions & 0 deletions test/Duende.Bff.Blazor.Client.UnitTests/TestMocks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Net;
using Microsoft.Extensions.Options;
using NSubstitute;

namespace Duende.Bff.Blazor.Client.UnitTests;

public static class TestMocks
{
public static IHttpClientFactory MockHttpClientFactory(string response, HttpStatusCode status)
{
var httpClient = new HttpClient(new MockHttpMessageHandler(response, status))
{
// Just have to set something that looks reasonably like a URL so that the HttpClient's internal validation
// doesn't blow up
BaseAddress = new Uri("https://example.com")
};
var factory = Substitute.For<IHttpClientFactory>();
factory.CreateClient(BffClientAuthenticationStateProvider.HttpClientName).Returns(httpClient);
return factory;
}

public static IOptions<BffBlazorOptions> MockOptions(BffBlazorOptions? opt = null)
{
var result = Substitute.For<IOptions<BffBlazorOptions>>();
result.Value.Returns(opt ?? new BffBlazorOptions());
return result;
}
}

0 comments on commit 2849247

Please sign in to comment.