Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

by-name parameters incorrectly evaluated when mocking #446

Open
bpatters opened this issue Apr 21, 2022 · 1 comment
Open

by-name parameters incorrectly evaluated when mocking #446

bpatters opened this issue Apr 21, 2022 · 1 comment

Comments

@bpatters
Copy link

I've provided a very simple sbt project that reproduces this problem.
by-name-repro.zip

In the reproduction project, similar to my actual scenario but simplified, the scenario is as follows:
I have a class we can call DataManager that I want to test. It's methods all follow a similar pattern of:

class DataManager {

  /**
    * Perform admin operations if the user is allowed to perform them
    * @param data  admin data
    * @param securityPolicy the security policy to check users access
    * @param userRole the current users role
    * @return
    */
  def doAdminStuff(
    data: String
  )(
    implicit securityPolicy: SecurityPolicy,
    userRole:                UserRole
  ): Future[Int] =
    securityPolicy.withRole(UserRole.ADMIN) {
      println("Performing admin operation")
      Future(1)
    }
}

The implicit Security policy is what I'm mocking in my tests and then testing a normal instance of the Data Manager
The security policy uses curried operations of the following form:

  /**
    * Run the specified operation only if the expectedRole is equal to the users role
    * @param expectedRole the required role to execute the operation
    * @param operation the operation to perform
    * @param userRole the users actual role
    * @return the result of the operation or throws an exception if unauthorized
    */
  def withRole(expectedRole: UserRole)(operation: => Future[Int])(implicit userRole: UserRole) =
    if (expectedRole == userRole)
      operation
    else
      throw new Exception(s"must have $expectedRole to perform this operation")

The operation should only ever be evaluated if the security check passes.

The following tests will fail because the operation is evaluated even though the security policy's mocked answer is executed correctly:

  test("Admin operation should not be called with user role") {
    implicit val securityPolicy: SecurityPolicy = mock[SecurityPolicy]
    val dataManager = new DataManager()

    // by-name parameter evaluation are implemented by wrapping in a Function0
    { (role: UserRole, operation: Function0[Future[Int]], userRole: UserRole) =>
      throw new Exception("Access denied")
    } willBe answered by securityPolicy.withRole(eqTo(UserRole.ADMIN))(any[Future[Int]])(
      any[UserRole]
    )
    intercept[Exception] {
      implicit val userRole: UserRole = UserRole.USER
      Await.result(dataManager.doAdminStuff("test"), Duration.Inf)
    }
    assert(dataManager.adminOperationCount == 0)
  }

However if I wrap the DataManager instance in a spy then the same test will pass as the operation isn't executed:

  test("using spy, Admin operation should not be called with user") {
    implicit val securityPolicy: SecurityPolicy = mock[SecurityPolicy]
    val dataManager = spy(new DataManager())

   // by-name parameter evaluation are implemented by wrapping in a Function0
    { (role: UserRole, operation: Function0[Future[Int]], userRole: UserRole) =>
      throw new Exception("Access denied")
    } willBe answered by securityPolicy.withRole(eqTo(UserRole.ADMIN))(any[Future[Int]])(
      any[UserRole]
    )
    doCallRealMethod().when(dataManager).doAdminStuff(anyString)(any[SecurityPolicy], any[UserRole])
    doCallRealMethod().when(dataManager).adminOperationCount

    intercept[Exception] {
      implicit val userRole: UserRole = UserRole.USER
      Await.result(dataManager.doAdminStuff("test"), Duration.Inf)
    }
    assert(dataManager.adminOperationCount == 0)
  }
@ultrasecreth
Copy link
Member

Sorry, lost track of this, will try to check it over the weekend

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants