I'm building a multi-tenant website, and so I need to overload UserManager.CreateAsync() to accept an additional parameter of type City. In other words, each User belongs to a City. I've configured extra properties on the User model accordingly.
The trouble is that I can't get Moq to raise the callback for the overloaded mocked method.
Here's my mock setup:
Protected Function GetUserManagerMock(Of TUser As Db.User)(Users As List(Of TUser), ExpectedResult As IdentityResult) As Mock(Of UserManager)
Dim oCreateSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))
Dim oDeleteSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))
Dim oUpdateSetup As Expression(Of Func(Of UserManager, Task(Of IdentityResult)))
Dim oManagerMock As Mock(Of UserManager)
Dim oStoreMock As Mock(Of IUserStore(Of TUser))
Dim oCallback As Action(Of TUser, String, Db.City)
Dim oManager As UserManager
Dim oResult As IdentityResult
oStoreMock = New Mock(Of IUserStore(Of TUser))
oManagerMock = New Mock(Of UserManager)(oStoreMock.Object)
oManager = oManagerMock.Object
oCallback = Sub(User, Password, City)
oResult = oManager.PasswordValidator.ValidateAsync(Password).Result
If oResult Is IdentityResult.Success Then
User.PasswordHash = oManager.PasswordHasher.HashPassword(Password)
Users.Add(User)
User.CityId = City.Id
User.City = City
City.Users.Add(User)
End If
End Sub
oCreateSetup = Function(Manager) Manager.CreateAsync(It.IsAny(Of TUser), It.IsAny(Of String), It.IsAny(Of Db.City))
oDeleteSetup = Function(Manager) Manager.DeleteAsync(It.IsAny(Of TUser))
oUpdateSetup = Function(Manager) Manager.UpdateAsync(It.IsAny(Of TUser))
oManagerMock.Setup(oCreateSetup).ReturnsAsync(ExpectedResult).Callback(oCallback)
oManagerMock.Setup(oDeleteSetup).ReturnsAsync(ExpectedResult)
oManagerMock.Setup(oUpdateSetup).ReturnsAsync(ExpectedResult)
Return oManagerMock
End Function
Here's the overloaded UserManager method:
Public Overridable Overloads Async Function CreateAsync(User As Db.User, Password As String, City As Db.City) As Task(Of IdentityResult)
Dim oResult As IdentityResult
Password.ThrowIfNothing(NameOf(Password))
User.ThrowIfNothing(NameOf(User))
City.ThrowIfNothing(NameOf(City))
User.CityId = City.Id
User.City = City
oResult = Await MyBase.CreateAsync(User, Password)
If oResult.Succeeded Then
City.Users.RemoveAll(Function(U) U.Id = User.Id)
City.Users.Add(User)
Else
User.CityId = Nothing
User.City = Nothing
End If
Return oResult
End Function
Here's the controller method under test:
<HttpPost>
<ActionName("Create")>
<AllowAnonymous>
<ValidateAntiForgeryToken>
Public Async Function CreateAsync(Model As Welcome) As Task(Of ActionResult)
Dim oIdentity As IdentityResult
Dim oAction As ActionResult
Dim oUser As Db.User
If Me.ModelState.IsValid Then
If Model.City.IsNothing Then
Model.City = Me.TempData.Peek(NameOf(Db.City))
oAction = Me.View("One", Model)
Else
oUser = New Db.User With {
.FirstName = Model.FirstName,
.LastName = Model.LastName,
.UserName = Model.Username,
.CityId = Model.City.Id,
.City = Model.City
}
oIdentity = Await Me.UserManager.CreateAsync(oUser, Model.Password, Model.City)
If oIdentity.Succeeded Then
oAction = Me.View("Two", Model)
Else
oAction = Me.View("Three", Model)
End If
End If
Else
oAction = Me.View("Four", Model)
End If
Return oAction
End Function
...and here's the test:
<Fact>
Public Async Function CreateUserSucceeds() As Task
Dim oUserManager As UserManager
Dim oController As UsersController
Dim oViewModel As Welcome
Dim oResult As ViewResult
Dim oCity As Db.City
Me.Users.Clear()
Me.Cities.ForEach(Sub(City) City.Users.Clear())
oCity = Me.Cities.First
oUserManager = Me.UserManagerMock(Me.Users, IdentityResult.Success).Object
oController = New UsersController(Me.Worker, Nothing, oUserManager, Nothing)
oViewModel = New Welcome With {.City = oCity, .FirstName = "User", .LastName = "Name", .Password = "P@ssw0rd!"}
oResult = TryCast(Await oController.CreateAsync(oViewModel), ViewResult)
Assert.True(Me.Users.Count = 1)
Assert.True(Me.Users.First.City.IsNotNothing)
Assert.True(oCity.Code = Me.Users.First.City.Code)
End Function
Execution skips the overloaded UserManager.CreateAsync() method as expected, but the callback Action never runs. This line of code from the controller method seems to be skipped entirely:
oIdentity = Await Me.UserManager.CreateAsync(oUser, Model.Password, Model.City)
Thus oIdentity is Nothing (null in C#), no matter what I do.
I tried creating the mock with CallBase turned on, like so:
oManagerMock = New Mock(Of UserManager)(oStoreMock.Object) With {.CallBase = True}
...but that resulted in the overloaded method itself being run, which of course won't do. I'm running unit tests, not integration tests.
Can anyone spot why this callback isn't being called?
--EDIT 1--
I've since discovered that the callback is no longer raised for any method that I mock—not just the overloaded method. This is exceedingly odd, as it was working in the past.
I've determined to restore the code from backup and return it to its working state. I'll have more information soon.
--EDIT 2--
The problem remains unsolved, but I've gotten a bit closer.
The callback is raised successfully when I cast the UserManager for the mock to the Identity framework class (i.e. UserManager(Of TUser)). However, when I cast it to my subclass, in order to get the overloaded method, it fails.
This is why I was initially under the impression that only the overloaded method was causing the failure—because I had to change the casting in order to be able to mock that method. The casting change had caused the problem, not the overload.
I've examined the source code for the Identity framework, and I'm not spotting anything that's different in concept from mine.
Here's my full UserManager subclass:
Imports System
Imports System.Threading.Tasks
Imports Intexx
Imports Website.Models
Imports Microsoft.AspNet.Identity
Imports Microsoft.AspNet.Identity.EntityFramework
Imports Microsoft.AspNet.Identity.Owin
Imports Microsoft.Owin
Imports Microsoft.Owin.Security.DataProtection
Public Class UserManager
Inherits UserManager(Of Db.User)
Public Sub New(Store As IUserStore(Of Db.User))
MyBase.New(Store)
Me.PasswordValidator = New PasswordValidator
Me.UserValidator = New UserValidator(Me)
End Sub
Public Shared Function Create(Options As IdentityFactoryOptions(Of UserManager), Context As IOwinContext) As UserManager
Dim oPhoneProvider As PhoneNumberTokenProvider(Of Db.User)
Dim oEmailProvider As EmailTokenProvider(Of Db.User)
Dim oDataProtector As IDataProtector
Dim oDataProvider As IDataProtectionProvider
Dim oUserStore As IUserStore(Of Db.User)
Dim oManager As UserManager
Dim oContext As Db.Context
oContext = Context.Get(Of Db.Context)
oUserStore = New UserStore(Of Db.User)(oContext)
oManager = New UserManager(oUserStore) With {
.MaxFailedAccessAttemptsBeforeLockout = 5,
.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5),
.UserLockoutEnabledByDefault = True
}
' Create the two-factor authentication providers
oPhoneProvider = New PhoneNumberTokenProvider(Of Db.User) With {.MessageFormat = "Your security code is {0}"}
oEmailProvider = New EmailTokenProvider(Of Db.User) With {.Subject = "Security Code", .BodyFormat = "Your security code is {0}"}
' Register the two-factor authentication providers. This application
' uses Phone and Email as a step of receiving a code for verifying
' the user. You can write your own provider and plug it in here.
oManager.RegisterTwoFactorProvider("Phone Code", oPhoneProvider)
oManager.RegisterTwoFactorProvider("Email Code", oEmailProvider)
oDataProvider = Options.DataProtectionProvider
If oDataProvider.IsNotNothing Then
oDataProtector = oDataProvider.Create("ASP.NET Identity")
oManager.UserTokenProvider = New DataProtectorTokenProvider(Of Db.User)(oDataProtector)
End If
oManager.EmailService = New EmailService
oManager.SmsService = New SmsService
Return oManager
End Function
Public Overridable Overloads Async Function CreateAsync(User As Db.User, Password As String, City As Db.City) As Task(Of IdentityResult)
Dim oResult As IdentityResult
Password.ThrowIfNothing(NameOf(Password))
User.ThrowIfNothing(NameOf(User))
City.ThrowIfNothing(NameOf(City))
User.CityId = City.Id
User.City = City
oResult = Await MyBase.CreateAsync(User, Password)
If oResult.Succeeded Then
City.Users.RemoveAll(Function(U) U.Id = User.Id)
City.Users.Add(User)
Else
User.CityId = Nothing
User.City = Nothing
End If
Return oResult
End Function
End Class
For the present, since I seem to be able to raise the callback only when using the native cast, a temporary workaround could be to use that cast and check for User.City = Nothing in the callback. At least I could unblock with that.
But for the long run, and for better code stability, I'd like to figure out why the callback isn't raised when I cast UserManager to my subclass.
Any ideas? It's a longshot, I know, but could this be a bug in Moq?
--EDIT 3--
Actually, it looks to be a bug in VB. It works fine in C#.
Here's a repro solution: OneDrive
I've raised an issue at the Moq repo, where I've provided some detail on how to use and debug the repro to get the same results.
Due to this new discovery, I'm adding both the VB.NET and C# tags to this question.
Can anyone spot any differences between the two projects?
All suggestions are welcome.