[Spring REST API #5] Bad Request 처리

입력값이 이상한 경우 테스트 방법 즉, Dto의 값이 비어있을 경우 Bad Request 처리

  • @Valid : Request에 들어있느 값들을 Dto에 바인딩할 때 검증을 수행할 수 있다.
package org.kyhslam.rest;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Builder @NoArgsConstructor @AllArgsConstructor
@Data
public class EventDto {

    private String name;
    private String description;
    private LocalDateTime beginEnrollmentDateTime;
    private LocalDateTime closeEnrollmentDateTime;
    private LocalDateTime beginEventDateTime;
    private LocalDateTime endEventDateTime;
    private String location; // (optional)
    private int basePrice; // (optional)
    private int maxPrice; // (optional)
    private int limitOfEnrollment;
}
package org.kyhslam.rest;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.net.URI;

@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {

    @Autowired
    EventRepository eventRepository;

    @Autowired
    ModelMapper modelMapper;

    @PostMapping
    public ResponseEntity createEvent(@RequestBody EventDto eventDto){
        Event event = modelMapper.map(eventDto, Event.class); // Dto를 event로 변환
        Event newEvent = eventRepository.save(event);
        URI createdUri = ControllerLinkBuilder.linkTo(EventController.class).slash(newEvent.getId()).toUri();
        return ResponseEntity.created(createdUri).body(event);
    }

}

테스트

package org.kyhslam.rest;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import java.time.LocalDateTime;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class EventControllerTests {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void createEvent_Bad_Request_Empty_input() throws Exception {

        EventDto eventDto = EventDto.builder().build();

        this.mockMvc.perform(MockMvcRequestBuilders.post("/api/events")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content(this.objectMapper.writeValueAsString(eventDto)))
                .andExpect(MockMvcResultMatchers.status().isBadRequest());

    }

}

입력값이 아무것도 없기 때문에 아래와 같은 201에러가 발생한다

결과

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/events
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]}
             Body = {"name":null,"description":null,"beginEnrollmentDateTime":null,"closeEnrollmentDateTime":null,"beginEventDateTime":null,"endEventDateTime":null,"location":null,"basePrice":0,"maxPrice":0,"limitOfEnrollment":0}
    Session Attrs = {}

Handler:
             Type = org.kyhslam.rest.EventController
           Method = public org.springframework.http.ResponseEntity org.kyhslam.rest.EventController.createEvent(org.kyhslam.rest.EventDto)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 201
    Error message = null
          Headers = {Location=[http://localhost/api/events/1], Content-Type=[application/hal+json;charset=UTF-8]}
     Content type = application/hal+json;charset=UTF-8
             Body = {"id":1,"name":null,"description":null,"beginEnrollmentDateTime":null,"closeEnrollmentDateTime":null,"beginEventDateTime":null,"endEventDateTime":null,"location":null,"basePrice":0,"maxPrice":0,"limitOfEnrollment":0,"offline":false,"free":false,"eventStatus":"DRAFT"}
    Forwarded URL = null
   Redirected URL = http://localhost/api/events/1
          Cookies = []

java.lang.AssertionError: Status 
Expected :400
Actual   :201
<Click to see difference>

입력값 제한을 위해 어떻게 처리를 해야하는가?

Dto와 Controller를 아래와 같이 변경한다

package org.kyhslam.rest;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

@Builder @NoArgsConstructor @AllArgsConstructor
@Data
public class EventDto {

    @NotEmpty
    private String name;
    @NotEmpty
    private String description;
    @NotNull
    private LocalDateTime beginEnrollmentDateTime;
    @NotNull
    private LocalDateTime closeEnrollmentDateTime;
    @NotNull
    private LocalDateTime beginEventDateTime;
    @NotNull
    private LocalDateTime endEventDateTime;

    private String location; // (optional)
    @Min(0)
    private int basePrice; // (optional)
    @Min(0)
    private int maxPrice; // (optional)
    @Min(0)
    private int limitOfEnrollment;
}

 입력값이 없을 경우를 체크할 경우, @NotEmpty, @NotNull 어노테이션을 붙여서 이 프로퍼티가 값을 꼭 가져가야한다는 것을 명시한다. 또한, basePrice, maxPrice 같은 정수값을 산정할 때 최소값 제한을 두어 ( @Min(0) ) 해당 프로퍼티에 잘못된 값이 들어오는 것을 방지할 수 있다.

@PostMapping
    public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){
        if(errors.hasErrors()){
            return ResponseEntity.badRequest().build();
        }
        Event event = modelMapper.map(eventDto, Event.class); // Dto를 event로 변환
        Event newEvent = eventRepository.save(event);
        URI createdUri = ControllerLinkBuilder.linkTo(EventController.class).slash(newEvent.getId()).toUri();
        return ResponseEntity.created(createdUri).body(event);
    }
  • EventDto에 @Valid 어노테이션을 붙이면 Request에 담겨진 데이터가 eventDto에 바인딩 될때 EventDto에 정의한 @NotNull 등 과 같은 정의들을 검사한다.
  • 그리고 만약 에러가 발생한다면 그 옆에 정의한 Errors의 객체에 에러들의 내용들이 정의되어진다.

댓글

Designed by JB FACTORY