Skip to content

Commit

Permalink
Feat/#113 로그 요청 목록 유효성 검사 기능 구현 (#115)
Browse files Browse the repository at this point in the history
* feat: 로그 요청 유효성 검사를 위한 커스텀 어노테이션 추가

- ValidLogRequests 어노테이션 생성

* feat: 로그 요청 목록 유효성 검사 기능 구현

- LogRequestsValidator 클래스 생성
- 빈 요청, 개별 요청, 전체 요청의 유효성 검사 로직 구현
- 유효하지 않은 요청 필터링 및 오류 메시지 생성 기능 추가

* test: 여러 로그 POST 요청에 대한 테스트 케이스 추가

- 빈 요청 시 400 에러 반환 테스트
- 모든 요청 무효 시 400 에러 반환 테스트
- 일부 요청만 유효한 경우 정상 처리 테스트
- 전체 요청 유효 시 정상 처리 테스트

* test: 빈 줄을 허용하는 비즈니스 로직 변화로 테스트 코드 수정

- data가 빈 줄일 때에 대한 예외상황을 허용하도록 변경

* feat: LogController에 로그 요청 유효성 검사 적용

- @ValidLogRequests 어노테이션을 LogController의 saveLogs 메서드에 적용

* refactor: indexOf 연산을 사용하지 않기 위한 리팩토링

- requests로 stream을 돌리지 않고 ,IntStream을 통해 index 기반 접근하여 반환하도록 수정했습니다.
  • Loading branch information
miiiinju1 authored Aug 25, 2024
1 parent 47a3234 commit ee5a8f4
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import info.logbat.dev.aop.CountTest;
import info.logbat.domain.log.application.LogService;
import info.logbat.domain.log.presentation.payload.request.CreateLogRequest;
import jakarta.validation.Valid;
import info.logbat.domain.log.presentation.validation.ValidLogRequests;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -27,8 +27,10 @@ public class LogController {
@ResponseStatus(HttpStatus.CREATED)
public void saveLogs(
@RequestHeader("App-Key") @NotBlank(message = "appKey가 비어있습니다.") String appKey,
@Valid @RequestBody List<CreateLogRequest> request) {
logService.saveLogs(appKey, request);
@RequestBody @ValidLogRequests List<CreateLogRequest> requests) {

logService.saveLogs(appKey, requests);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package info.logbat.domain.log.presentation.validation;

import info.logbat.domain.log.presentation.payload.request.CreateLogRequest;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
* 로그 요청 목록의 유효성을 검사하는 밸리데이터 클래스입니다. 빈 요청, 개별 요청의 유효성, 전체 요청의 유효성을 검사합니다.
*/
@Component
@RequiredArgsConstructor
public class LogRequestsValidator implements
ConstraintValidator<ValidLogRequests, List<CreateLogRequest>> {

private final Validator validator;

/**
* 로그 요청 목록의 유효성을 검사합니다.
*
* @param requests 검사할 로그 요청 목록
* @param context 제약 조건 컨텍스트
* @return 유효성 검사 결과 (true: 유효, false: 무효)
*/
@Override
public boolean isValid(List<CreateLogRequest> requests, ConstraintValidatorContext context) {
if (isEmptyRequest(requests, context)) {
return false;
}

List<String> errorMessages = new ArrayList<>();
List<CreateLogRequest> validRequests = filterValidRequests(requests, errorMessages);

updateRequestsList(requests, validRequests);

if (requests.isEmpty()) {
addErrorMessage(context, String.join("\n", errorMessages));
return false;
}

return true;
}

/**
* 요청 목록이 비어있는지 확인합니다.
*
* @param requests 검사할 요청 목록
* @param context 제약 조건 컨텍스트
* @return 비어있으면 true, 그렇지 않으면 false
*/
private boolean isEmptyRequest(List<CreateLogRequest> requests,
ConstraintValidatorContext context) {
if (requests == null || requests.isEmpty()) {
addErrorMessage(context, "빈 요청이 전달되었습니다.");
return true;
}
return false;
}

/**
* 유효한 요청만 필터링합니다.
*
* @param requests 전체 요청 목록
* @param errorMessages 오류 메시지를 저장할 리스트
* @return 유효한 요청 목록
*/
private List<CreateLogRequest> filterValidRequests(List<CreateLogRequest> requests,
List<String> errorMessages) {

return IntStream.range(0, requests.size())
.filter(index -> isValidRequest(requests.get(index), index + 1, errorMessages))
.mapToObj(requests::get)
.toList();
}

/**
* 개별 요청의 유효성을 검사합니다.
*
* @param request 검사할 요청
* @param index 요청의 인덱스
* @param errorMessages 오류 메시지를 저장할 리스트
* @return 유효하면 true, 그렇지 않으면 false
*/
private boolean isValidRequest(CreateLogRequest request, int index,
List<String> errorMessages) {
Set<ConstraintViolation<CreateLogRequest>> violations = validator.validate(request);
if (!violations.isEmpty()) {
String message = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
errorMessages.add("Request " + index + ": " + message);
return false;
}
return true;
}

/**
* 요청 목록을 유효한 요청만으로 갱신합니다.
*
* @param requests 원본 요청
* @param validRequests 유효한 요청 목록
*/
private void updateRequestsList(List<CreateLogRequest> requests,
List<CreateLogRequest> validRequests) {
requests.clear();
requests.addAll(validRequests);
}

/**
* 사용자 정의 제약 조건 위반을 추가합니다.
*
* @param context 제약 조건 컨텍스트
* @param message 제약 조건 위반 메시지
*/
private void addErrorMessage(ConstraintValidatorContext context, String message) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addConstraintViolation();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package info.logbat.domain.log.presentation.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = LogRequestsValidator.class)
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidLogRequests {

String message() default "유효하지 않은 로그 요청입니다.";

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

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,78 @@ class LogControllerTest extends ControllerTestSupport {
new CreateLogRequest(expectedLogLevel, expectedLogData, expectedLogTimestamp)
);

@Nested
@DisplayName("여러 개 로그에 대한 POST 요청에 대해")
class describeMultipleLogsPost {

@Test
@DisplayName("빈 요청이 전달되면 400 에러를 반환한다.")
void willReturn400IfEmptyRequest() throws Exception {
// Arrange
MockHttpServletRequestBuilder post = post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("App-Key", EXPECTED_APP_KEY_STRING)
.content(objectMapper.writeValueAsString(List.of()));

// Act & Assert
mockMvc.perform(post)
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("모든 요청에 예외가 발생하면 400 에러를 반환한다.")
void willReturn400IfAllRequestsAreInvalid() throws Exception {
// Arrange
List<CreateLogRequest> expectedWrongRequest = List.of(
new CreateLogRequest(null, null, null),
new CreateLogRequest(null, null, null)
);

MockHttpServletRequestBuilder post = post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("App-Key", EXPECTED_APP_KEY_STRING)
.content(objectMapper.writeValueAsString(expectedWrongRequest));

// Act & Assert
mockMvc.perform(post)
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("일부 요청만 정상적인 경우는 정상적으로 처리한다.")
void willSaveLogsIfSomeRequestsAreValid() throws Exception {
// Arrange
List<CreateLogRequest> expectedWrongRequest = List.of(
new CreateLogRequest(expectedLogLevel, expectedLogData, expectedLogTimestamp),
new CreateLogRequest(null, null, null)
);

MockHttpServletRequestBuilder post = post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("App-Key", EXPECTED_APP_KEY_STRING)
.content(objectMapper.writeValueAsString(expectedWrongRequest));

// Act & Assert
mockMvc.perform(post)
.andExpect(status().isCreated());
}

@Test
@DisplayName("전체 요청이 정상적인 경우는 정상적으로 처리한다.")
void willSaveLogsIfAllRequestsAreValid() throws Exception {
// Arrange
MockHttpServletRequestBuilder post = post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("App-Key", EXPECTED_APP_KEY_STRING)
.content(objectMapper.writeValueAsString(expectedRequests));

// Act & Assert
mockMvc.perform(post)
.andExpect(status().isCreated());
}

}

@Nested
@DisplayName("POST 요청에 대해")
class describePost {
Expand Down Expand Up @@ -82,7 +154,6 @@ private static Stream<Arguments> invalidLogCreationInputs() {
Arguments.of(null, "테스트_로그_데이터", LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of(" ", "테스트_로그_데이터", LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("INFO", null, LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("INFO", " ", LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("INFO", "테스트_로그_데이터", null)
);
}
Expand Down

0 comments on commit ee5a8f4

Please sign in to comment.