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

Feat/#113 로그 요청 목록 유효성 검사 기능 구현 #115

Merged
merged 6 commits into from
Aug 25, 2024
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
Loading