Test Helpers

Silhouette provides some test helpers that can be used to easily test your Silhouette application. These helpers are located in the additional TestKit dependency.

libraryDependencies ++= Seq(
	"com.mohiva" %% "play-silhouette-testkit" % "version" % "test"
)

After providing the dependency, the helpers can be used by importing the following package into your test scenarios.

import com.mohiva.play.silhouette.test._

All helpers are test framework agnostic. You can use it with Specs2, ScalaTest or every other testing framework. In our examples we use Specs2 to demonstrate how to test Silhouette applications, because until Play 2.4 specs2 was to be the default framework shipped with Play and we still use specs2 to test Silhouette.

Test Silhouette actions

With the previous mentioned test helpers it's really easy to test your Silhouette actions.
As first lets look how a typical controller instance could look like:

class UserController(silhouette: Silhouette[MyEnv]) extends Controller {

  /**
   * Gets a user.
   */
  def user = silhouette.SecuredAction { implicit request =>
    Ok(Json.toJson(request.identity))
  }

  /**
   * Checks if a user is authenticated.
   */
  def isAuthenticated = silhouette.UserAwareAction { implicit request =>
    request.identity match {
      case Some(identity) => Ok
      case None => Unauthorized
    }
  }
}

If you would like to test this controller, you must provide an environment that can handle your Identity and Authenticator implementation. For this case Silhouette provides a FakeEnvironment which automatically sets up all components needed to test your specific actions.

FakeEnvironment

The fake environment does all the annoying steps for you to create and instantiate all dependencies that you need for your test. You must only specify one or more LoginInfo -> Identity pairs that should be returned by calling request.identity in your action and the authenticator instance that tracks this user.

val identity = User(LoginInfo("facebook", "[email protected]"))
implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))

Under the hood, the environment instantiates a FakeIdentityService which stores your given identities and returns it if needed. It instantiates also the appropriate AuthenticatorService based on your defined Authenticator type. All Authenticator services are real service instances set up with their default values and dependencies.

FakeRequest

Let us summarize briefly how an Action in Play works. An action is basically an anonymous function that handles a Request and returns a Result. With this in mind we must now create a request that we can pass to our action. Play ships with a test helper called FakeRequest which does exactly what we want. But this helper cannot embed an Authenticator into the created fake request. Therefore we have extend Play's fake request helper with two additional methods.

The first method accepts an Authenticator instance which is then embedded into the request.

val identity = User(LoginInfo("facebook", "[email protected]"))
implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
val authenticator = new CookieAuthenticator("test", identity.loginInfo, ...)
val request = FakeRequest().withAuthenticator(authenticator)

The second method accepts a LoginInfo instance for which an authenticator will be created and embedded into the request.

val identity = User(LoginInfo("facebook", "[email protected]"))
implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
val request = FakeRequest().withAuthenticator(identity.loginInfo)

๐Ÿ“˜

Note

To embed an authenticator into a request you need an implicit environment in scope.

Tying the Pieces Together

So far, we've learned how to setup a test environment and how to create a request which contains an embedded authenticator. Now we combine these techniques and create a complete controller test.

Simulate a missing authenticator

To simulate that an authenticator couldn't be found for a request, you must only submit a request without an authenticator.

class UserSpec extends PlaySpecification {

  "The `user` method" should {
    "return status 401 if no authenticator was found" in new WithApplication {
      val identity = User(LoginInfo("facebook", "[email protected]"))
      val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest()

      val controller = app.injector.instanceOf[UserController]
      val result = controller.user(request)

      status(result) must equalTo(UNAUTHORIZED)
    }
  }

  "The `isAuthenticated` method" should {
    "return status 401 if no authenticator was found" in new WithApplication {
      val identity = User(LoginInfo("facebook", "[email protected]"))
      val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest()

      val controller = app.injector.instanceOf[UserController]
      val result = controller.isAuthenticated(request)

      status(result) must equalTo(UNAUTHORIZED)
    }
  }
}

Simulate a missing identity

To simulate that an identity couldn't be found for a valid authenticator, you must pass different login information to the user and the authenticator.

class UserSpec extends PlaySpecification {

  "The `user` method" should {
    "return status 401 if authenticator but no identity was found" in new WithApplication {
      val identity = User(LoginInfo("facebook", "[email protected]"))
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest()
        .withAuthenticator(LoginInfo("xing", "[email protected]"))

      val controller = app.injector.instanceOf[UserController]
      val result = controller.user(request)

      status(result) must equalTo(UNAUTHORIZED)
    }
  }

  "The `isAuthenticated` method" should {
    "return status 401 if authenticator but no identity was found" in new WithApplication {
      val identity = User(LoginInfo("facebook", "[email protected]"))
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest()
        .withAuthenticator(LoginInfo("xing", "[email protected]"))

      val controller = app.injector.instanceOf[UserController]
      val result = controller.isAuthenticated(request)

      status(result) must equalTo(UNAUTHORIZED)
    }
  }
}

Simulate an authenticated identity

To simulate an authenticated identity we must submit a valid authenticator and the login information of both the authenticator and the identity must be the same.

class UserSpec extends PlaySpecification {

  "The `user` method" should {
    "return status 200 if authenticator and identity was found" in new WithApplication {
      val identity = User(LoginInfo("facebook", "[email protected]"))
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest().withAuthenticator(identity.loginInfo)

      val controller = app.injector.instanceOf[UserController]
      val result = controller.user(request)

      status(result) must equalTo(OK)
    }
  }

  "The `isAuthenticated` method" should {
    "return status 200 if authenticator and identity was found" in new WithApplication {
      val identity = User(LoginInfo("facebook", "[email protected]"))
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest().withAuthenticator(identity.loginInfo)

      val controller = app.injector.instanceOf[UserController]
      val result = controller.isAuthenticated(request)

      status(result) must equalTo(OK)
    }
  }
}

Test default Play actions

Typically Silhouette authentication code is implemented inside default Play actions. To test such actions you don't need specific helper classes. Here you could use Mockito to mock the Silhouette instances or other related testing tools.