scalaの認証ライブラリSilhouetteでの認証をかましたリクエストテスト方法

## はじめに
scalaの認証テストsilhoetteを使った場合のリクエストテストの方法。
routeに対して認証をかけた時、普通にリクエストテストを実行すると落ちるが、それを回避するためのtips。
内容は殆ど公式準拠。

https://www.silhouette.rocks/docs/testing

## Test Helpers
playを拡張するヘルパーライブラリをまずinstall。

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

テストのファイルにimport。

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

併用するテストフレームワークは選ばないが、以下の例はすべてSpecs2。

## Test silhouette actions
以下のようなコントローラをテストしたい。
その場合、IdentityとAuthenticatorをハンドルするFaceEnvironmentを使用してテストを実行する。

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

  /**
   * userを取得.
   * 認証あり.
   */
  def user = silhouette.SecuredAction { implicit request =>
    // 認証が通っている場合はok
    Ok(Json.toJson(request.identity))
  }

  /**
   * userが認証されているかチェック.
   * コントローラ自体に認証はかかっていないがどのユーザからのアクセスかは知覚する.
   */
  def isAuthenticated = silhouette.UserAwareAction { implicit request =>
    request.identity match {
      case Some(identity) => Ok  // 認証済みuserからのアクセスはok
      case None => Unauthorized  // 認証されていないuserからのアクセスはUnauthorized
    }
  }
}
```

### FakeEnvironment
テスト用のFakeEnvironmentにはLoginInfoとIdentityのペアを渡しさえすれば良い。
あとは勝手にFakeIdentityServicerやAuthenticatorServiceをいい感じに作成してくれる。

```scala
val identity = User(LoginInfo("facebook", "apollonia.vanova@watchmen.com"))
implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
```

### FakeRequest
silhouetteのテストヘルパーはplay組み込みテストヘルパーのFakeRequestを拡張する。

１つ目のメソッドはAuthenticatorを受け取りリクエスト内に組み込む。

```scala
val identity = User(LoginInfo("facebook", "apollonia.vanova@watchmen.com"))
implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
val authenticator = new CookieAuthenticator("test", identity.loginInfo, ...)
val request = FakeRequest().withAuthenticator(authenticator)
```

２つ目のメソッドはAuthenticatorの元になるLoginInfoを受け取り、リクエストに組み込む。

```scala
val identity = User(LoginInfo("facebook", "apollonia.vanova@watchmen.com"))
implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
val request = FakeRequest().withAuthenticator(identity.loginInfo)
```

> **NOTE**
> これらのメソッドを使用する際にはimplicitなenvironmentを同じスコープにおいておく必要がある。

### 実際のテスト
#### *authenticator(認証子)の欠落ケースのテスト*
リクエスト内にtokenなどの認証子がないケースのテストでは、リクエストに何も渡さずにテストすればいい。

```scala
class UserSpec extends PlaySpecification {

  "The `user` method" should {
    "authenticatorがない場合401が返る" in new WithApplication {
      val identity = User(LoginInfo("facebook", "apollonia.vanova@watchmen.com"))
      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 {
    "authenticatorがない場合401が返る" in new WithApplication {
      val identity = User(LoginInfo("facebook", "apollonia.vanova@watchmen.com"))
      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)
    }
  }
}
```

#### *Identity(認証対象)が存在しないケースのテスト*
authenticatorに対応するidentityがないケースをテストする場合、user(ここで言うIdentity)とauthenticatorに異なるlogin infoを渡せばいい。

```scala
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", "apollonia.vanova@watchmen.com")) // ここのlogin infoと、
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest()
        .withAuthenticator(LoginInfo("xing", "comedian@watchmen.com")) // ここのlogin infoが違う

      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", "apollonia.vanova@watchmen.com")) // ここのlogin infoと、
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest()
        .withAuthenticator(LoginInfo("xing", "comedian@watchmen.com")) // ここのlogin infoが異なる

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

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

#### *Identityの正常認証ケースのテスト*
正常な認証ケースをテストしたい場合同じlogin infoをidentityとauthenticatorに渡せばいい。

```scala
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", "apollonia.vanova@watchmen.com")) // ここのlogin infoと、
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest().withAuthenticator(identity.loginInfo) // ここのlogin infoが同じ

      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", "apollonia.vanova@watchmen.com")) // ここのlogin infoと、
      implicit val env = FakeEnvironment[MyEnv](Seq(identity.loginInfo -> identity))
      val request = FakeRequest().withAuthenticator(identity.loginInfo) // ここのlogin infoが同じ

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

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