MockMVC で Spring Security に対応させる

# はじめに

以前、MockMvcを使う記事を書きました。その後、Spring Securityを使ったアプリで MockMvcを上手く動作させられませんでした。SpringやJUnitのバージョンの違いか、色々な記事があるのですが、どれの方法を試しても駄目でした。それらの記事を参考にして試しながら、何とかテストコードが動かせるようになったので、記事にまとめます。

# 以前の記事でまとめたポイント

- JUnit5を使う為にアノテーションを差し替え
- テスト用コンフィグクラウを準備

以前の記事：　[SpringBootとJUnit5でMockMvcを使うには](https://qiita.com/thankkingdom/items/f1b600a3b27ee858c448)

```java
// テストクラス
@ExctendWith(SpringExtension.class)
@WebMvcTest(XxxController.class)

// テスト用コンフィグクラス
@Configuration
@ComponentScan({"パッケージ名"})
```

# 今回の遭遇した問題

## ①アノテーション

例外が発生するので、どのアノテーションを使えばいいかを試行錯誤しました。
※記事末尾のサンプルをご参照下さい。

## ②HTTP403エラー Forbidden

Spring Securityを使うと、CSRF対策がデフォルトで有効になる為、HTTP403が発生。
MockMVCでリクエストする際、SecurityMockMvcRequestPostProcessors.csrf() を追加すればエラーを回避できる。

```java
mockMvc.perform(post("http://localhost:8080/customers")
	.contentType(MediaType.TEXT_HTML)
	.with(SecurityMockMvcRequestPostProcessors.csrf())
...
```

## ③HTTP404エラー

リクエストURLをRequestMappingの記述部分しか書いていなかった為に発生。
※色々試していたアノテーションが原因かと勘違いして意外と時間がかかった。

```java
java.lang.AssertionError: Range for response status value 404 expected:<SUCCESSFUL> but was:<CLIENT_ERROR>
```

## ④Thymeleafで出力しているログインユーザー情報の参照

これが一番難しかったです。
これは正にと思える @WithMockUser と言うアノテーションがあったので、設定するも、Thymeleafでログインユーザー情報が参照できず。

```java
// エラーメッセージ
Caused by: org.springframework.beans.NotReadablePropertyException: Invalid property 'principal.user' of bean class 
```

```java
// @WithMockUser で設定したユーザーを指定して見たが駄目だった例
mockMvc.perform(get("http://localhost:8080/customers")
	.contentType(MediaType.TEXT_HTML)
	.with(SecurityMockMvcRequestPostProcessors.csrf())
	.with(user("user1"))
```

org.springframework.security.core.userdetails.UserDetailsService インターフェースを実装したクラスを利用する事で参照できるように。

```java
// まず、テストクラスのメンバ変数にサービスクラスを作成
@Autowired
private LoginUserDetailsService loginUserDetailsService;

// 次に、テストクラスのメソッドで、以下を実装
// インプリメントしたloadUserByUsername()メソッドでユーザー情報を取得
LoginUserDetails userDetails = (LoginUserDetails) loginUserDetailsService.loadUserByUsername("admin1");

// 取得したユーザー情報を渡す
mockMvc.perform(get("http://localhost:8080/customers")
	.contentType(MediaType.TEXT_HTML)
	.with(SecurityMockMvcRequestPostProcessors.csrf())
	.with(user(userDetails))
```

## ⑤フォーム情報

MockHttpServletRequestBuilder.param()メソッドを利用するだけ。


# コード

テストクラスに設定するアノテーションが変わりました。
前回必須だと思ったコンフィグクラスは不要でした。

```java
@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestInstance(Lifecycle.PER_CLASS)
@AutoConfigureMockMvc
public class CustomerControllerTest {

	@Test
	public void test() throws Exception {

		LoginUserDetails userDetails = (LoginUserDetails) loginUserDetailsService.loadUserByUsername("admin1");

		//@formatter:off
		mockMvc.perform(post("http://localhost:8080/customers/create")
			.contentType(MediaType.TEXT_HTML)
			.with(SecurityMockMvcRequestPostProcessors.csrf())
			.with(user(userDetails))
			.param("firstName", "太郎")
			.param("lastName", "山田")
		)
		.andExpect(SecurityMockMvcResultMatchers.authenticated())
		.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
		.andExpect(MockMvcResultMatchers.redirectedUrl("/customers"))
		.andReturn();
		//@formatter:on
	}
}
```



# 参考資料

- [MockMvcを使ったControllerのUnitTestでCsrfトークンをRequestする](https://qiita.com/su-kun1899/items/b661d8a84e6acb2d7751)
- [Spring Security 使い方メモ　CSRF](https://qiita.com/opengl-8080/items/3720ed415c2df26251a7)
- [Mocking Springframework Security Principal for testing Spring MVC Controllers](http://javaninja.net/2014/02/mocking-spring-security-principal-for-testing-spring-mvc-controllers/)
- [Status expected:<200> but was:<404> in spring test](https://stackoverflow.com/questions/19138255/status-expected200-but-was404-in-spring-test)
- [Java Code Examples for org.springframework.security.authentication.TestingAuthenticationToken](http://www.javased.com/?api=org.springframework.security.authentication.TestingAuthenticationToken)
- [Spring Securityを使っているWebアプリのUnitテスト](https://ishiis.net/2016/11/12/spring-boot-security-unit-test/)
- [Testing Form posts through MockMVC](https://stackoverflow.com/questions/36568518/testing-form-posts-through-mockmvc)