最近はちまちまとKotlinとSpringBootのアプリケーションを勉強しています。
SpringBootの場合、Kotlinでも、JUnitを使うほうが何かとサポートが手厚く、Web上のドキュメントも豊富ですが、今回は勉強ということでkotest(旧KotlinTest)を利用してみました。
その際に、Spring Securityのユーザーを使ったControllerのテストで少しつまずいたので備忘として記事にしておきます。
kotestでは、@WithMockUserが使えない
JUnitを利用する場合、Spring Securityで特定のパラメータを持ったユーザーでログインしていることを想定したテストは、 @WithMockUser
などを使えば簡単にテストすることができます。
一方で、kotestの場合はサポートしていないため、自分でAuthenticationを設定する必要があります。
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)
とはいえ、このままだと SecurityContextHolder
の Authentication
の設定とロールをそれぞれで設定することになってしまいます。
そこでちょっとした工夫として、 ユーザーのロールの 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を使ったほうが楽でいいですね…