Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 헤더 존재 여부 검증 #207

Merged
merged 19 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.converter.HttpMessageNotReadableException;
Expand Down Expand Up @@ -74,8 +75,7 @@ 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})
public ProblemDetail handleRequestFormatException(Exception ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "요청의 형식이 잘못되었습니다.");
}
Expand Down Expand Up @@ -103,4 +103,14 @@ public ProblemDetail handleMethodArgumentNotValid(MethodArgumentNotValidExceptio
problemDetail.setProperties(properties);
return problemDetail;
}

@ExceptionHandler(HandlerMethodValidationException.class)
public ProblemDetail handleHandlerMethodValidationException(HandlerMethodValidationException ex) {
String message = ex.getAllErrors()
.stream()
.map(MessageSourceResolvable::getDefaultMessage)
.toList()
.get(0);
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

권한에 해당하는 헤더 정보가 없이 온 경우에 대해서 HandlerMethodValidationException를 내려주는 게 적절한지 궁금합니다!
권한에 해당하는 헤더 정보가 없는 경우는 인증의 문제 느낌이 있기도 해서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원복했습니다 👍🏻

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -19,7 +20,7 @@ public interface ReviewApi {
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "리뷰 등록 요청 성공")
})
ResponseEntity<Void> createReview(@RequestBody CreateReviewRequest request);
ResponseEntity<Void> createReview(@Valid @RequestBody CreateReviewRequest request);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 api 문서에도 valid 를 다신건지.. 궁금헙니다.. <--@

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 없으니까 안 돌아가던데요 !?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭐지 없어도 되었네요..? 🤔 Validator 추가할 때 어딘가 꼬였나봐요, 다시 돌려놓겠습니다 👍🏻


@Operation(summary = "리뷰 작성 정보 조회", description = "리뷰 생성 시 필요한 정보를 조회한다.")
@ApiResponses(value = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reviewme.review.controller.validator.ContainsHeaderName;
import reviewme.review.dto.request.CreateReviewRequest;
import reviewme.review.dto.response.ReceivedReviewsResponse;
import reviewme.review.dto.response.ReviewDetailResponse;
Expand All @@ -32,7 +33,9 @@ public ResponseEntity<Void> createReview(@Valid @RequestBody CreateReviewRequest
}

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

@GetMapping("/reviews/{id}")
public ResponseEntity<ReviewDetailResponse> findReceivedReviewDetail(@PathVariable long id, HttpServletRequest request) {
public ResponseEntity<ReviewDetailResponse> findReceivedReviewDetail(
@PathVariable long id,
@ContainsHeaderName(GROUP_ACCESS_CODE_HEADER) HttpServletRequest request
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

범용적으로 사용할 것을 대비해 @ContainsHeaderName이라고 지었어요. 만약 단순 헤더 이름이 GroupAccessCode였다면 그렇게도 할 수 있었겠지만.. 이 부분은 따로 빼서 값을 넣어주는 식으로 하는 게 나아 보였습니다.

) {
String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER);
ReviewDetailResponse response = reviewService.findReceivedReviewDetail(groupAccessCode, id);
return ResponseEntity.ok(response);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package reviewme.review.controller.validator;

import jakarta.validation.Constraint;
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)
@Constraint(validatedBy = ContainsHeaderValidator.class)
public @interface ContainsHeaderName {

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

@AliasFor("value")
String headerName() default "";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AliasFor는 어노테이션 내부에 headerName=을 적지 않고 바로 적용할 수 있도록 합니다. 기본적으로 아무것도 적지 않으면 value와 같은데, 이를 매핑한 것이라고 보면 돼요.


String message() default "필수 값이 헤더에 존재하지 않아요";

Class<?>[] groups() default {};

Class<?>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package reviewme.review.controller.validator;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class ContainsHeaderValidator implements ConstraintValidator<ContainsHeaderName, HttpServletRequest> {

private String headerName;

@Override
public void initialize(ContainsHeaderName constraintAnnotation) {
this.headerName = constraintAnnotation.headerName();
}

@Override
public boolean isValid(HttpServletRequest request, ConstraintValidatorContext context) {
return request.getHeader(headerName) != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package reviewme.review.controller.validator;

import static org.assertj.core.api.Assertions.assertThat;
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.mock.web.MockHttpServletRequest;

class ContainsHeaderNameValidatorTest {

private final ContainsHeaderValidator validator = new ContainsHeaderValidator();
private final ContainsHeaderName validationAnnotation = mock(ContainsHeaderName.class);

@BeforeEach
void setUp() {
given(validationAnnotation.headerName()).willReturn("test");
validator.initialize(validationAnnotation);
}

@Test
void 헤더에_값이_존재하지_않는_경우_검증에_실패한다() {
// Given
MockHttpServletRequest request = new MockHttpServletRequest();
validator.initialize(validationAnnotation);

// When
boolean result = validator.isValid(request, null);

// Then
assertThat(result).isFalse();
}

@Test
void 헤더에_값이_존재하는_경우_검증에_성공한다() {
// given
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("test", "value");

// when
boolean result = validator.isValid(request, null);

// then
assertThat(result).isTrue();
}
}