Spring/Spring Boot

[Spring Boot] 테스트 시큐리티 적용

ozofweird 2020. 9. 17. 23:11

1. 테스트 시큐리티 적용

1) 테스트 시큐리티

이전에는 API를 호출을 하여 테스트 코드도 간단하게 호출하도록 구성하였지만, 시큐리티 적용 후 인증된 사용자만 API를 호출하도록 구성되었다. 테스트 코드마다 인증한 사용자가 호출한 것처럼 작동하도록 수정해야 한다.

전체 테스트

2) 문제 (1)

'CustomOAuth2UserService를 찾을 수 없음' 문제점은 CustomOAuth2UserService를 생성하는데 필요한 소셜 로그인 관련 설정값들이 없어서 발생한다. 'src/main'과 'src/test' 환경은 각각의 환경 구성을 지니기에 테스트 시 application-oauth.properties가 제대로 작동하지 않는다. (application.properties는 test에 없을 경우 main에서 자동으로 가져온다.) 따라서 테스트만을 위한 application.properties 파일을 생성한다.

spring.jpa.show_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.h2.console.enabled=true
spring.session.store-type=jdbc

# Test OAuth
spring.security.oauth2.client.registration.google.client-id=test
spring.security.oauth2.client.registration.google.client-secret=test
spring.security.oauth2.client.registration.google.scope=profile,email

3) 문제 (2)

응답의 결과로 200이 아닌 302 응답이 오는 경우다. 이는 스프링 시큐리티 설정으로인해 인증되지 않은 사용자의 요청은 이동시키기 때문이다. 이를 해결하기 위해 임의로 인증된 사용자를 추가하여 API를 테스트하도록 한다. 스프링 시큐리티에서 공식적으로 사용하는 테스트를 위한 의존성을 추가해준다.

testCompile('org.springframework.security:spring-security-test')

이 후 테스트 메서드에 @WithMockUser 어노테이션을 이용하여 사용자 인증을 추가해준다. 하지만 사용자 인증 추가를 위한 어노테이션인 @WithMockUser는 MockMvc에서만 동작하기에 @SpringBootTest에서 @MockMvc를 사용하도록 설정해준다.

어노테이션 및 코드 설명
@Before 매번 테스트가 시작되기 전에 MockMvc 인스턴스를 생성한다.
mvc.perform 생성된 MockMvc를 통해 API를 테스트한다. 본문 영역은 문자열로 표현하기 위해 ObjectMapper를 통해 문자열 JSON으로 변환한다.
package com.springbook.biz.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.springbook.biz.domain.posts.Posts;
import com.springbook.biz.domain.posts.PostsRepository;
import com.springbook.biz.web.dto.PostsSaveRequestDto;
import com.springbook.biz.web.dto.PostsUpdateRequestDto;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.*;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {

    ...

    // MockMvc
    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                .build()
    }

    @Test
    @WithMockUser(roles = "USER")
    public void postSave() throws Exception {
        ...

        // when (MockMvc)
        mvc.perform(post(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(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }

    @Test
    @WithMockUser(roles = "USER")
    public void postsUpdate() throws Exception {
        ...
        
        // when (MockMvc)
        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);
    }
}

4) 문제 (3)

@WebMvcTest를 사용한 테스트 Controller의 경우, CustomOAuth2UserService를 스캔하지 않는다. 스캔하는 대상은 WebSecurityConfigurerAdapter, WebMvcConfigurer를 비롯한 @ControllerAdvice, @Controller를 읽는다. @Repostiory, @Service, @Componet는 스캔 대상이 아니다. SecurityConfig는 읽지만, SecurityConfig를 생성하기 위해 필요한 CustomOAuth2UserService는 읽을 수 없기에 발생한 문제이다. 이를 해결하기 위해서는 SecurityConfig를 제거해주고 사용자 인증을 추가해준다.

package com.springbook.biz.web;

import com.springbook.biz.config.auth.SecurityConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class,
    excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
    })
public class HelloControllerTest {

    ...

    @WithMockUser(roles = "USER")
    @Test
    public void helloReturn() throws Exception {
        ...
    }

    @WithMockUser(roles = "USER")
    @Test
    public void hellodtoReturn() throws Exception {
        ...
    }
}

5) 문제 (4)

'At least one JPA metamodel must be present' 에러의 경우, @EnableJpaAuditing 어노테이션으로 인해 발생한다. @EnableJpaAuditing을 사용하기 위해서는 최소 하나의 @Entity 클래스가 필요하며, @WebMvcTest에는 존재하지 않는다. @EnableJpaAuditing이 @SpringBootApplication과 함께 존재하기에 Application 메인 클래스에서 분리(삭제)하여 config 패키지에 JpaConfig 클래스를 생성하여 @EnableJpaAuditing을 추가한다.

package com.springbook.biz.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {
}

6) 테스트

모든 테스트를 통과하는지 확인해본다.


[참고] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스

728x90