Justin의 개발 로그
article thumbnail

구글, 네이버 로그인 구현을 완료했고

기존 테스트에 시큐리티 적용으로

문제가 되는 것을 해결한다.

 

 

기존 테스트 코드에서는

API를 바로 호출하도록 구성되어 있었다.

 

그러나 시큐리티 옵션이 활성화되면

인증된 사용자만이 API를 호출할 수 있다.

 

기존 API 테스트 코드들은 인증에 대한 권한을 받지 못해

테스트 코드마다 인증한 사용자가 호출한 것처럼

작동할 수 있도록 수정해야 한다.

 

 

우선 전체 테스트를 수행해본다.

 

전체 테스트 수행하기

 

 

인텔리제이 화면 우측의

Gradle 탭을 누른 뒤

Tasks - verification - test를 차례로 선택한 뒤

전체 테스트를 수행한다.

 

 

전체 테스트를 수행하면 위와 같은 결과가 나온다.

 

문제 1. Test 환경 구성

 

src/main과 src/test의 환경은

독자적인 환경 구성을 갖는다.

 

src/main/resources/application.properties가

테스트 코드를 수행할 때에도 적용되는데

test에 application.properties가 없는 경우

main의 설정을 그대로 반영한다.

 

그러나, 자동으로 반영하는 범위는

application.properties까지이고

application.oauth.properties는 test에 파일이 없어도

가져오는 파일이 아니다.

 

이 문제를 해결하기 위해 테스트 환경을 위한

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

 

문제 2. 302 Status Code

 

registerd_Posts의 로그를 확인하면

다음과 같은 실패 로그를 볼 수 있다.

 

302 Status Code

 

200(정상 응답)을 원했지만

실제로 302(redirection 응답)가 도착해 실패했다.

 

이는 스프링 시큐리티 설정 때문에

인증되지 않은 사용자의 요청을 이동시키기 때문이다.

 

이런 API 요청은

임의로 인증된 사용자를 추가해

API만 테스트할 수 있게 설정한다.

 

스프링 시큐리티 테스트를 위한

여러 도구를 지원하는 spring-security-test를

build.gradle에 추가해 준다.

 

build.gradle

 

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

 

그리고 PostsApiControllerTest의 

2개의 테스트 메소드에

임의 사용자 인증을 추가한다.

 

PostsApiControllerTest.java

 

@Test
@WithMockUser(roles = "USER")
public void registered_Posts() throws Exception{
	// 생략
}

@Test
@WithMockUser(roles = "USER")
public void update_Posts() throws Exception{
	// 생략
}

 

@WithMockUser(roles = "USER")

인증된 임의의 사용자를 만들어 사용.

rolse = ""에 권한을 추가할 수 있다.

 

여기까지 코드를 작성했을 때

테스트 코드는 아직 작동하지 않는다.

권한을 주는 어노테이션이 MockMvc에서만 작동하기 때문이다.

 

PostsApiController는 @SpringBootTest로 되어 있고

MockMvc를 사용하지 않는다.

 

MockMvc를 사용하기 위해

코드를 수정해 준다.

 

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.springframework.http.MediaType;
import com.mieumje.springboot.domain.posts.Posts;
import com.mieumje.springboot.domain.posts.PostsRepository;
import com.mieumje.springboot.web.dto.PostsSaveRequestDto;
import com.mieumje.springboot.web.dto.PostsUpdateRequestDto;
import org.junit.After;
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.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.MockMvcBuilder;
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 {
    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

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

    @After
    public void tearDown() throws Exception{
        postsRepository.deleteAll();
    }
    // 등록 기능
    @Test
    @WithMockUser(roles = "USER")
    public void registered_Posts() throws Exception{
        //given
        String title = "title";
        String content = "content";
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

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

        //when
        //ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);
        mvc.perform(post(url)
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());

        //then
        //assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        //assertThat(responseEntity.getBody()).isGreaterThan(0L);
        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 update_Posts() throws Exception{
        //given
        Posts savedPosts = postsRepository.save(Posts.builder()
            .title("title")
            .content("content")
            .author("author")
            .build());

        Long updateId = 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/" + updateId;

        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

        //when
        //ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,requestEntity,Long.class);
        mvc.perform(put(url)
            .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());


        //then
        //assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        //assertThat(responseEntity.getBody()).isGreaterThan(0L);
        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(expectedContent);

    }
}

 

@Before

테스트가 시작되기 전에

MockMvc 인스턴스를 생성.

 

mvc.perform

생성된 MockMvc를 통해 API를 테스트.

 

그리고 다시 Gradle로 테스트를 수행한다.

 

Posts 테스트도

테스트를 통과한 것을 볼 수 있다.

 

 

문제 3. @WebMvcTest에서 CustomOAuth2UserService를 찾을 수 없다.

 

return_hello 테스트를 확인해보면

No qualifying bean of type 'com.mieumje.springboot.config.auth.CustomOAuth2UserService

메시지가 나오는 것을 확인할 수 있다.

 

 

 

문제 1에서 스프링 시큐리티 설정은 했지만

@WebMvcTest는 CustomOAuth2UserService를

스캔하지 않아 발생하는 메시지이다.

 

@WebMvcTest는

@ControllerAdvice, @Controller를 읽고

@Repository, @Service, @Component는 스캔 대상이 아니다.

 

SecurityConfig는 읽지만,

SecufityConfig를 생성하기 위한

CustomOAuth2UserService를 읽을 수 없어 에러가 발생하는 것이다.

 

이 문제를 해결하기 위해

스캔 대상에서 SecurityConfig를 제거해 준다.

 

HelloControllerTest.java

 

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

)

 

그리고 마찬가지로

@WithMockUser(roles = "USER")를 통해

가짜로 인증된 사용자를 추가한다.

 

public class HelloControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    @WithMockUser(roles = "USER")
    public void return_hello() throws Exception{
        // 생략
    }

    @Test
    @WithMockUser(roles = "USER")
    public void return_hello_dto() throws Exception{
        //생략
    }
}

 

다시 테스트를 진행하면

다음과 같은 추가 에러가 발생한다.

 

 

@EnableJpaAuditing으로 인해 발생한 것인데

이를 사용하기 위해서는

최소한 하나의 @Entity 클래스가 필요하다.

 

@WebMvcTest라서 @Entity 클래스가 없는 것이다.

 

@EnableJpaAuditing가 @SpringBootApplication과 함께 있어

@WebMvcTest에서도 스캔하게 되었다. 

그래서 이 둘을 분리해주도록 한다.

 

Application.java에서

@EnableJpaAuditing을 제거한다.

 

Application.java

 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

// @EnableJpaAuditing //JPA Auditing 제거
@SpringBootApplication
public class Application {
    public static void main(String[] args){
        SpringApplication.run(Application.class, args);
    }
}

 

그리고 confing 패키지에

JpaConfig를 생서해

@EnableJpaAuditing을 추가해 준다.

 

JpaConfig.java 위치

 

JpaConfig.java

 

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

@Configuration
@EnableJpaAuditing // JPA Auditing 활성화, Application.java에서 활성화 하던 것을 옮김
public class JpaConfig {
}

 

그리고 다시 전체 테스트를 진행하면

모든 테스트가 통과된 것을 볼 수 있다.

 

 

profile

Justin의 개발 로그

@라이프노트

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!