Spring SecurityのOAuth2ログインしてる体でコントローラーのテストをする

# やりたかったこと
外部のIdpを使ってログインを実装したアプリケーションで、コントローラーのテストをするときにログイン済みの体でControllerのテストをしたかった。

かなり雰囲気でやったらできた感じなのでそもそもSpringがこういうの提供してるよ的なのあったら教えてください。

## テスト対象はこういうやつ
### build.gradle（抜粋）
```groovy
buildscript {
	ext {
		defaultEncoding  = 'UTF-8'
		compatibility    = 1.8
		springBootVersion = '2.0.0.RELEASE'
	}
	repositories {
		mavenCentral()
		maven { url "https://repo.spring.io/snapshot" }
		maven { url "https://repo.spring.io/milestone" }
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

repositories {
	mavenCentral()
	maven { url "https://repo.spring.io/snapshot" }
	maven { url "https://repo.spring.io/milestone" }
}

dependencyManagement {
	imports {
		mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
	}
}

dependencies {
	compile('org.springframework.boot:spring-boot-starter-actuator')
	compile('org.springframework.boot:spring-boot-starter-mail')
	compile('org.springframework.boot:spring-boot-starter-security')
	compile('org.springframework.boot:spring-boot-starter-web')

	compile('org.springframework.security:spring-security-oauth2-client')
	compile('org.springframework.security:spring-security-oauth2-jose')

	runtime('org.springframework.boot:spring-boot-devtools')
	testCompile('org.springframework.boot:spring-boot-starter-test')
	testCompile('org.springframework.security:spring-security-test')
	testCompile group: 'com.h2database', name: 'h2', version: '1.4.196'

}

```
### application.yml
```yml
####################################################################################################
# spring
####################################################################################################
spring:
  security.oauth2.client.registration:
      google:
        client-id: xxxxx
        client-secret: xxxxx
```

### コントローラー
```java
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


/**
 * Controller for login.
 */
@Controller
public class LoginController {

    /**
     * 認証後に表示するページ。
     *
     * @param authentication authentication info
     * @return user info
     */
    @GetMapping("/")
    public String index(final OAuth2AuthenticationToken authentication) {
        return "index";
    }

    /**
     * ログイン画面を表示する.
     *
     * @return login page
     */
    @GetMapping("/login")
    public String viewLogin() {
        return "login";
    }
}

```

## 設定
```java
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * Configuration about security.
 *
 * @since
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final HttpSecurity http) throws Exception { // NOPMD
        // Define NOPMD because HttpSecurity$authorizeRequests throws Exception.
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .logout().permitAll()
                .and()
                .oauth2Login()
                .loginPage("/login").permitAll();
    }
}

```

# テストはこうした
## Mockユーザー
このアノテーションがついてると認証済みの状態でアクセスするよ的な。

```java
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import org.springframework.security.test.context.support.WithSecurityContext;

/**
 * Annotation for test with OAuth2 Authentication.
 */
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithOAuth2SecurityContextFactory.class)
public @interface WithMockOAuth2User {

    /**
     * Username.
     *
     * @return username
     */
    String username() default "testuser";

}

```

## SecurityContextFactory
とにかく```OAuth2AuthenticationToken```を作ってSecurityContextに叩き込んでるやつ。XXXなユーザでテストしたいぞ的な気持ちはここをごにょごにょすれば良さそう。

```java
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

/**
 * OAuth2 token provider(put to {@SecurityContext} for integration test.
 */
public class WithOAuth2SecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2User> {

    /**
     * Create security context with OAuth2 token.
     *
     * @param user user
     * @return security context
     */
    @Override
    public SecurityContext createSecurityContext(final WithMockOAuth2User user) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", "testsub");

        Set<GrantedAuthority> authorities = new HashSet<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));

        OAuth2User oidcUser = new DefaultOidcUser(authorities, new OidcIdToken("sampletoken", Instant.MIN, Instant.MAX, claims));
        OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(oidcUser, authorities, "test-client");
        context.setAuthentication(token);
        return context;
    }
}

```
## テストクラス
`@WithMockOAuth2User`がついてるとログイン後の世界でURLを叩ける。

```java

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;



/**
 * Test for {@link LoginController}.
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class LoginControllerTest {

    /**
     * MockMvc.
     */
    @Autowired
    private MockMvc mockMvc;

    /**
     * Test for index page before login (redirect to login page).
     *
     * @throws Exception exception
     */
    @Test
    public void indexBeforeLogin() throws Exception { // NOPMD
        mockMvc.perform(get("/")).andExpect(status().is3xxRedirection());
    }

    /**
     * Test for index page after login.
     *
     * @throws Exception exception
     */
    @Test
    @WithMockOAuth2User()
    public void indexAfterLogin() throws Exception { // NOPMD
        mockMvc.perform(get("/")).andExpect(status().isOk());
    }


    /**
     * Test for login page.
     *
     * @throws Exception exception
     */
    @Test
    public void viewLogin() throws Exception { // NOPMD
        mockMvc.perform(get("/login")).andExpect(status().isOk());
    }
}

```