What is it, naokirin?

kotest(旧KotlinTest)でSpring Securityのテストをする

最近はちまちまとKotlinとSpringBootのアプリケーションを勉強しています。

SpringBootの場合、Kotlinでも、JUnitを使うほうが何かとサポートが手厚く、Web上のドキュメントも豊富ですが、今回は勉強ということでkotest(旧KotlinTest)を利用してみました。

その際に、Spring Securityのユーザーを使ったControllerのテストで少しつまずいたので備忘として記事にしておきます。

kotestでは、@WithMockUserが使えない

JUnitを利用する場合、Spring Securityで特定のパラメータを持ったユーザーでログインしていることを想定したテストは、 @WithMockUser などを使えば簡単にテストすることができます。

一方で、kotestの場合はサポートしていないため、自分でAuthenticationを設定する必要があります。

github.com

Controllerで特に認証情報などを受け取るようになっていなければ、このIssueの方法で解決できます。

// USERロールでログインしている状態にする
val authentication = PreAuthenticatedAuthenticationToken(
    null, null, listOf(SimpleGrantedAuthority("ROLE_USER"))
)
SecurityContextHolder.getContext().authentication = authentication

// 普通にMockMvcでテストする
mvc.perform(MockMvcRequestBuilders.get("/foo"))
    .andExpect(MockMvcResultMatchers.status().is2xxSuccessful)

ControllerでPrincipalを受け取っていると追加で対応が必要になる

特に引数を受け取らないメソッドでは上記で問題なかったのですが、認証情報を扱いたかったのでControllerのメソッドが以下のようになっていました。

@RestController
class FooController(
    val request: HttpServletRequest
) {
    // 認証情報を使いたいので、Principalを受け取るようにしている
    @RequestMapping("foo")
    fun foo(principal: Principal): FooResponse {
        // ...
    }
}

この場合、Principalにはデータが渡らないので、テストが失敗します。

これをどう解決するかというと、別途テストでUserDetailsのインスタンスを自分で作成して、メソッドの引数に渡されるようにします。

val userDetails = MyProjectUserDetails(1, "", "", UserRole.USER)

// ...

mvc.perform(
    MockMvcRequestBuilders.get("/foo")
        .with(SecurityMockMvcRequestPostProcessors.user(userDetails))
)
    .andExpect(MockMvcResultMatchers.status().is2xxSuccessful)

とはいえ、このままだと SecurityContextHolderAuthentication の設定とロールをそれぞれで設定することになってしまいます。

そこでちょっとした工夫として、 ユーザーのロールの enum class で、文字列を返せるようにします。

enum class UserRole(var value: String) {
    ADMIN("ROLE_ADMIN"),
    USER("ROLE_USER");

    override fun toString(): String = value
}

こうすることで、 SimpleGrantedAuthority の生成部分を、 UserDetails のデータを使って以下のように書くことができます。

// PreAuthenticatedAuthenticationToken に渡すデータをUserDetailsから生成する
SimpleGrantedAuthority(userDetails.roleType.toString())

ここまでやっておけば、ある程度整理された状態で、ログイン状態かつ、メソッドでPrincipalを受け取るようなControllerのテストを書くことができます。

まとめ

kotestで Spring Security を使ったControllerのログイン済みテストをする場合は、

  • 特にメソッドで認証情報などを受け取らないなら、 PreAuthenticatedAuthenticationToken を生成して、 SecurityContextHolder.getContext().authentication に設定する
  • Principal を受け取るメソッドをテストするなら、追加で UserDetails を生成して with() で渡す

という感じです。 ロールの enum class で工夫することで、共通化もある程度できそうなので、kotestの場合はこの対応で行きたいと思います。

とはいえ、SpringBootでは、素直にJUnitを使ったほうが楽でいいですね…