[SpringSecurity] 테스트 코드에 시큐리티 적용하기

Kaite.Kang·2023년 1월 23일
0
post-thumbnail

* 목표

스프링 시큐리티를 적용하고 나면 기존에 작성한 테스트 코드에 에러가 발생한다.

기존에는 바로 API를 호출할 수 있어 테스트 코드 역시 바로 API를 호출하도록 구성하였다.

하지만 시큐리티 옵션이 활성화되면 인증된 사용자만 API를 호출할 수 있기 때문에 에러가 발생하는 것이다.

테스트 코드마다 인증한 사용자가 호출한 것처럼 작동하도록 수정해보자.

1. 302 Status Code

전체 테스트를 수행해보자.

아래 테스트 코드는 모두 302에러가 발생한다.

hello가_리턴된다()

helloDto가_리턴된다()

Posts_등록된다()

Posts_수정된다()

1) 원인

302 Status Code(리다이렉션 응답)는 요청한 리소스가 임시적으로 이동하였음을 나타낸다. 스프링 시큐리티 설정 때문에 인증되지 않은 사용자의 요청은 이동시키기 때문이다.

java.lang.AssertionError: Status expected:<200> but was:<302>
필요:200
실제   :302

2) 코드 수정하기

A. oauth2Login 이용하기

  • build.gradle

스프링 시큐리티 테스트를 위한 여러 도구를 지원하는 spring-security-test 를 build.gradle에 추가한다.

testImplementation("org.springframework.security:spring-security-test")
  • oauth2Login() 추가

hello가_리턴된다(), helloDto가_리턴된다()는 SecurityMockMvcRequestPostProcessors 클래스의 메소드인 oauth2Login()을 이용하면 된다.

해당 클래스는 OAuth2 뿐만 아니라 스프링 시큐리티에서 지원하는 다양한 인증 처리 방식들을 테스트하기 쉽도록 제공한다.

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login;
...

@Test
    public void hello가_리턴된다() throws Exception {
        String hello = "hello";

// 변경 후
        mvc.perform(get("/hello").with(oauth2Login()))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
/* 변경 전
        mvc.perform(get("/hello"))
                 .andExpect(status().isOk())
                .andExpect(content().string(hello));
*/
    }

    @Test
    public void helloDto가_리턴된다() throws Exception {
        String name = "hello";
        int amount = 1000;

// 변경 후
        mvc.perform(get("/hello/dto").with(oauth2Login()).param("name", name).param("amount", String.valueOf(amount)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(name)))
                .andExpect(jsonPath("$.amount", is(amount)));

/* 변경 전
        mvc.perform(get("/hello/dto").param("name", name).param("amount", String.valueOf(amount)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(name)))
                .andExpect(jsonPath("$.amount", is(amount)));
*/

    }
  • 재실행 결과

(해결) hello가_리턴된다(), helloDto가_리턴된다()

B. 테스트 코드에 임시로 사용자 인증 추가해주기

나머지 Posts등록된다(), Posts수정된다() 도 해결해보자.

package com.spring.book.web;

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

    ...

    //MockMvc code start
    @Autowired
    private WebApplicationContext context;
    private MockMvc mvc;

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

    @Test
    @WithMockUser(roles="USER")
    public void 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)
                        .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 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)
                        .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);
    }
}

(1) 임시 사용자 인증 추가

Posts등록된다() 와 Posts수정된다() 에 @WithMockUser(roles="USER") 임시 사용자 인증을 추가하자.

이 어노테이션으로 인해 ROLE_USER 권한을 가진 사용자가 API를 요청하는 것과 동일한 효과를 가진다.

(2) @SpringBootTest에서 MockMvc 사용하기

@WithMockUser는 MockMvc에서만 작동한다. 현재 PostsApiControllerTest 클래스는 @SpringBootTest로만 되어 있으며 MockMvc를 전혀 사용하지 않는다. 코드에 다음 내용을 추가해주자.

  • @Before

    • 매번 테스트가 시작되기 전에 MovkMvc 인스턴스를 생성한다.
  • mvc.perform

    • 생성된 MockMvc를 통해 API를 테스트한다.
    • 본문(Body) 영역은 문자열로 표현하기 위해 ObjectMapper를 통해 문자열 JSON으로 변환한다.
  • 재실행 결과

(해결) Posts등록된다(), Posts수정된다()

2. 405 Status Code

“B. 테스트 코드에 임시로 사용자 인증 추가해주기” 코드 수정 후 405 에러 발생

1) 원인

405 (Http method not supportred) 에러는 컨트롤러에서 동작에 맞는(get,Post, put,delete)는 요청값이나 Url이 없으면 발생하는 에러이다.

게시글 수정하기 동작에서는 put 으로 요청해야 되는데 post로 요청하였다.

구현된 컨트롤러에는 http://localhost:" + port + "/api/v1/posts/" + updateId url에는 put 요청만 받을 수 있다.

2) 코드 수정하기

post → put 으로 동작하니 상태 코드가 200으로 정상적으로 나왔다.

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

Reference

302 에러 참고

405 에러 참고

참고

도서 - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스

0개의 댓글