Callback not being raised for an overloaded mocked method

Viewed 275

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.

1 Answers
Related