Spring の Controllerで認証情報を参照するメソッドをテストするときの準備

::: note warn
ここに書かれている情報について最近理解を深めた結果、書き方に不正確な点があったり別の方法で対応した方が良いことがわかったのでいずれ書き直そうと思います

https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/authentication.html
:::


## 認証済みのテストユーザーを使って、テストを行いたい場合

例えば、以下のように　Contextの認証情報からユーザー情報を取ってきて利用するメソッドがあり、このメソッドをテストしたい。

```kotlin
@RestController
@RequestMapping("item")
@CrossOrigin
class ItemController(private val service: ItemService) {

    @PostMapping("/register")
    fun register(@RequestBody request: RegisterItemRequest) {
        val user = SecurityContextHolder.getContext().authentication.principal as CustomUserDetails
        service.register(request.itemId, account.id)
    }
}
```

このようにテストコードを書いても失敗する。　理由はコンテキストから認証情報を取得できていないから。

```kotlin

internal class ItemControllerTest {
    
    @Test
    fun startRental() {

        // Given
        val accountRepository = mock<accountRepository>()
        val itemRepository = mock<ItemRepository>()
        val itemService = ItemService(accountRepository, itemRepository)
        val itemController = itemController(itemService)
        ｖal mockMvc = MockMvcBuilders.standaloneSetup(itemController).build()

        val account = Account(1000L, "test@example.com", "pass", Role.USER)
        val request = RegisterItemRequest(100L)
        val json = ObjectMapper().registerKotlinModule().writeValueAsString(request)

        whenever(accountRepository.findById(any())).thenReturn(account)
        whenever(itemRepository.findById(any())).thenReturn(Item(request.id))

        // When
        mockMvc.perform(post("/item/register").contentType(APPLICATION_JSON).content(json))
            .andExpect(status().isOk)

        // Then
        verify(itemRepository).register(any())
    }

}
```

なので、認証済みのテストユーザーを用意することになるので、その方法を幾つか明記する。

### MockUser を使用する

テストメソッドに　`@WithMockUser` をつけてUser情報を定義することで、そのユーザーの認証情報でテストを実行してくれる。
このアノテーションの属性には、Authenticationプリンシパルの "username","password", "roles" などを指定して、ダミーユーザーで認証することができる。

`@WithMockUser(username="hogehoge", password="12345", roles="USER")`

### カスタムモックユーザーを用意する

今回の場合、Authenticationプリンシパルに、ID情報を追加してカスタマイズを行なっている。
このようにAuthenticationプリンシパルをカスタマイズしている場合、`@WithSecurityContext` を利用して、`@WithCustomMockUser` のようなカスタムアノテーションを作って利用することができる。

```kotlin
@Retention(AnnotationRetention.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory::class)
annotation class WithCustomMockUser(
    val id: Long = 1000L,
    val email: String = "test@example.com",
    val pass: String = "pass",
    val roleType: RoleType = RoleType.USER
) {}

class WithMockCustomUserSecurityContextFactory() : WithSecurityContextFactory<WithCustomMockUser> {
    override fun createSecurityContext(user: WithCustomMockUser): SecurityContext {
        val account = Account(user.id, user.email, user.pass, user.roleType)
        val principal = CustomUserDetails(account)
        val auth = UsernamePasswordAuthenticationToken(principal, principal.password, principal.authorities)
        val context = SecurityContextHolder.createEmptyContext()
        context.authentication = auth
        return context
    }
}
```

作成した `@WithCustomMockUser` をテストメソッドにつける

```kotlin
internal class ItemControllerTest {
    
    @Test
    @WithCustomMockUser(id=100L)
    fun startRental() {
        ......
    }
}
```

しかしこれでテストを実行しても、認証情報(principal)が取得できず、まだテストが失敗する
理由は、このテストクラスを単体で実行していたので、 `@WithCustomMockUser` で宣言していても、別のContextとなってしまっており、認証情報が取得できていなかった。

回避方法は、 テストクラスに `@SpringBootTest` を付ければ良い。

最終的には次のようにテストクラスを書くことで、テストが成功した。

```kotlin

@SpringBootTest
internal class ItemControllerTest {
    
    @Test
    @WithCustomMockUser(id=100L)
    fun startRental() {

        // Given
        val accountRepository = mock<accountRepository>()
        val itemRepository = mock<ItemRepository>()
        val itemService = ItemService(accountRepository, itemRepository)
        val itemController = itemController(itemService)
        ｖal mockMvc = MockMvcBuilders.standaloneSetup(itemController).build()

        val account = Account(1000L, "test@example.com", "pass", Role.USER)
        val request = RegisterItemRequest(100L)
        val json = ObjectMapper().registerKotlinModule().writeValueAsString(request)

        whenever(accountRepository.findById(any())).thenReturn(account)
        whenever(itemRepository.findById(any())).thenReturn(Item(request.id))

        // When
        mockMvc.perform(post("/item/register").contentType(APPLICATION_JSON).content(json))
            .andExpect(status().isOk)

        // Then
        verify(itemRepository).register(any())
    }

}
```

### 備考① テストクラスのSetup()で認証情報をセットする

MockUserを使う方法は、テスト用のUserDetailsを１箇所にまとめて利用できるので便利だが、 `@SpringBootTest` でSpringを起動することになるので、テストが増えると時間が掛かってしまう場合がある。

なので別の方法として、テストクラスの　SetUpメソッド (`@BeforeEach`)で、認証情報をコンテキストにセットすることでも対応できる。もちろん認証情報をクラスごとに定義するので、特にこちらが良いというわけではないが、記録として残しておく。

```kotlin

internal class ItemControllerTest {

   val account = Account(1000L, "test@example.com", "pass", Role.USER)

    @BeforeEach
    internal fun setUp() {
        val principal = CustomeUserDetails(account)
        val auth = UsernamePasswordAuthenticationToken(principal, principal.password, principal.authorities)
        SecurityContextHolder.getContext().authentication = auth
    }
    
    @Test
    fun startRental() {

        // Given
        val accountRepository = mock<accountRepository>()
        val itemRepository = mock<ItemRepository>()
        val itemService = ItemService(accountRepository, itemRepository)
        val itemController = itemController(itemService)
        ｖal mockMvc = MockMvcBuilders.standaloneSetup(itemController).build()

        val request = RegisterItemRequest(100L)
        val json = ObjectMapper().registerKotlinModule().writeValueAsString(request)

        whenever(accountRepository.findById(any())).thenReturn(account)
        whenever(itemRepository.findById(any())).thenReturn(Item(request.id))

        // When
        mockMvc.perform(post("/item/register").contentType(APPLICATION_JSON).content(json))
            .andExpect(status().isOk)

        // Then
        verify(itemRepository).register(any())
    }

}
```

### 備考② `@WithUserDetails`を使って認証情報を取得する

`@WithSecurityContext`を利用してカスタムアノテーションを作らなくても、 `@WithUserDetails` を使って、UserDetailseServiceを指定し、カスタムAuthenticationプリンシパルを取得することもできる。

ただし、UserDetailsServiceを使って、プリンシパルを取得する場合は、テストユーザーを事前にDBなどから登録する必要があるので、先に挙げた、カスタムアノテーションでテストモックユーザーを用意する方が、テストコードでユーザーを柔軟に作成できる。

```kotlin
/* このCustomeUserDetailsServiceクラスは、usernameをキーに、　
　　＊　　authenticationService.findAccount(username) で、アカウントを検索して、カスタムプリンシパルを返している。
　　＊　　findAccount()メソッドでは、最終的にDBのテーブルを検索している。
　　*/
class CustomeUserDetailsService(private val authenticationService: AuthenticationService) : UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails? {
        val account = authenticationService.findAccount(username)
        return account?.let { CustomeUserDetails(account) }
    }
}
```

`@WithUserDetails` で、CustomeUserDetailsService を利用するため、テスト用のConfiguration定義でBean登録を行う。

```kotlin
@Configuration
class SecurityTestConfiguration(private val authenticationService: AuthenticationService) {

    @Bean
    fun customeUserDetailsService(): CustomeUserDetailsService {
        return CustomeUserDetailsService(authenticationService)
    }
}
```

あとは、テストメソッド (またはテストクラス) に　`@WithUserDetails` を付けて実行する。
このとき、value属性に設定した、usernameを使って、CostomeUserDetailsServiceクラスから、カスタムプリンシパルを取得してテストユーザーとする訳だが、　アカウント情報をもつDBに接続できていなかったり、指定したusernameが存在しないとテストユーザーが取れないので、テストが失敗する。

```kotlin

@SpringBootTest
internal class ItemControllerTest {
    
    @Test
    // @WithCustomMockUser(id=100L)
    @WithUserDetails(value = "user", userDetailsServiceBeanName = "customeUserDetailsService")
    fun startRental() {
        ...
    }
}
```

## MockMvc のセットアップ方法

認証情報を取る方法を試行錯誤しているときに、MockMVCのセットアップ方法の違いについても少し調べたときに下記の公式ドキュメントを見つけたので、リンクだけしておく。
ちなみに今回作成しているテストは、Controller単位のテストだったので、 `standaloneSetup` を選択している。

[Spring テスト リファレンス - MockMVC - セットアップの選択](https://spring.pleiades.io/spring-framework/docs/current/reference/html/testing.html#spring-mvc-test-server-setup-options)

## 参考リンク
[Spring Security ユニットテスト](https://reiphiel.hatenablog.com/entry/spring-security-unit-test)
[Spring Securityを使っているWebアプリのUnitテスト](https://ishiis.net/2016/11/12/spring-boot-security-unit-test/)
[Spring Security リファレンス - メソッドのセキュリティテスト](https://spring.pleiades.io/spring-security/site/docs/current/reference/html5/#test-method)
[Spring Security for Spring Boot Integration Tests](https://www.baeldung.com/spring-security-integration-tests)