기존 테스트 코드에 시큐리티 적용하기

hyyyynjn·2021년 3월 3일
2
post-thumbnail
post-custom-banner

전체 테스트 수행하기

  • Gradle 탭에서 Tasks > verification > test를 선택하여 전체 테스트를 수행할 수 있다.
    • 롬복 테스트 이외의 모든 테스트에서 실패한다.

테스트에 실패한 이유들

1. CustomOAuth2UserService를 찾을 수 없음

  • src/main과 src/test의 환경 차이이다.
    • 각각 자신만의 환경 구성을 가진다.
    • test에 application.yml 파일이 없으면 main의 application.yml 파일을 가져온다. 단, application.yml 파일까지만 가져올 뿐 application-oauth.yml 파일은 test에 없다고 가져오지 않는다.

해결방법 : 가짜 설정값을 등록해야한다.

```yaml
spring:
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true

  h2:
    console:
      enabled: true
  session:
    store-type: jdbc

  # test oauth
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: test
            client-secret: test
            scope: profile, email
```

2. 302 Status Code

  • 스프링 시큐리티 설정으로 인해 인증되지 않은 사용자의 요청은 이동시키때문에 발생한 오류이다.

해결방안 : 임의로 인증된 사용자를 추가하여 API만 테스트한다.

  • build.gradle 에 스프링 시큐리티 테스트를 위한 여러 도구를 지원하는 spring-security-test 를 추가한다.
    // 스프링 시큐리티 테스트를 위한 여러 도구를 지원하는 의존성
    testCompile('org.springframework.security:spring-security-test')
  • PostsApiControllerTest의 @Test 메소드에 임의의 사용자 인증 추가 어노테이션을 추가한다.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {

...

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    // @Before
    // 매번 테스트가 시작되기 전에 MockMvc 인스턴스를 생성한다
    @Before
    public void setUp() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }

...

    @Test
    @WithMockUser(roles = "USER")
    public void Posts_등록된다() throws Exception {
        String title = "title";
        String content = "content";
        PostSaveRequestDto requestDto = PostSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts";

        // mvc.perform
        // 생성된 MockMvc를 통해 API를 테스트한다.
        mvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                // 본문(Body) 영역은 문자열로 표현하기 위해 ObjectMapper를 통해 문자열 JSON으로 변환한다.
                .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);

    }

    @Test
    @WithMockUser(roles = "USER")
    public void Posts_수정된다() throws Exception {
        Posts savedPosts = postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long updatedId = savedPosts.getId();
        String expectedTitle = "title2";
        String expectedContent = "content2";

        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(expectedTitle)
                .content(expectedContent)
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts/"+ updatedId;

        mvc.perform(put(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(expectedContent);
    }
}
  • @WithMockUser(roles = "USER")

    • 인증된 모의(가짜) 사용자를 만들어서 사용한다.
    • roles에 권한을 추가할 수 있다.
    • 해당 어노테이션으로 ROLE_USER 권한을 가진 사용자가 API를 요철하는 것과 동일한 효과를 가지게 된다.
    • @WithMockUser 는 MockMvc에서만 작동한다.
    • 단, WithMockUser는 MockMvc에서만 작동한다.
      • @SpringBootTest에서 MockMvc를 사용해야한다.
@SpringBootTest에서 MockMvc를 사용하는 방법
    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    // @Before
    // 매번 테스트가 시작되기 전에 MockMvc 인스턴스를 생성한다
    @Before
    public void setUp() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build();
    }
    
       @Test
    @WithMockUser(roles = "USER")
    public void Posts_등록된다() throws Exception {

...

        // mvc.perform
        // 생성된 MockMvc를 통해 API를 테스트한다.
        mvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                // 본문(Body) 영역은 문자열로 표현하기 위해 
                // ObjectMapper를 통해 문자열 JSON으로 변환한다.
                .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());
...

    }

3. @WebMvcTest에서 CustomOAuth2UserService을 찾을 수 없음

  • @WebMvcTest에서 CustomOAuth2UserService을 스캔하지 않기 때문에 발생한다.
    • @WebMvcTest는 WebSecurityConfigurerAdapter, WebMvcConfigurer 를 비롯한 @ControllerAdvice, @Controller 를 읽지만 @Repository, @Service, @Component는 스캔 대상이 아니므로 읽지 않는다.
  • 정리하면 WebSecurityConfigurerAdapter를 implements하는 SecurityConfig 클래스의 CustomOAuth2UserService를 읽지 못한다는 의미이다.
  • 이 문제를 해결하기 위해서 @WebMvcTest의 스캔 대상에서 SecurityConfig를 제거해야한다.

해결방안

  1. WebMvcTest의 스캔 대상에서 SecurityConfig를 제거한다.
  2. @WithMockUser(roles = "USER")을 사용하여 가짜로 인증된 사용자를 생성한다.
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class,
        excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
        }
)
public class HelloControllerTest {

    // @Autowired
    //  스프링이 관리하는 Bean을 주입 받는 역할을 한다.
    // private MockMvc mvc
    //  웹 API를 테스트할 경우에에 사용한. HTTP GET,POST 등에 대한 테스트가 능가
    @Autowired
    private MockMvc mvc;

    @Test
    @WithMockUser(roles = "USER")
    public void hello가_리턴된다() throws Exception {
		...
    }

    @Test
    @WithMockUser(roles = "USER")
    public void helloDto가_리턴된다() throws Exception {
		...

    }
}
  1. @EnableAuditing 어노테이션을 @SpringBootApplication과 분리한다
    • @EnableAuditing 를 사용하기위해선 @Entity 클래스가 필요하다.
    • @WebMvcTest에서는 당연히 @Entity 클래스가 없다.
      • 그러므로 @EnableAuditing 어노테이션을 @SpringBootApplication과 분리한다
        • 왜냐하면, @WebMvcTest에서 @SpringBootApplication를 스캔할 때 @EnableAuditing 어노테이션도 같이 스캔하기 때문이다.
  • Application.java에서 @EnableAuditing 어노테이션을 JpaConfig 클래스에 옮긴다.
//@EnableJpaAuditing 제거
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        // SpringApplication.run 함수 : 내장 WAS(웹 어플리케이션 서버)를 실행한다. 외부의 별도 WAS(Tomcat 등)가 필요 없게된다
        // 스프링 부트로 만들어진 Jar 파일(실행가능한 Java 패키징 파일)로 실행하면 된다.
        // 내장 WAS는 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있다.
        SpringApplication.run(Application.class, args);
    }
}

@Configuration
@EnableJpaAuditing
public class JpaConfig {
}

  • 해당 에러를 다 해결하고 난뒤 전ㄴ체 테스트를 수행하면 모두 통과한다.
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 1월 18일

너무 감사합니다 바로 해결되네요 ㅎㅎ

답글 달기