2008-09-30

Unit testing security

Following on from my previous post about using(Tricks) here is an example which makes writing test cases easier rather than just for making your code nicely formatted. Take a look at the following test which ensures Article.Publish sets the PublishedDate correctly:

[TestMethod]
public void PublishedDateIsSet()
{
  //Create the EcoSpace, set its PMapper to a memory mapper
  var ecoSpace = TestHelper.EcoSpace.Create();

  //Creat an article
  var article = new Article(ecoSpace);

  //Create our Rhino Mocks repository
  var mocks = new MockRepository();

  //Mock the date/time to give us a predictable value
  var mockDateTimeService = mocks.StrictMock<IDateTimeService>();
  ecoSpace.RegisterEcoService(typeof(IDateTimeService), mockDateTimeService);

  //Get a date/time to return from the mock DateTimeService
  var now = DateTime.Now;
  using (mocks.Record())
  {
    //When asked, return the value we recorded earlier
    Expect.Call(mockDateTimeService.Now).Return(now);
  }

  //Check mockDateTimeService.Now is called from Article.Publish
  using (mocks.Playback())
  {
    article.Publish();
  }

  //Check the date/time from IDateTimeService is stored in PublishedDate
  Assert.AreEqual(now, article.PublishedDate);
}


The method it is testing

public void Publish()
{
  IEcoServiceProvider serviceProvider = AsIObject().ServiceProvider;
  var dateTimeService = serviceProvider.GetEcoService<IDateTimeService>();
  PublishedDate = dateTimeService.Now;
}


Now at some point in the future you decide to enforce some security within your business objects. You decide that only certain people can publish your article, such as the author or an administrator. This will now break every test you have which assumes article.Publish will just work.


public void Publish()
{
  IEcoServiceProvider serviceProvider = AsIObject().ServiceProvider;

  var currentUserService = serviceProvider.GetEcoService<ICurrentUserService>();
  var currentUser = currentUserService.CurrentUser;
  if (currentUser != this.Author && !currentUser.HasRole<SystemAdministratorRole>())
    throw new SecurityException("Cannot publish this article");

  var dateTimeService = serviceProvider.GetEcoService<IDateTimeService>();
  PublishedDate = dateTimeService.Now;
}


This would dramatically complicated any test which relied on having a published article. Each time you would additionally have to:
01: Create a user.
02: Mock ICurrentUserService to return that user.
03: Ensure the article.Author is set to that user, or the user owns a SystemAdministratorRole.

If you have 20 tests requiring a published article this is going to cause you a lot of work! The first mistake here is that the article is testing for permissions; permission granting should be a service. I would separate the service out like so...

public interface IPermissionService
{
  bool MayPublishArticle(Article article);
}


Your EcoSpace would have a class implementing this interface and return the appropriate result, the EcoSpace would register this default service.

public class PermissionService : IPermissionService
{
  IEcoServiceProvider ServiceProvider;
  
  public PermissionService(IEcoServiceProvider serviceProvider)
  {
    ServiceProvider = serviceProvider;
  }

  public bool MayPublishArticle(Article article)
  {
    var currentUserService = serviceProvider.GetEcoService<ICurrentUserService>();
    var currentUser = currentUserService.CurrentUser;
    return currentUser == article.Author
      || currentUser.HasRole<SystemAdministratorRole>());
  }
}


In the EcoSpace:
public override bool Active
{
  get { return base.Active; }
  set
  {
    if (value && !Active)
      RegisterDefaultServices();
    base.Active = value;
  }
}

private void RegisterDefaultServices()
{
  RegisterEcoService(typeof(IPermissionService), new PermissionService(this));
  RegisterEcoService(typeof(ICurrentUserService), new CurrentUserService());
  RegisterEcoService(typeof(IDateTimeService), new DateTimeService());
}


Now Article.Publish looks like this

public void Publish()
{
  IEcoServiceProvider serviceProvider = AsIObject().ServiceProvider;
  var permissionService = serviceProvider.GetEcoService<IPermissionService>();

  if (!permissionService.MayPublishArticle(this))
    throw new SecurityException("Cannot publish this article");

  var dateTimeService = serviceProvider.GetEcoService<IDateTimeService>();
  PublishedDate = dateTimeService.Now;
}


But how does this help? The first advtange is that we have separated the PermissionService so that we can test it in isolation, but we would still need to mock the IPermissionService wouldn't we? Yes we would! But how does this look?

using (TestHelper.Permissions.PermitAll(ecoSpace))
{
  article.Publish();
}


Much more simple eh? To achieve this I have a static class named Permissions in my test project

public static class Permissions
{
  private class TestPermissionService : IPermissionService
  {
    (simple code omitted)
    Accept a boolean in the constructor, and return
    it for every method call.
  }

  public static IDisposable Allow(MyEcoSpaceType ecoSpace)
  {
    var originalService = ecoSpace.GetEcoService<IPermissionService>();
    var newService = new TestPermissionService(true);
    ecoSpace.RegisterEcoService(typeof(IPermissionService), newService);
    return DisposableAction(
      () => ecoSpace.RegisterEcoService(typeof(IPermissionService), originalService)
    );
  }
}


All this does is to record the current IPermissionService, register a new one which always returns True/False (depending on what we pass to its constructor), and then return an instance of DisposableAction. To this instance we pass an Action which re-registers the original service. The action is called when IDisposable.Dispose is called:

public class DisposableAction : IDisposable
{
  Action Action;
  public DisposableAction(Action action)
  {
    Action = action
  }

  void IDisposable.Dispose()
  {
    Action();
  }
}



So the following line

using (TestHelper.Permissions.PermitAll(ecoSpace))
{
  article.Publish();
}


will
01: Record the original IPermissionService.
02: Register a new one which always returns True.
03: Execute the code within the Using { } block.
04: IDisposable.Dispose will be called on my DisposableAction.
05: The previous service will be restored.

2 comments:

Joe Hendricks said...

Really nice approach!
- Joe

JornWildt said...

Interesting use of IDisposable - thanks for sharing.