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

박찬미·2022년 2월 13일
0

Spring Boot

목록 보기
12/17
post-thumbnail

기존 테스트에 시큐리티 적용으로 문제가 되는 부분들이 존재한다.

  • 기존
    바로 API 호출 가능해 테스트 코드 역시 바로 API 호출하도록 구성

  • 시큐리티 옵션 활성화
    인증된 사용자만 API 호출 가능

그래서 기존의 API 테스트 코드들이 모두 인증에 대한 권한을 받지 못했으므로, 테스트 코드마다 인증한 사용자가 호출한 것처럼 작동하도록 수정해야 한다.

클릭하여 전체 테스트를 수행한다.

당근 테스트 실패

실패 원인

  1. CustomOAuth2UserService를 찾을 수 없음

CustomOAuth2UserService를 생성하는데 필요한 소셜 로그인 관련 설정 값들이 없기 때문
=> src/main에서는 설정을 했지만 src/test에서는 설정하지 않았기 때문이다.

원래 테스트 할 때 application.properties는 자동으로 가져온다.(test에 없고, main에 있으면)
그러나, 그 외 application-oauth.properties(소셜 로그인 관련 설정 값 파일)와 같은 파일은 가져오지 않기 때문에 오류가 나는 것

그러니 만들어주겠다. 테스트라서 실제 구글 연동까지 진행할 것은 아니므로 가짜 설정 값을 등록한다.

src/test/resources/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

  1. 302 Status Code

200(정상 응답)이 아닌 302가 온다.
이유는 스프링 시큐리티 설정 때문에 인증되지 않은 사용자의 요청은 이동시키기 때문

임의로 인증된 사용자를 추가해 API만 테스트 해보겠다.

build.gradle에 spring-security-test를 추가한다.

다음으로 PostsApiControllerTest의 2개 테스트 메소드에 임의 사용자 인증을 추가한다.

위 어노테이션은 인증된 가짜 사용자를 만들어 사용하는 것으로 roles에 권한을 추가할 수 있다.
즉, 이 어노테이션을 통해 ROLE_USER 권한을 가진 사용자가 API를 요청하는 것과 동일한 효과

이렇게 해도 모든 테스트를 통과하지 못한다.
@WithMockUser가 MockMvc에서만 작동하기 때문이다.

현재 PostsApiControllerTest는 @SpringBootTest로만 되어있으며 MockMvc를 전혀 사용하지 않는다.

@SpringBootTest에서 MockMvc를 사용하는 방법

  • PostsApiControllerTest
package com.chanmi.book.springboot.web;

import com.chanmi.book.springboot.domain.posts.Posts;
import com.chanmi.book.springboot.domain.posts.PostsRepository;
import com.chanmi.book.springboot.web.dto.PostsSaveRequestDto;
import com.chanmi.book.springboot.web.dto.PostsUpdateRequestDto;
import com.fasterxml.jackson.databind.ObjectMapper;
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.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;
//@SpringBootTest에서 MockMvc를 사용하기 위한 추가
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
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;

    //@WebMvcTest 사용하지 않고
    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    //@SpringBootTest에서 MockMvc를 사용하기 위한 =========
    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    //매번 테스트 시작 전에 MockMvc 인스턴스 생성
    @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 save_Posts() throws Exception{
        //given
        String title = "title";
        String content = "content";

        //데이터를 넣어서 DTO 하나 생성
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

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

        //when, 생성한 Dto가지고 url로 post/Long으로 반환받음
        //ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class); //MockMvc 사용 추가 후 주석 처리
        //MockMvc 사용 추가
        //생성된 MockMvc를 통해 API 테스트, 본문(Body) 영역을 ObjectMapper를 통해 문자열 JSON으로 변환
        mvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());

        //then
        //MockMvc 사용 추가 후 주석 처리
//        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
//        assertThat(responseEntity.getBody()).isGreaterThan(0L);//바디 > 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{
        //근데 내가 빡대갈이라 그러는데, 여기서 Posts 클래스로 만들었네..?왜..?
        //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); //MockMvc 사용 추가 후 주석 처리
        //MockMvc 사용 추가
        //생성된 MockMvc를 통해 API 테스트, 본문(Body) 영역을 ObjectMapper를 통해 문자열 JSON으로 변환
        mvc.perform(put(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(new ObjectMapper().writeValueAsString(requestDto)))
                .andExpect(status().isOk());

        //then
        //MockMvc 사용 추가 후 주석 처리
//        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);
    }
}

추가한 부분과 주석 처리한 부분 주석으로 달아놨다. 이렇게 하고 posts 부분만 돌려보면 테스트 성공


  1. @WebMvcTest에서 CustomOAuth2UserService를 찾을 수 없음

1번과 다른 점은 @WebMvcTest 사용한다는 점
1번을 통해 스프링 시큐리티 설정은 잘 작동했지만, @WebMvcTest는 CustomOAuth2UserService를 스캔하지 않기 때문에 문제 발생

@WebMvcTest는 WebSecurityConfigurerAdapter, WebMvcConfigurer를 비롯한 @ControllerAdvice, @Controller를 읽는다. 즉, @Repository, @Service, @Component는 스캔 대상이 아니기 때문에 SecurityConfig는 읽었지만, SecurityConfig를 생성하기 위해 필요한 CustomOAuth2UserService는 읽을 수가 없어 에러가 발생한 것

문제 해결 : 스캔 대상에서 SecurityConfig 제거

  • HelloControllerTest

기존

변경

@WithMockUser로 가짜 인증된 사용자 생성


근데 이렇게 해도 에러가 발생한다.

이 에러는 Application 클래스의 @EnableJpaAuditing으로 인해 발생
@EnableJpaAuditing을 위해 최소 하나의 @Entity 클래스가 필요하다. 근데 @WebMvcTest라서 당연히 없다.

@EnableJpaAuditing이 @SpringBootApplication와 함꼐 있다보니 @WebMvcTest에서도 스캔한다.
그래서 @EnableJpaAuditing과 @SpringBootApplication 둘을 분리한다.

  • Application.java 에서 어노테이션 제거

  • config 패키지에 JpaConfig 생성 -> @EnableJpaAuditing 추가

    @WebMvcTest는 일반적인 @Configuration을 스캔하지 않는다.



모든 테스트를 성공하였다. 아래 testImplementation 부분은 위 문제를 해결하였는데도 테스트 에러가 나서 build.gradle에 추가한 코드이다.

이제 스프링 시큐리티 적용한 테스트를 수행할 수 있게 되었다.

0개의 댓글