1

I am using Visual Studio 2015 Enterprise Update 1 and ASP.NET 5 rc1-final to build an endpoint that both issues and consumes JWT tokens as described in detail here.

Now I'm moving on to unit testing and encountering friction when testing certain aspects of AspNet.Security.OpenIdConnect.Server (OIDC/ASOS). Specifically, some ASOS primitives such as LogoutEndpointContext aren't abstract so they aren't easily mocked. Normally I'd just throw a fake at these kinds of types but DNX doesn't appear to support fakes, at least not yet. These types are also sealed so I can't specialize them.

This has forced me to write some brittle reflection code in order to construct these kinds of sealed ASOS types. Here's an example XUnit tests that needs a LogoutEndpointContext so I can test my OpenIdConnectServerProvider event handling (in this case a non-POST logout should throw an exception); note the reflection I'm having to do in order to instantiate a LogoutEndpointContext:

[Fact]
async public Task API_Initialization_Services_AuthenticatedUser_Authentication_LogoutEndpoint_XSRF_Unit()
{
    // Arrange

        Mock<HttpRequest> mockRequest = new Mock<HttpRequest>();
        mockRequest.SetupGet(a => a.Method).Returns(() => "Not Post");
        Mock<HttpContext> mockContext = new Mock<HttpContext>();
        mockContext.SetupGet(a => a.Request).Returns(() => mockRequest.Object);

        OpenIdConnectServerOptions options = new OpenIdConnectServerOptions();

        OpenIdConnectMessage request = new OpenIdConnectMessage();

        // I would prefer not to use reflection
        var ctorInfo = typeof(LogoutEndpointContext).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).Single();

        LogoutEndpointContext context = (LogoutEndpointContext)ctorInfo.Invoke(new object[] {mockContext.Object,options, request});

    // Act

        AuthenticationEvents authenticationEvents = new AuthenticationEvents();

    // Assert

        await Assert.ThrowsAsync<SecurityTokenValidationException>(() => authenticationEvents.LogoutEndpoint(context));
}

Any advice on how to better instantiate/mock/fake/specialize sealed ASOS types like LogoutEndpointContext would be very much appreciated.

Community
  • 1
  • 1
42vogons
  • 683
  • 7
  • 19
  • First, you shouldn't be testing framework code itself, but you do need to test code that uses the framework code. The correct way to do this is to wrap the object in some kind of mockable interface, which you then forward to a contained object within it. An example of this is HttpContextBase/HttpContextWrapper. Unfortunately, you can't really pass these mocks to other framework functions, so that makes this approach less useful in some situations, or at least requires a bit more work. – Erik Funkenbusch Jan 04 '16 at 16:02

1 Answers1

0

When testing middleware that heavily depends on the HTTP context, unit testing is rarely the easiest option. Our recommended approach here is to opt for functional testing by using TestServer to set up an in-memory pipeline:

public class OAuthLogoutEndpointTests {
    [Fact]
    public async Task EnsureThatOnlyPostLogoutRequestsAreValid() {
        // Arrange
        var server = CreateAuthorizationServer();
        var client = server.CreateClient();

        var request = new HttpRequestMessage(HttpMethod.Get, "/connect/logout");

        // Act
        var response = await client.SendAsync(request);

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    private static TestServer CreateAuthorizationServer(Action<OpenIdConnectServerOptions> configuration = null) {
        var builder = new WebHostBuilder();

        builder.UseEnvironment("Testing");

        builder.ConfigureServices(services => {
            services.AddAuthentication();
            services.AddCaching();
            services.AddLogging();
        });

        builder.Configure(app => {
            // Add a new OpenID Connect server instance.
            app.UseOpenIdConnectServer(options => {
                // Use your own provider:
                // options.Provider = new AuthenticationEvents();

                options.Provider = new OpenIdConnectServerProvider {
                    OnValidateLogoutRequest = context => {
                        // Reject non-POST logout requests.
                        if (!string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) {
                            context.Reject(
                                error: OpenIdConnectConstants.Errors.InvalidRequest,
                                description: "Only POST requests are supported.");
                        }

                        return Task.FromResult(0);
                    }
                };

                // Run the configuration delegate
                // registered by the unit tests.
                configuration?.Invoke(options);
            });
        });

        return new TestServer(builder);
    }
}

(you can find more tests here: https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions/blob/master/test/AspNet.Security.OAuth.Introspection.Tests/OAuthIntrospectionMiddlewareTests.cs)

Edit: the rest of this answer no longer applies, as the context classes have been unsealed in ASOS beta5.


In ASOS, all the context classes have been sealed and their constructor marked as internal to prevent developers from instantiating them and manually calling the provider in production code. That said, if you strongly think we shouldn't do that, feel free to open a new ticket on the GitHub repository.

A last remark: rejecting GET requests is probably not a good idea if you want your OIDC server to be spec-compliant, as it's the only verb explicitly mentioned by the specifications.

Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131