Skip to content
This repository has been archived by the owner on Jan 24, 2025. It is now read-only.

Commit

Permalink
Add Blazor.Client unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
josephdecock committed Aug 26, 2024
1 parent 8d650a3 commit 4af5a33
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 3 deletions.
7 changes: 7 additions & 0 deletions src/Duende.Bff.Blazor.Client/BffBlazorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ public class BffBlazorOptions
/// </summary>
public string? RemoteApiBaseAddress { get; set; } = null;

/// <summary>
/// The base address to use for the state provider's calls to the
/// /bff/user endpoint. If unset (the default), the blazor hosting
/// environment's base address is used.
/// </summary>
public string? StateProviderBaseAddress { get; set; } = null;

/// <summary>
/// The delay, in milliseconds, before the AuthenticationStateProvider
/// will start polling the /bff/user endpoint. Defaults to 1000 ms.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public BffClientAuthenticationStateProvider(
IOptions<BffBlazorOptions> options,
ILogger<BffClientAuthenticationStateProvider> logger)
{
_client = factory.CreateClient("BffAuthenticationStateProvider");
_client = factory.CreateClient(nameof(BffClientAuthenticationStateProvider));
_logger = logger;
_cachedUser = GetPersistedUser(state);
if (_cachedUser.Identity?.IsAuthenticated == true)
Expand Down
41 changes: 39 additions & 2 deletions src/Duende.Bff.Blazor.Client/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,31 @@ public static IServiceCollection AddBffBlazorClient(this IServiceCollection serv
services
.AddAuthorizationCore()
.AddScoped<AuthenticationStateProvider, BffClientAuthenticationStateProvider>()
// TODO - Should this have a different lifetime?
.AddTransient<AntiforgeryHandler>()
.AddHttpClient("BffAuthenticationStateProvider", (sp, client) =>
.AddHttpClient(nameof(BffClientAuthenticationStateProvider), (sp, client) =>
{
var baseAddress = GetBaseAddress(sp);
var baseAddress = GetStateProviderBaseAddress(sp);
client.BaseAddress = new Uri(baseAddress);
}).AddHttpMessageHandler<AntiforgeryHandler>();

return services;
}

private static string GetStateProviderBaseAddress(IServiceProvider sp)
{
var opt = sp.GetRequiredService<IOptions<BffBlazorOptions>>();
if (opt.Value.StateProviderBaseAddress != null)
{
return opt.Value.StateProviderBaseAddress;
}
else
{
var hostEnv = sp.GetRequiredService<IWebAssemblyHostEnvironment>();
return hostEnv.BaseAddress;
}
}

private static string GetBaseAddress(IServiceProvider sp)
{
var opt = sp.GetRequiredService<IOptions<BffBlazorOptions>>();
Expand Down Expand Up @@ -96,20 +111,42 @@ private static void SetBaseAddress(IServiceProvider sp, HttpClient client)
client.BaseAddress = new Uri(new Uri(baseAddress), remoteApiPath);
}

/// <summary>
/// Adds a named <see cref="HttpClient"/> for use when invoking remote APIs
/// proxied through Duende.Bff and configures the client with a callback.
/// </summary>
/// <param name="clientName">The name of that <see cref="HttpClient"/> to
/// configure. A common use case is to use the same named client in multiple
/// render contexts that are automatically switched between via interactive
/// render modes.</param>
/// <param name="configureClient">A configuration callback used to set up
/// the <see cref="HttpClient"/>.</param>
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
Action<HttpClient> configureClient)
{
return services.AddHttpClient(clientName, SetBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

/// <summary>
/// Adds a named <see cref="HttpClient"/> for use when invoking remote APIs
/// proxied through Duende.Bff and configures the client with a callback
/// that has access to the underlying service provider.
/// </summary>
/// <param name="clientName">The name of that <see cref="HttpClient"/> to
/// configure. A common use case is to use the same named client in multiple
/// render contexts that are automatically switched between via interactive
/// render modes.</param>
/// <param name="configureClient">A configuration callback used to set up
/// the <see cref="HttpClient"/>.</param>
public static IHttpClientBuilder AddRemoteApiHttpClient(this IServiceCollection services, string clientName,
Action<IServiceProvider, HttpClient>? configureClient = null)
{
return services.AddHttpClient(clientName, SetBaseAddress(configureClient))
.AddHttpMessageHandler<AntiforgeryHandler>();
}

// TODO - Manually test this API.
public static IHttpClientBuilder AddRemoteApiHttpClient<T>(this IServiceCollection services,
Action<HttpClient> configureClient)
where T : class
Expand Down
30 changes: 30 additions & 0 deletions test/Duende.Bff.Blazor.Client.UnitTests/AntiforgeryHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using NSubstitute;
using Shouldly;

namespace Duende.Bff.Blazor.Client.UnitTests;

public class AntiforgeryHandlerTests
{
[Fact]
public async Task Adds_expected_header()
{
var sut = new TestAntiforgeryHandler()
{
InnerHandler = Substitute.For<HttpMessageHandler>()
};

var request = new HttpRequestMessage();

await sut.SendAsync(request, CancellationToken.None);

request.Headers.ShouldContain(h => h.Key == "X-CSRF" && h.Value.Contains("1"));
}
}

public class TestAntiforgeryHandler : AntiforgeryHandler
{
public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../src/Duende.Bff.Blazor.Client/Duende.Bff.Blazor.Client.csproj"></ProjectReference>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;

namespace Duende.Bff.Blazor.Client.UnitTests;

public class ServiceCollectionExtensionsTests
{
[Theory]
[InlineData("https://example.com/", "https://example.com/")]
[InlineData("https://example.com", "https://example.com/")]
public void When_base_address_option_is_set_AddBffBlazorClient_configures_HttpClient_base_address(string configuredRemoteAddress, string expectedBaseAddress)
{
var sut = new ServiceCollection();
sut.AddBffBlazorClient();
sut.Configure<BffBlazorOptions>(opt =>
{
opt.StateProviderBaseAddress = configuredRemoteAddress;
});


var sp = sut.BuildServiceProvider();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var httpClient = httpClientFactory?.CreateClient(nameof(BffClientAuthenticationStateProvider));
httpClient.ShouldNotBeNull();
httpClient.BaseAddress.ShouldNotBeNull();
httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
}

[Fact]
public void When_base_address_option_is_default_AddBffBlazorClient_configures_HttpClient_base_address_from_host_env()
{
var expectedBaseAddress = "https://example.com/";

var sut = new ServiceCollection();
sut.AddBffBlazorClient();
var env = Substitute.For<IWebAssemblyHostEnvironment>();
env.BaseAddress.Returns(expectedBaseAddress);
sut.AddSingleton(env);

var sp = sut.BuildServiceProvider();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var httpClient = httpClientFactory?.CreateClient(nameof(BffClientAuthenticationStateProvider));
httpClient.ShouldNotBeNull();
httpClient.BaseAddress.ShouldNotBeNull();
httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
}

[Theory]
[InlineData("https://example.com/", "remote-apis", "https://example.com/remote-apis/")]
[InlineData("https://example.com/", null, "https://example.com/remote-apis/")]
[InlineData("https://example.com", null, "https://example.com/remote-apis/")]
[InlineData("https://example.com", "custom/route/to/apis", "https://example.com/custom/route/to/apis/")]
[InlineData("https://example.com/with/base/path", "custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
[InlineData("https://example.com/with/base/path/", "custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
[InlineData("https://example.com/with/base/path", "/custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
[InlineData("https://example.com/with/base/path/", "/custom/route/to/apis", "https://example.com/with/base/path/custom/route/to/apis/")]
[InlineData("https://example.com/with/base/path", null, "https://example.com/with/base/path/remote-apis/")]
public void AddRemoteApiHttpClient_configures_HttpClient_base_address(string configuredRemoteAddress, string? configuredRemotePath, string expectedBaseAddress)
{
var sut = new ServiceCollection();
sut.AddBffBlazorClient();
sut.AddRemoteApiHttpClient("clientName");
sut.Configure<BffBlazorOptions>(opt =>
{
if (configuredRemoteAddress != null)
{
opt.RemoteApiBaseAddress = configuredRemoteAddress;
}
if (configuredRemotePath != null)
{
opt.RemoteApiPath = configuredRemotePath;
}
});


var sp = sut.BuildServiceProvider();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var httpClient = httpClientFactory?.CreateClient("clientName");
httpClient.ShouldNotBeNull();
httpClient.BaseAddress.ShouldNotBeNull();
httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
}

[Fact]
public void When_base_address_option_is_default_AddRemoteApiHttpClient_configures_HttpClient_base_address_from_host_env()
{
var hostBaseAddress = "https://example.com/";
var expectedBaseAddress = "https://example.com/remote-apis/";

var sut = new ServiceCollection();
sut.AddBffBlazorClient();
sut.AddRemoteApiHttpClient("clientName");
var env = Substitute.For<IWebAssemblyHostEnvironment>();
env.BaseAddress.Returns(hostBaseAddress);
sut.AddSingleton(env);

var sp = sut.BuildServiceProvider();
var httpClientFactory = sp.GetService<IHttpClientFactory>();
var httpClient = httpClientFactory?.CreateClient("clientName");
httpClient.ShouldNotBeNull();
httpClient.BaseAddress.ShouldNotBeNull();
httpClient.BaseAddress.AbsoluteUri.ShouldBe(expectedBaseAddress);
}
}

0 comments on commit 4af5a33

Please sign in to comment.