在Servicestack Ormlite中使用MVC进行测试/模拟AuthUserSession

huangapple go评论113阅读模式
英文:

Testing/Mocking AuthUserSession in Servicestack Ormlite with MVC

问题

我有一个现有的MCV应用程序,使用ServiceStack Ormlite。我正在添加一些控制器测试,并且可以成功地模拟注入的类,但是我们在使用一些控制器来检查会话的权限和其他属性时遇到了问题。

所有的控制器都继承自基础控制器_Controller,该控制器具有以下属性:

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

MyAuthUserSession扩展了AuthUserSession类,并具有一些额外的方法和数据成员。

在控制器中,我们使用这个属性来检查特定情况下的权限:

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

然而,在运行单元测试时,调用AuthUserSession总是抛出空引用异常。

我尝试通过包含一个测试AppHost来填充会话:

  1. public class TestAppHost : AppHostBase
  2. {
  3. public TestAppHost() : base("Test Service", typeof(LocationsService).Assembly) { }
  4. public override void Configure(Container container)
  5. {
  6. Plugins.Add(new AuthFeature(() => new MyAuthUserSession(), new IAuthProvider[] { new BasicAuthProvider() })
  7. {
  8. IncludeAssignRoleServices = false,
  9. IncludeAuthMetadataProvider = false,
  10. IncludeRegistrationService = false,
  11. GenerateNewSessionCookiesOnAuthentication = false,
  12. DeleteSessionCookiesOnLogout = true,
  13. AllowGetAuthenticateRequests = req => true
  14. });
  15. }
  16. }

这个AppHost接受与主应用程序相同的参数,但是AuthUserSession从未被填充。我尝试手动设置它:

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

手动设置会话不起作用-我怀疑保存的会话与控制器的SessionAs<MyAuthUserSession>()没有关联。

这看起来更像是一个集成测试而不是单元测试,但基本上我只需要基础控制器上的AuthUserSession属性返回一个用于测试的会话,无论是手动设置还是模拟设置。由于这是一个遗留应用程序,改变控制器上的AuthUserSession属性是不切实际的(可能有1000个引用,所以改变它将需要大量的重构)。由于它是一个常规属性,我不能用NSubstitute替换响应(例如MyController.AuthUserSession.Returns(new MyAuthUserSession()))。

有没有办法在Servicestack中设置或替换SessionAs<MyAuthUserSession>()方法或控制器上的AuthUserSession属性?

英文:

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 =&gt; SessionAs&lt;MyAuthUserSession&gt;();

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:

  1. public class TestAppHost : AppHostBase
  2. {
  3. public TestAppHost() : base(&quot;Test Service&quot;, typeof(LocationsService).Assembly) { }
  4. public override void Configure(Container container)
  5. {
  6. Plugins.Add(new AuthFeature(() =&gt; new MyAuthUserSession(), new IAuthProvider[] { new BasicAuthProvider() })
  7. {
  8. IncludeAssignRoleServices = false,
  9. IncludeAuthMetadataProvider = false,
  10. IncludeRegistrationService = false,
  11. GenerateNewSessionCookiesOnAuthentication = false,
  12. DeleteSessionCookiesOnLogout = true,
  13. AllowGetAuthenticateRequests = req =&gt; true
  14. });
  15. }
  16. }

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

  1. var authService = appHost.Container.Resolve&lt;AuthenticateService&gt;();
  2. var session = new MyAuthUserSession();
  3. authService.SaveSessionAsync(session).Wait();

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

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&lt;MyAuthUserSession&gt;() method in Servicestack or the AuthUserSession property on a controller?

EDIT:

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

  1. TestMode = true;
  2. container.Register&lt;IAuthSession&gt;(new MyAuthUserSession { UserAuthId = &quot;1&quot; });

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:

  1. public class WebApiService : Service
  2. {
  3. public AuthUserSession AuthUserSession =&gt; SessionAs&lt;AuthUserSession&gt;();
  4. ...
  5. }

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

On the base controller in the MVC application I have:

  1. public abstract class _Controller : ServiceStackController
  2. {
  3. public AuthUserSession =&gt; SessionAs&lt;MyAuthUserSession&gt;();
  4. ...
  5. }

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:

  1. at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
  2. at ServiceStack.Host.NetCore.NetCoreRequest.TryResolve[T]()
  3. at ServiceStack.ServiceStackHost.GetCacheClient(IRequest req)
  4. at ServiceStack.ServiceStackProvider.get_Cache()
  5. at ServiceStack.ServiceStackProvider.SessionAs[TUserSession]()
  6. at ServiceStack.Mvc.ServiceStackController.SessionAs[TUserSession]()
  7. at WebApplication.Controllers._Controller.get_AuthUserSession() in _Controller.cs:line 93
  8. at WebApplication.Controllers.MyController.Common(PageAction action, MyDataModel model) in MyController.cs:line 288
  9. at WebApplication.Controllers.MyController.Show(Int64 id) in MyController.cs:line 157
  10. 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:

  1. public class TestAppHost : BasicAppHost
  2. {
  3. public TestAppHost() : base(typeof(LocationsService).Assembly) { }
  4. public override void Configure(Container container)
  5. {
  6. container.Adapter = new UnityIocAdapter(Repository.Container.Instance);
  7. Plugins.Add(new AuthFeature(() =&gt; new AuthUserSession(),
  8. new IAuthProvider[] {
  9. new BasicAuthProvider(), //Sign-in with HTTP Basic Auth
  10. new CredentialsAuthProvider(), //HTML Form post of UserName/Password credentials
  11. }));
  12. container.Register&lt;ICacheClient&gt;(new MemoryCacheClient());
  13. var userRepo = new InMemoryAuthRepository();
  14. container.Register&lt;IAuthRepository&gt;(userRepo);
  15. container.Register&lt;IAuthSession&gt;(new AuthUserSession());
  16. }
  17. }

Using an empty controller with:

  1. public class EmptyController: ServiceStackController
  2. {
  3. public EmptyController() {}
  4. public AuthUserSession AuthUserSession =&gt; SessionAs&lt;AuthUserSession&gt;();
  5. }

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

  1. _appHost = new TestAppHost();
  2. _appHost.Init();
  3. var contr = _appHost.Resolve&lt;EmptyController&gt;();
  4. 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.

答案1

得分: 1

首先,请查看ServiceStack测试文档,了解如何使用BasicAppHost创建一个测试AppHost,并配置Config.TestMode=true,这样可以在IOC中注册你的会话:

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

或者,你可以通过在IRequest.Items字典中设置会话来为请求填充会话,例如:

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

我已经更新了你的ServicestackAuthExample仓库中的单元测试示例,以确保通过测试:

  1. // 用于测试的空TempData
  2. public class TestDataDictionary : Dictionary<string, object>, ITempDataDictionary
  3. {
  4. public void Load() {}
  5. public void Save() {}
  6. public void Keep() {}
  7. public void Keep(string key) {}
  8. public object? Peek(string key) => null;
  9. }
  10. public class Tests
  11. {
  12. private static TestAppHost _appHost;
  13. [SetUp]
  14. public void Setup()
  15. {
  16. _appHost = new TestAppHost();
  17. _appHost.Init();
  18. }
  19. [Test]
  20. public void TestPrivacyPage()
  21. {
  22. // 控制器不是从ServiceStack IOC解析出来的
  23. var sut = new HomeController
  24. {
  25. // 返回View()需要TempData
  26. TempData = new TestDataDictionary()
  27. };
  28. // 需要配置RequestServices来使用IOC
  29. sut.ControllerContext.HttpContext = new DefaultHttpContext {
  30. RequestServices = _appHost.Container
  31. };
  32. sut.ServiceStackRequest.Items[Keywords.Session] = new AuthUserSession();
  33. var result = sut.Privacy();
  34. Assert.IsNotNull(result);
  35. }
  36. }
英文:

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:

  1. container.Register&lt;IAuthSession&gt;(new MyAuthUserSession());

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

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

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

  1. // Empty TempData for testing
  2. public class TestDataDictionary : Dictionary&lt;string,object&gt;, ITempDataDictionary
  3. {
  4. public void Load() {}
  5. public void Save() {}
  6. public void Keep() {}
  7. public void Keep(string key) {}
  8. public object? Peek(string key) =&gt; null;
  9. }
  10. public class Tests
  11. {
  12. private static TestAppHost _appHost;
  13. [SetUp]
  14. public void Setup()
  15. {
  16. _appHost = new TestAppHost();
  17. _appHost.Init();
  18. }
  19. [Test]
  20. public void TestPrivacyPage()
  21. {
  22. // Controllers aren&#39;t resolved from ServiceStack IOC
  23. var sut = new HomeController
  24. {
  25. // Returning View() requires TempData
  26. TempData = new TestDataDictionary()
  27. };
  28. // RequestServices needs to be configured to use an IOC
  29. sut.ControllerContext.HttpContext = new DefaultHttpContext {
  30. RequestServices = _appHost.Container
  31. };
  32. sut.ServiceStackRequest.Items[Keywords.Session] = new AuthUserSession();
  33. var result = sut.Privacy();
  34. Assert.IsNotNull(result);
  35. }
  36. }

huangapple
  • 本文由 发表于 2023年8月9日 06:54:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/76863622.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定