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/#31 Log를 저장하는 API 구현 #34

Merged
merged 15 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions logbat/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ dependencies {
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// SpringBoot Validation
implementation 'org.springframework.boot:spring-boot-starter-validation:3.3.1'
// MySQL Connector
runtimeOnly 'com.mysql:mysql-connector-j'
// SpringBoot Test
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package info.logbat.domain.log.application;

import info.logbat.domain.log.application.payload.request.CreateLogServiceRequest;
import info.logbat.domain.log.domain.Log;
import info.logbat.domain.log.repository.LogRepository;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogService {

private final LogRepository logRepository;

public long saveLog(CreateLogServiceRequest request) {
Long appId = request.applicationId();
String logLevel = request.logLevel();
String logData = request.logData();
LocalDateTime timestamp = request.timestamp();
// TODO Log 저장 전 Application ID 체크 로직 추가 필요

Log log = Log.of(appId, logLevel, logData, timestamp);

return logRepository.save(log);
}
}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package info.logbat.domain.log.application.payload.request;

import info.logbat.domain.log.presentation.payload.request.CreateLogRequest;
import java.time.LocalDateTime;

public record CreateLogServiceRequest(
Long applicationId,
String logLevel,
String logData,
LocalDateTime timestamp
) {

public static CreateLogServiceRequest of(Long applicationId, CreateLogRequest request) {
return new CreateLogServiceRequest(
applicationId,
request.logLevel(),
request.logData(),
request.timestamp()
);
}
}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 5 additions & 2 deletions logbat/src/main/java/info/logbat/domain/log/domain/Log.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package info.logbat.domain.log.domain;

import info.logbat.domain.log.domain.values.LogData;
import info.logbat.domain.log.domain.enums.Level;

import info.logbat.domain.log.domain.values.LogData;
import java.time.LocalDateTime;
import lombok.Getter;

Expand All @@ -15,6 +14,10 @@ public class Log {
private final LogData logData;
private final LocalDateTime timestamp;

public static Log of(Long applicationId, String level, String logData, LocalDateTime timestamp) {
return new Log(applicationId, level, logData, timestamp);
}

public Log(Long applicationId, String level, String logData, LocalDateTime timestamp) {
this(null, applicationId, level, logData, timestamp);
}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package info.logbat.domain.log.presentation;

import info.logbat.domain.log.application.LogService;
import info.logbat.domain.log.application.payload.request.CreateLogServiceRequest;
import info.logbat.domain.log.presentation.payload.request.CreateLogRequest;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/logs")
@RequiredArgsConstructor
public class LogController {

private final LogService logService;

@PostMapping
public ResponseEntity<Void> saveLog(
@RequestHeader("app_id")
@NotNull(message = "Application ID가 비어있습니다.")
@Positive(message = "Application ID는 양수여야 합니다.") Long applicationId,
Copy link
Member

Choose a reason for hiding this comment

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

어플리케이션 ID를 어떤 형식으로 해야할지 더 논의해보면 좋겠어요!
난수 String이 더 안전하지 않을까 생각이 듭니다 🥹


@Valid @RequestBody CreateLogRequest request
) {

logService.saveLog(CreateLogServiceRequest.of(applicationId, request));

return ResponseEntity.ok()
.build();
}

}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package info.logbat.domain.log.presentation.payload.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;

public record CreateLogRequest(
@NotBlank(message = "로그 레벨이 비어있습니다.")
String logLevel,

@NotBlank(message = "로그 데이터가 비어있습니다.")
String logData,

@NotNull(message = "타임스탬프가 비어있습니다.")
LocalDateTime timestamp
) {

}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
Expand All @@ -14,7 +14,7 @@
import org.springframework.stereotype.Repository;

@Repository
@AllArgsConstructor
@RequiredArgsConstructor
public class LogRepository {

private final JdbcTemplate jdbcTemplate;
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Copy link
Member

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
@@ -0,0 +1,35 @@
package info.logbat.domain.common;

import com.fasterxml.jackson.databind.ObjectMapper;
import info.logbat.domain.log.application.LogService;
import info.logbat.domain.log.presentation.LogController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

/**
* 이후 테스트 하려면 Controller에 대해 controllers에 추가하고, ControllerTestSupport를 상속받아 테스트를 진행하시면 됩니다.
*/
@WebMvcTest(controllers = {
LogController.class
}
)
@ActiveProfiles("test")
public abstract class ControllerTestSupport {

@Autowired
protected MockMvc mockMvc;

@Autowired
protected ObjectMapper objectMapper;

@MockBean
protected LogService logService;

/**
* 이후 필요한 서비스에 대해 MockBean을 추가하여 테스트를 진행하시면 됩니다.
*/

}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package info.logbat.domain.log.application;

import static org.assertj.core.api.Assertions.assertThat;

import info.logbat.domain.log.application.payload.request.CreateLogServiceRequest;
import info.logbat.domain.log.domain.Log;
import info.logbat.domain.log.domain.enums.Level;
import info.logbat.domain.log.repository.LogRepository;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
Copy link
Member

Choose a reason for hiding this comment

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

@WebMVCTest vs @SpringBootTest

Copy link
Member Author

Choose a reason for hiding this comment

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

Service 레이어는 DB붙여서 테스트하는 것이 더 좋다고 생각해서 @SpringBootTest를 이용해야 하지 않을까 생각됩니다!!

@Transactional
@ActiveProfiles("test")
@DisplayName("LogService 테스트")
class LogServiceTest {

@Autowired
private LogService logService;

@Autowired
private LogRepository logRepository;
Copy link
Member

Choose a reason for hiding this comment

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

이부분도 서비스 테스트에서 데이터베이스는 모킹 안되어 있는데 어떤 방식으로 하면 좋을지 더 논의해봐요!
repository layer는 저도 이렇게 주입받아 쓰는면이 유지보수도 용이하고 테스트 컨테이너를 사용할 예정이라 좋아보여요!


@DisplayName("Log를 저장할 수 있다.")
@Test
void saveLog() {
// given
Long 어플리케이션_ID = 1L;
String 로그_레벨 = "INFO";
String 로그_데이터 = "테스트_로그_데이터";
LocalDateTime 타임스탬프 = LocalDateTime.of(2021, 1, 1, 0, 0, 0);

CreateLogServiceRequest 요청_DTO = new CreateLogServiceRequest(어플리케이션_ID, 로그_레벨, 로그_데이터, 타임스탬프);

// when
long 저장된_ID = logService.saveLog(요청_DTO);

// then
Optional<Log> 찾은_로그 = logRepository.findById(저장된_ID);

assertThat(찾은_로그).isPresent()
.get()
.extracting("logId", "applicationId", "level", "logData.value", "timestamp")
.contains(저장된_ID, 1L, Level.INFO, "테스트_로그_데이터", 타임스탬프);
}

}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package info.logbat.domain.log.presentation;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import info.logbat.domain.common.ControllerTestSupport;
import info.logbat.domain.log.presentation.payload.request.CreateLogRequest;
import java.time.LocalDateTime;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultActions;

class LogControllerTest extends ControllerTestSupport {

@DisplayName("[POST] 로그를 정상적으로 생성한다.")
@Test
void createLog() throws Exception {
// given
Long 어플리케이션_ID = 1L;
String 로그_레벨 = "INFO";
String 로그_데이터 = "테스트_로그_데이터";
LocalDateTime 타임스탬프 = LocalDateTime.of(2021, 1, 1, 0, 0, 0);

CreateLogRequest 요청_DTO = new CreateLogRequest(로그_레벨, 로그_데이터, 타임스탬프);

when(logService.saveLog(any()))
.thenReturn(1L);

// when
ResultActions perform = mockMvc.perform(post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("app_id", 어플리케이션_ID)
.content(objectMapper.writeValueAsString(요청_DTO)));

// then
perform.andExpect(status().isOk());
}

@DisplayName("[POST] 어플리케이션_ID가 없으면 400 에러를 반환한다.")
@Test
void createLogWithoutAppId() throws Exception {
// given
String 로그_레벨 = "INFO";
String 로그_데이터 = "테스트_로그_데이터";
LocalDateTime 타임스탬프 = LocalDateTime.of(2021, 1, 1, 0, 0, 0);

CreateLogRequest 요청_DTO = new CreateLogRequest(로그_레벨, 로그_데이터, 타임스탬프);

when(logService.saveLog(any()))
.thenReturn(1L);

// when
ResultActions perform = mockMvc.perform(post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(요청_DTO)));

// then
perform.andExpect(status().isBadRequest());
}

@DisplayName("[POST] 잘못된 입력으로 로그 생성 시 400 에러를 반환한다.")
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("invalidLogCreationInputs")
void createLogWithInvalidInput(String testCase, Long appId, String level, String data,
LocalDateTime timestamp) throws Exception {
// given
CreateLogRequest 요청_DTO = new CreateLogRequest(level, data, timestamp);
when(logService.saveLog(any())).thenReturn(1L);

// when
ResultActions perform = mockMvc.perform(post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("app_id", appId)
.content(objectMapper.writeValueAsString(요청_DTO)));

// then
perform.andExpect(status().isBadRequest());
}

private static Stream<Arguments> invalidLogCreationInputs() {
return Stream.of(
Arguments.of("어플리케이션_ID가 음수인 경우", -1L, "INFO", "테스트_로그_데이터",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_레벨이 null인 경우", 1L, null, "테스트_로그_데이터",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_레벨이 빈 문자열인 경우", 1L, " ", "테스트_로그_데이터",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_데이터가 null인 경우", 1L, "INFO", null,
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_데이터가 빈 문자열인 경우", 1L, "INFO", " ",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("타임스탬프가 null인 경우", 1L, "INFO", "테스트_로그_데이터", null)
);
}
}
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

import info.logbat.domain.log.domain.Log;
import info.logbat.domain.log.domain.enums.Level;
import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@Transactional
miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down