Design, Test and Code like it’s heaven on Earth - Part 21

Published on 11/20/2019

Following the really technical R&D part, we need to clean up the house a bit. There’s a lot of new code, but no new tests. We also need to clean up the whole site a bit more, to make it presentable.

What of the auth can we test?

There are two bits that we need to address: authentication and authorization. Authentication is mostly dealt with in the Blazor page code, so that bit might not be testable (I haven’t investigated yet how to run unit tests against that code). The authorization is a handler that can easily be instantiated, so we can look at testing that very effectively.

Test later vs test first

So how does it work when you write your unit tests after the fact? And incidentally, a lot of developers only write their tests afterward. Often it’s really easy to see that tests were written afterward when doing a code review. The giveaway is “crafted tests” to conform to the current implementation, rather than crafted code to pass well-designed test cases and edge-cases. Anyway, there are valid reasons to write tests afterward. One is when there are no tests (perhaps you inherited an older or existing code base), and you want to make a change. You want to capture the existing functionality around as much of the flow that the change will affect so that you can be sure where your change’s consequences are reaching. Often you cannot capture all of it because you simply don’t know, and the few unit-tests that you do write before your change can hint at where else you need to place unit tests. Another scenario is when you have a bug to fix. Write a negative test for the affected code to reproduce the bug in a unit test. Proceed to confirm that the negative test “passes” and confirms the bug is there. Then proceed to fix the bug, and make sure that the test now fails. Finally, you proceed to change it into a positive one, making sure you cover the use case that resulted in the bug. This is the Red/Green/Blue flow slightly changed.

Here we are more in the first scenario than in the second, of course. We capture all the functionality of the authorization handler in a unit test.

[TestClass]
public class helloserveAuthorizationTests
{
    [DataTestMethod]
    [DataRow("<hardcoded email address>", true, false)]
    [DataRow("<a different email address>", false, true)]
    public async Task HandleAsync_Verify(string email, bool succeeded, bool failed)
    {
        //arrange
        helloserveAuthorizationHandler handler = new helloserveAuthorizationHandler();
        IEnumerable<ClaimsIdentity> identities = new List<ClaimsIdentity>()
        {
            new ClaimsIdentity(new List<Claim>()
            {
                new Claim(ClaimTypes.Email, email)
            })
        };
        ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(identities);
        IEnumerable<IAuthorizationRequirement> requirements = new List<IAuthorizationRequirement>()
        {
            new helloserveAuthorizationRequirement()
        };
        AuthorizationHandlerContext context = new AuthorizationHandlerContext(requirements, claimsPrincipal, null);

        //act
        await handler.HandleAsync(context);

        //assert
        Assert.AreEqual(succeeded, context.HasSucceeded);
        Assert.AreEqual(failed, context.HasFailed);
    }
}

I’m not making changes to the code of course, so these two tests are both positive. The next bit of code that we added in the previous part is the customer controller that initiates the challenge and accepts the callback. Although we can manually instantiate this controller and call these methods, there are some under-the-hood web stack things that unit tests cannot do, and one of these is it cannot initialize an actual HttpContext for the controller. This means these controller actions are not unit-testable, but it might be possible through (integration testing)[https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.0]. I’m not going to look at that for this project, however. The only other things that were added were either Blazor pages or javascript related.

Cleaning up the Front End

Now I’m going to look at moving that sign-in button. Typically users won’t be signed in to my site - it’s a basic blog site of course. However I need to be signed in of course, or whoever I want to invite as a contributor. I’m going to move the sign-in button into its own layout, and make my administrative pages use this new layout. The new LoginLayout.razor file looks like this:

@inherits LayoutComponentBase
@layout MainLayout
@using Microsoft.JSInterop
@using helloserve.com.Auth
@inject IJSRuntime JSRuntime

@if (userState == null) // Retrieving the login state
{
    <text>Loading...</text>
}
else
{
    <AuthorizeView>
        <Authorized>
            @Body
        </Authorized>
        <NotAuthorized>
            <div class="main">
                <h2>You're signed out</h2>
                <p>To continue, please sign in.</p>
                <button class="btn btn-danger" onclick="@SignIn">Sign in</button>
            </div>
        </NotAuthorized>
    </AuthorizeView>
}

@functions {
    [CascadingParameter] Task<AuthenticationState> authStateTask { get; set; }
    UserState userState;

    protected override async Task OnInitAsync()
    {
        AuthenticationState authState = await authStateTask;
        userState = UserState.LoggedOutState;

        if (authState.User != null && authState.User.Identity != null)
        {
            userState.DisplayName = authState.User.Identity.Name;
            userState.PictureUrl = authState.User.FindFirst("picture")?.Value;
            userState.IsLoggedIn = authState.User.Identity.IsAuthenticated;
        }
    }

    public async Task SignIn()
    {
        await JSRuntime.InvokeAsync<object>("openLoginPopup", DotNetObjectRef.Create(this));
    }

    [JSInvokable]
    public void OnSignInStateChanged(UserState userState)
    {
        this.userState = userState;
        StateHasChanged();
    }
}

This is more along the lines of the Blazing Pizza app design which has the ForceLoginLayout.razor file. It is concerned with boxing the user into a signed-out state, otherwise, it just displays the body of the underlying form. I used this layout in the administrative Blog.razor page that I created before which contains the form. Except now that form doesn’t need the <AuthorizeView> tag on it, as it renders as the body of this page.

You might have noticed that this new layout doesn’t contain the sign out functionality. This remained in the main layout.

@inherits LayoutComponentBase
@using System.Net.Http
@using helloserve.com.Auth
@inject HttpClient HttpClient
@inject IJSRuntime JSRuntime

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <div class="top-row px-4">
        <AuthorizeView>
            <Authorized>
                <span><img src="@userState.PictureUrl" width="32" height="32" /></span>
                <span>&nbsp;</span>
                <span>
                    <button class="btn btn-info" onclick="@SignOut">Sign out</button>
                </span>
            </Authorized>
        </AuthorizeView>
        <a href="https://docs.microsoft.com/en-us/aspnet/" target="_blank" class="ml-md-auto">About</a>
    </div>

    <div class="content px-4">
        @Body
    </div>
</div>

@functions {
    [CascadingParameter] Task<AuthenticationState> authStateTask { get; set; }
    UserState userState;

    protected override async Task OnInitAsync()
    {
        AuthenticationState authState = await authStateTask;
        userState = UserState.LoggedOutState;

        if (authState.User != null && authState.User.Identity != null)
        {
            userState.DisplayName = authState.User.Identity.Name;
            userState.PictureUrl = authState.User.FindFirst("picture")?.Value;
            userState.IsLoggedIn = authState.User.Identity.IsAuthenticated;
        }
    }

    public async Task SignOut()
    {
        // Transition to "loading" state synchronously, then asynchronously update
        userState = null;
        StateHasChanged();

        userState = await HttpClient.PutJsonAsync<UserState>("user/signout", null);
        StateHasChanged();
    }
}

I still show the user’s picture and the Sign-Out button here on the main header bar. Unfortunately, there is some code duplication since both layout files have to have knowledge of the AuthenticationState and set the UserState class.

Authorization Configuration

The last remaining item to take care of is the authorization. The handler I wrote checks against a specific email address to decide whether to allow a logged-in user into these administrative pages. This was the helloserve policy that we set up. I decided that I’m going to drive this through the config. I don’t want to create a table in the database or anything like that. Google takes care of who you are for me, and I just need to keep a vetted list of email addresses (or claims) that I can check against. This is a simple array in the config.

  "Users": [ ],

The contents of this array are simply email-addresses that I want to allow as administrators. In the code, I change the handler slightly to take in the configuration abstraction, and then use it to compare the authenticated email address.

Again, I start by amending my unit test that we wrote earlier. It needs to pass configuration to the handler. We can easily set up the configuration, using a memory collection. It uses the same configuration key as you would use in the user secrets config override file. To match this we adjust the data row values for the success scenario.

[TestClass]
public class helloserveAuthorizationTests
{
    [DataTestMethod]
    [DataRow("[email protected]", true, false)]
    [DataRow("<a different email address>", false, true)]
    public async Task HandleAsync_Verify(string email, bool succeeded, bool failed)
    {
        //arrange
        IConfiguration configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("Users:0", "[email protected]") })
            .Build();
        helloserveAuthorizationHandler handler = new helloserveAuthorizationHandler(configuration);
        // …. omitted for brevity
    }
}

This doesn’t compile, because the constructor for the handler doesn’t accept this parameter. I proceed to make the required changes to the handler.

readonly List<string> users;

public helloserveAuthorizationHandler(IConfiguration configuration)
{
    users = configuration.GetSection("Users").Get<List<string>>();
}

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, helloserveAuthorizationRequirement requirement)
{
    string email = context.User.FindFirst(ClaimTypes.Email)?.Value;
    if (!string.IsNullOrEmpty(email))
    {
        if (!users.Contains(email))
        {
            context.Fail();
        }
        context.Succeed(requirement);
    }

    return Task.CompletedTask;
}

The test passes and the authorization is now driven by configuration. With this completed, I’m happy with the state of the auth.

Dressing it

Now I’m going to start removing front-end elements that I don’t need for my blog site. I clean out all the pages that I don’t want showing, adjust the color scheme and add in a bunch of links to all my other stuff. It looks a lot like my old site, but also not. I’m not going to detail all these small changes in HTML and CSS here.

Conclusion

In this part we wrote a unit test after we wrote the code. And then we had to make additional changes to the unit test when we added the configuration. We took apart the authentication flow by splitting it between different layouts to drive authorization aware pages separately. At this point, I’ve got three remaining items to sort out before I can deploy this rebuild of the site: actually saving a blog edit, media hosting, and data migration. We’re getting close, so lookout for the next few parts.