unit-testingormlite-servicestack

Testing/Mocking AuthUserSession in Servicestack Ormlite with MVC


I have an existing MCV application that uses ServiceStack Ormlite. I am adding some controller tests and can mock injected classes without issue, however we are having problems with the ServiceStack AuthUserSession which is used by some controllers to check permissions and other properties of the session.

All the controllers inherit from the base controller _Controller which has the following property:

public MyAuthUserSession AuthUserSession => SessionAs<MyAuthUserSession>();

MyAuthUserSession extends the AuthUserSession class and has some extra methods and data members.

In the controllers we use this property to check permissions for particular cases:

model.CanEdit = AuthUserSession.HasEntityPermission(PermissionType.Update, PermissionEntity.MyEntity);

However when running as a unit test, calling AuthUserSession always throws a null reference exception.

I've attempted to populate the session by including a test AppHost:

public class TestAppHost : AppHostBase
{
    public TestAppHost() : base("Test Service", typeof(LocationsService).Assembly) { }

    public override void Configure(Container container)
    {
        Plugins.Add(new AuthFeature(() => new MyAuthUserSession(), new IAuthProvider[] { new BasicAuthProvider() })
        {
            IncludeAssignRoleServices = false,
            IncludeAuthMetadataProvider = false,
            IncludeRegistrationService = false,
            GenerateNewSessionCookiesOnAuthentication = false,
            DeleteSessionCookiesOnLogout = true,
            AllowGetAuthenticateRequests = req => true
        });
    }
}

Which takes the same arguments as the main application, however the AuthUserSession is never populated. I've tried setting it manually:

var authService = appHost.Container.Resolve<AuthenticateService>();
var session = new MyAuthUserSession();
authService.SaveSessionAsync(session).Wait();

Manually setting a session doesn't work - I suspect the saved session is not associated with the controller's SessionAs<MyAuthUserSession>().

This is starting to look closer to an integration test than a unit test, but essentially I just need the AuthUserSession property on the base controller to return a session for testing, either by setting it manually or by mocking it. Since this is a legacy application changing the AuthUserSession property on the controller is not practical (there are perhaps 1000 references to it, so changing it will require a huge amount of refactoring). Since it is a regular property I cannot substitute the response with NSubstitute (eg. MyController.AuthUserSession.Returns(new MyAuthUserSession())

Is there any way of setting or substituting the SessionAs<MyAuthUserSession>() method in Servicestack or the AuthUserSession property on a controller?

EDIT:

I've added the following lines to my AppHost with no changes:

TestMode = true;
container.Register<IAuthSession>(new MyAuthUserSession { UserAuthId = "1" });

I also attempted to add the session to the request items via the controller's ControllerContext.HttpContext with no success.

Since most examples I can find are for testing APIs I had a look at writing tests for our Servicestack API, and for the API registering the IAuthSession as above is all that's needed, it just works. Doing the same thing in the MVC application doesn't seem to work. I'm struggling to find the difference between the two.

The service has the session defined as:

public class WebApiService : Service
{
    public AuthUserSession AuthUserSession => SessionAs<AuthUserSession>();

...
}

And any services inheriting from WebApiService have access to the AuthUserSession.

On the base controller in the MVC application I have:

public abstract class _Controller : ServiceStackController
{
    public AuthUserSession => SessionAs<MyAuthUserSession>();
    ...
}

The AppHosts for both the API and MVC controller tests are identical and both register the IAuthSession session the same way. There's got to be something very simple difference between the ServiceStack.Mvc.ServiceStackController and the ServiceStack.Service or how I'm using them.

EDIT: Including stack trace:

at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
at ServiceStack.Host.NetCore.NetCoreRequest.TryResolve[T]()
at ServiceStack.ServiceStackHost.GetCacheClient(IRequest req)
at ServiceStack.ServiceStackProvider.get_Cache()
at ServiceStack.ServiceStackProvider.SessionAs[TUserSession]()
at ServiceStack.Mvc.ServiceStackController.SessionAs[TUserSession]()
at WebApplication.Controllers._Controller.get_AuthUserSession() in _Controller.cs:line 93
at WebApplication.Controllers.MyController.Common(PageAction action, MyDataModel model) in MyController.cs:line 288
at WebApplication.Controllers.MyController.Show(Int64 id) in MyController.cs:line 157
at WebApplication.UnitTests.Controllers.MyControllerTests.Show_WithReadPrivileges_ReturnsExpectedSettingValue() in MyControllerTests.cs:line 115

Where _Controller is the base controller which has the SessionAs call, and MyController is the controller that inherits the base.

EDIT:

I've stripped everything back as far as I can, and still getting a null on the session. My AppHost is taken from the 'Basic Configuration' in the docs, plus a UnityIocAdapter for my controller resolution and registering the IAuthSession as suggested:

public class TestAppHost : BasicAppHost
{
    public TestAppHost() : base(typeof(LocationsService).Assembly) { }

    public override void Configure(Container container)
    {
        container.Adapter = new UnityIocAdapter(Repository.Container.Instance);
        Plugins.Add(new AuthFeature(() => new AuthUserSession(),
    new IAuthProvider[] {
        new BasicAuthProvider(),       //Sign-in with HTTP Basic Auth
        new CredentialsAuthProvider(), //HTML Form post of UserName/Password credentials
    }));

        container.Register<ICacheClient>(new MemoryCacheClient());
        var userRepo = new InMemoryAuthRepository();
        container.Register<IAuthRepository>(userRepo);
        container.Register<IAuthSession>(new AuthUserSession());
    }
}

Using an empty controller with:

public class EmptyController: ServiceStackController
{
    public EmptyController() {}

    public AuthUserSession AuthUserSession => SessionAs<AuthUserSession>();
}

Calling the following still results in the null exception with the stack trace above:

_appHost = new TestAppHost();
_appHost.Init();
var contr = _appHost.Resolve<EmptyController>();
var session = contr.AuthUserSession;

Removing the UnityIocAdapter and creating a new controller with new EmptyController() does the same, but that's far removed from how the controllers are resolved in the application, but I don't know of another way to resolve a controller without more complication.


Solution

  • First have a look at ServiceStack Testing Docs for using a BasicAppHost to create a test AppHost which is configured with Config.TestMode=true which allows you to register your Session in the IOC:

    container.Register<IAuthSession>(new MyAuthUserSession());
    

    Alternatively you can populate a session for a request by setting it in IRequest.Items dictionary, e.g:

    service.Request ??= new BasicRequest();
    service.Request.Items[Keywords.Session] = new MyAuthUserSession();
    

    I've updated the Unit Test example in your ServicestackAuthExample repository so it passes:

    // Empty TempData for testing
    public class TestDataDictionary : Dictionary<string,object>, ITempDataDictionary
    {
        public void Load() {}
        public void Save() {}
        public void Keep() {}
        public void Keep(string key) {}
        public object? Peek(string key) => null;
    }
    
    public class Tests
    {
        private static TestAppHost _appHost;
    
        [SetUp]
        public void Setup()
        {
            _appHost = new TestAppHost();
            _appHost.Init();
        }
    
        [Test]
        public void TestPrivacyPage()
        {
            // Controllers aren't resolved from ServiceStack IOC
            var sut = new HomeController
            {
                // Returning View() requires TempData
                TempData = new TestDataDictionary()
            };
            // RequestServices needs to be configured to use an IOC
            sut.ControllerContext.HttpContext = new DefaultHttpContext {
                RequestServices = _appHost.Container
            };
            sut.ServiceStackRequest.Items[Keywords.Session] = new AuthUserSession();
    
            var result = sut.Privacy();
            Assert.IsNotNull(result);
        }
    }