英文:
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来填充会话:
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
});
}
}
这个AppHost接受与主应用程序相同的参数,但是AuthUserSession从未被填充。我尝试手动设置它:
var authService = appHost.Container.Resolve<AuthenticateService>();
var session = new MyAuthUserSession();
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 => 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.
答案1
得分: 1
首先,请查看ServiceStack测试文档,了解如何使用BasicAppHost
创建一个测试AppHost,并配置Config.TestMode=true
,这样可以在IOC中注册你的会话:
container.Register<IAuthSession>(new MyAuthUserSession());
或者,你可以通过在IRequest.Items
字典中设置会话来为请求填充会话,例如:
service.Request ??= new BasicRequest();
service.Request.Items[Keywords.Session] = new MyAuthUserSession();
我已经更新了你的ServicestackAuthExample仓库中的单元测试示例,以确保通过测试:
// 用于测试的空TempData
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()
{
// 控制器不是从ServiceStack IOC解析出来的
var sut = new HomeController
{
// 返回View()需要TempData
TempData = new TestDataDictionary()
};
// 需要配置RequestServices来使用IOC
sut.ControllerContext.HttpContext = new DefaultHttpContext {
RequestServices = _appHost.Container
};
sut.ServiceStackRequest.Items[Keywords.Session] = new AuthUserSession();
var result = sut.Privacy();
Assert.IsNotNull(result);
}
}
英文:
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);
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论