Skip to content

Commit

Permalink
[BE] feat: 헤더 존재 여부 검증 (#207)
Browse files Browse the repository at this point in the history
* fix: 인터페이스와 구현체 어노테이션 일치

* feat: 헤더 검사 어노테이션

* feat: 헤더 밸리데이터

* feat: 컨트롤러에 헤더 검사

* feat: 헤더 검증 메시지 클라이언트에 전달

* fix: 누락된 `@Valid` 어노테이션 추가

* refactor: 요청이 null인 경우 핸들링

* chore: 테스트 이름 간결하게 변경

* chore: 소문자 컨벤션

* feat: ArgumentResolver를 활용한 헤더 검증

* feat: 공용 패키지로 이동 및 범용적으로 사용할 수 있도록 수정

* chore: 사용하지 않는 import 제거

* feat: ArgumentResolver 적용, Validator 삭제

* chore: 사용하지 않는 커스텀 예외 원복

* refactor: 헤더 존재하지 않는 경우 메시지 수정

* chore: 불필요한 `@Valid` 어노테이션 삭제

* refactor: 변수 추출

Co-authored-by: Yeongseo Na <[email protected]>

* fix: 컴파일 에러 해결

---------

Co-authored-by: Yeongseo Na <[email protected]>
  • Loading branch information
donghoony and nayonsoso authored Aug 6, 2024
1 parent dbeb7c4 commit de21b0d
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 19 deletions.
16 changes: 16 additions & 0 deletions backend/src/main/java/reviewme/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package reviewme.config;

import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import reviewme.global.HeaderPropertyArgumentResolver;

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new HeaderPropertyArgumentResolver());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ public ProblemDetail handleServletRequestBindingException(Exception ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "요청을 읽을 수 없습니다.");
}

@ExceptionHandler({MethodValidationException.class, BindException.class,
TypeMismatchException.class, HandlerMethodValidationException.class})
@ExceptionHandler({
MethodValidationException.class, BindException.class,
TypeMismatchException.class, HandlerMethodValidationException.class
})
public ProblemDetail handleRequestFormatException(Exception ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "요청의 형식이 잘못되었습니다.");
}
Expand Down
18 changes: 18 additions & 0 deletions backend/src/main/java/reviewme/global/HeaderProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package reviewme.global;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface HeaderProperty {

@AliasFor("headerName")
String value() default "";

@AliasFor("value")
String headerName() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package reviewme.global;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import reviewme.global.exception.MissingHeaderPropertyException;

public class HeaderPropertyArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(HeaderProperty.class);
}

@Override
public String resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

HeaderProperty parameterAnnotation = parameter.getParameterAnnotation(HeaderProperty.class);
String headerName = parameterAnnotation.headerName();
String headerProperty = request.getHeader(headerName);

if (headerProperty == null) {
throw new MissingHeaderPropertyException(headerName);
}
return headerProperty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package reviewme.global.exception;

public class MissingHeaderPropertyException extends BadRequestException {

public MissingHeaderPropertyException(String headerName) {
super("요청에 %s이(가) 존재하지 않아요.".formatted(headerName));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package reviewme.review.controller;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.net.URI;
import lombok.RequiredArgsConstructor;
Expand All @@ -11,6 +10,7 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reviewme.global.HeaderProperty;
import reviewme.review.dto.request.CreateReviewRequest;
import reviewme.review.dto.response.ReceivedReviewsResponse;
import reviewme.review.dto.response.ReviewDetailResponse;
Expand All @@ -32,8 +32,9 @@ public ResponseEntity<Void> createReview(@Valid @RequestBody CreateReviewRequest
}

@GetMapping("/reviews")
public ResponseEntity<ReceivedReviewsResponse> findReceivedReviews(HttpServletRequest request) {
String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER);
public ResponseEntity<ReceivedReviewsResponse> findReceivedReviews(
@HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode
) {
ReceivedReviewsResponse response = reviewService.findReceivedReviews(groupAccessCode);
return ResponseEntity.ok(response);
}
Expand All @@ -45,9 +46,9 @@ public ResponseEntity<ReviewSetupResponse> findReviewCreationSetup(@RequestParam
}

@GetMapping("/reviews/{id}")
public ResponseEntity<ReviewDetailResponse> findReceivedReviewDetail(@PathVariable long id,
HttpServletRequest request) {
String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER);
public ResponseEntity<ReviewDetailResponse> findReceivedReviewDetail(
@PathVariable long id,
@HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode) {
ReviewDetailResponse response = reviewService.findReceivedReviewDetail(groupAccessCode, id);
return ResponseEntity.ok(response);
}
Expand Down

This file was deleted.

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package reviewme.global;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.NativeWebRequest;
import reviewme.global.exception.MissingHeaderPropertyException;

class HeaderPropertyArgumentResolverTest {

private final HeaderPropertyArgumentResolver resolver = new HeaderPropertyArgumentResolver();
private final MethodParameter parameter = mock(MethodParameter.class);
private final HeaderProperty headerProperty = mock(HeaderProperty.class);

@BeforeEach
void setUp() {
given(parameter.hasParameterAnnotation(HeaderProperty.class)).willReturn(true);
given(parameter.getParameterAnnotation(HeaderProperty.class)).willReturn(headerProperty);
}

@Test
void 검증값이_헤더에_존재하지_않으면_검증에_실패한다() {
// given
NativeWebRequest request = mock(NativeWebRequest.class);
given(request.getNativeRequest()).willReturn(new MockHttpServletRequest());
given(headerProperty.headerName()).willReturn("test");

// when, then
assertThatThrownBy(() -> resolver.resolveArgument(parameter, null, request, null))
.isInstanceOf(MissingHeaderPropertyException.class);
}

@Test
void 검증값이_헤더에_존재하면_값을_반환한다() {
// given
String headerName = "test";
String headerValue = "1234";
NativeWebRequest request = mock(NativeWebRequest.class);
MockHttpServletRequest mockRequest = (new MockHttpServletRequest());
mockRequest.addHeader(headerName, headerValue);
given(request.getNativeRequest()).willReturn(mockRequest);
given(headerProperty.headerName()).willReturn(headerName);

// when
String actual = resolver.resolveArgument(parameter, null, request, null);

// then
assertThat(actual).isEqualTo(headerValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import reviewme.review.repository.ReviewContentRepository;
import reviewme.review.repository.ReviewKeywordRepository;
import reviewme.review.repository.ReviewRepository;
import reviewme.review.service.exception.InvalidGroupAccessCodeException;
import reviewme.reviewgroup.domain.ReviewGroup;
import reviewme.reviewgroup.repository.ReviewGroupRepository;
import reviewme.support.ServiceTest;
Expand Down

0 comments on commit de21b0d

Please sign in to comment.