bUnit-dev / bUnit

bUnit is a testing library for Blazor components that make tests look, feel, and runs like regular unit tests. bUnit makes it easy to render and control a component under test’s life-cycle, pass parameter and inject services into it, trigger event handlers, and verify the rendered markup from the component using a built-in semantic HTML comparer.
https://bunit.dev
MIT License
1.11k stars 104 forks source link

WASM MSAL Configuration Not Correctly Supported #1453

Open SeanC2222 opened 3 months ago

SeanC2222 commented 3 months ago

Describe the bug

In the MSAL backed Blazor WASM authentication packages, there exists an overloaded class the RemoteAuthenticationService.

This service implements the AuthenticationStateProvider and an IRemoteAuthenticationService<>.

The DI for this package abuses this a bit by casting the configured AuthenticationStateProvider to it's alternate interface form.

This doesn't play nicely with BUnit's configuration which simply adds a singleton definition for AuthenticationStateProvider for the Fake that doesn't implement both the interface and abstract class.

Example: Testing this component:

Any component where the host program uses MSAL:

With this test: Requires Package Reference:

    <PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="8.0.3" />
public class TestClass : Bunit.TestContext
{

    [Fact]
    public void Broken()
    {
        var authContext = this.AddTestAuthorization();

        this.Services.AddMsalAuthentication(options => { });

        // Demo to simply show the DI demand of a component
        var throws = this.Services.GetService<IRemoteAuthenticationService<RemoteAuthenticationState>>();
    }
}

Results in this output:

 TestClass.Broken
   Source: TestMe.cs line 9
   Duration: 949 ms

  Message: 
System.InvalidCastException : Unable to cast object of type 'Bunit.TestDoubles.FakeAuthenticationStateProvider' to type 'Microsoft.AspNetCore.Components.WebAssembly.Authentication.IRemoteAuthenticationService`1[Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationState]'.

  Stack Trace: 
<>c__1`1.<AddAuthenticationStateProvider>b__1_0(IServiceProvider sp)
CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
ServiceProviderEngineScope.GetService(Type serviceType)
TestServiceProvider.GetServiceInternal(Type serviceType) line 159
TestServiceProvider.GetService(Type serviceType) line 151
TestServiceProvider.GetService[TService]() line 141
TestClass.Broken() line 15
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Expected behavior:

Not triggering the cast error.

Version info:

Additional context:

We're wrapping the BUnit FakeAuthenticationStateProvider in an extra layer that implements both the AuthenticationStateProvider and the IRemoteAuthenticationService which is working, but it also requires us taking the BUnit implementation out of the DI and sticking ours in based on this little insight. Not ideal.

Not sure what the right answer is for BUnit as BUnit targets all Blazor, and this is a WASM specific problem.

egil commented 3 months ago

Thanks for reporting this. My initial take is that we won't be taking a dependency on an 3rd party package to implement a base type or interface from that.

However, I am open to adding to our fake Auth API in (non-breeding way) to enable your scenario in a way that feels natural.

Do you have any suggestions in this regard?