Skip to content

Commit

Permalink
Feat/#19 App Key 생성 및 서비스 구현 (#42)
Browse files Browse the repository at this point in the history
* feat: App token 추가

- App 생성시 UUID 기반 토큰이 생성되도록 했습니다.
- 이에 따른 테스트를 수정했습니다.

* feat: App 생성 로직 구현 - 서비스

- App 생성을 진행하고 DB 저장후 결과를 반환하는 서비스 로직을 구현했습니다.
  - 요청에 대한 반환은 `AppCommonResponse` DTO를 통해 반환됩니다.

* feat: App 조회 로직 구현 - 서비스

- Token을 활용해 App을 조회하는 로직을 구현했습니다.
- ID를 활용해 App을 조회하는 로직을 구현했습니다.
- 이에 따른 테스트를 추가했습니다.

* chore: 인덱싱 추가

- 토큰 기반 조회 쿼리 성능 향상을 위해 인덱싱을 명시했습니다.

* feat: App 삭제 로직 구현 - 서비스

- App 삭제를 진행하고 성공시 삭제한 App Id를 반환하도록 했습니다.

* feat: App 서비스 트랜젝션 추가

- App 서비스 메서드들에 대해 `@Transactional`을 적용했습니다.
  • Loading branch information
tidavid1 authored Aug 13, 2024
1 parent 20c2844 commit d59a0fe
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package info.logbat.domain.project.application;

import info.logbat.domain.project.domain.App;
import info.logbat.domain.project.domain.Project;
import info.logbat.domain.project.domain.enums.AppType;
import info.logbat.domain.project.presentation.payload.response.AppCommonResponse;
import info.logbat.domain.project.repository.AppJpaRepository;
import info.logbat.domain.project.repository.ProjectJpaRepository;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class AppService {

private static final String APP_NOT_FOUND_MESSAGE = "앱을 찾을 수 없습니다.";

private final AppJpaRepository appRepository;
private final ProjectJpaRepository projectRepository;

public AppCommonResponse createApp(Long projectId, AppType appType) {
Project project = getProject(projectId);
return AppCommonResponse.from(appRepository.save(App.of(project, appType)));
}

@Transactional(readOnly = true)
public AppCommonResponse getAppByToken(String token) {
UUID tokenUUID = UUID.fromString(token);
App app = appRepository.findByToken(tokenUUID)
.orElseThrow(() -> new IllegalArgumentException(APP_NOT_FOUND_MESSAGE));
return AppCommonResponse.from(app);
}

@Transactional(readOnly = true)
public AppCommonResponse getAppById(Long id) {
App app = appRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException(APP_NOT_FOUND_MESSAGE));
return AppCommonResponse.from(app);
}

public Long deleteApp(Long projectId, Long appId) {
App app = appRepository.findByProject_IdAndId(projectId, appId)
.orElseThrow(() -> new IllegalArgumentException(APP_NOT_FOUND_MESSAGE));
appRepository.delete(app);
return app.getId();
}

private Project getProject(Long id) {
return projectRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("프로젝트를 찾을 수 없습니다."));
}
}
11 changes: 10 additions & 1 deletion logbat/src/main/java/info/logbat/domain/project/domain/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -21,7 +23,9 @@

@Entity
@Getter
@Table(name = "entities")
@Table(name = "entities", indexes = {
@Index(name = "idx_app_token", columnList = "token")
})
@SoftDelete
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class App {
Expand All @@ -38,13 +42,18 @@ public class App {
@Column(name = "app_type", nullable = false)
private AppType appType;

@Column(name = "token", nullable = false, unique = true)
private UUID token;

@CreationTimestamp
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;


private App(Project project, AppType appType) {
this.project = Objects.requireNonNull(project, "프로젝트는 필수입니다.");
this.appType = Objects.requireNonNull(appType, "앱 타입은 필수입니다.");
this.token = UUID.randomUUID();
}

public static App of(Project project, AppType appType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package info.logbat.domain.project.presentation.payload.response;

import info.logbat.domain.project.domain.App;
import java.time.LocalDateTime;

public record AppCommonResponse(Long id, Long projectId, String appType, String token,
LocalDateTime createdAt) {

public static AppCommonResponse from(App app) {
return new AppCommonResponse(app.getId(), app.getProject().getId(), app.getAppType().name(),
app.getToken().toString(), app.getCreatedAt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package info.logbat.domain.project.repository;

import info.logbat.domain.project.domain.App;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Repository;

@Repository
public interface AppJpaRepository extends JpaRepository<App, Long> {

Optional<App> findByToken(@NonNull UUID token);

Optional<App> findByProject_IdAndId(@NonNull Long id, @NonNull Long id1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package info.logbat.domain.project.application;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;

import info.logbat.domain.project.domain.App;
import info.logbat.domain.project.domain.Project;
import info.logbat.domain.project.domain.enums.AppType;
import info.logbat.domain.project.presentation.payload.response.AppCommonResponse;
import info.logbat.domain.project.repository.AppJpaRepository;
import info.logbat.domain.project.repository.ProjectJpaRepository;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
@DisplayName("AppService는")
class AppServiceTest {

@InjectMocks
private AppService appService;

@Mock
private AppJpaRepository appRepository;
@Mock
private ProjectJpaRepository projectRepository;

private final Project expectedProject = mock(Project.class);
private final App expectedApp = spy(App.of(expectedProject, AppType.JAVA));
private final Long expectedProjectId = 1L;
private final Long expectedAppId = 1L;

@Nested
@DisplayName("새로운 App을 생성할 때")
class whenCreateNewApp {

@Test
@DisplayName("프로젝트가 존재하지 않으면 IllegalArgumentException을 던진다.")
void willThrowExceptionWhenProjectNotFound() {
// Arrange
Long notExistProjectId = 2L;
given(projectRepository.findById(notExistProjectId)).willReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> appService.createApp(notExistProjectId, AppType.JAVA))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("프로젝트를 찾을 수 없습니다.");
}

@Test
@DisplayName("정상적으로 생성한다.")
void willCreateNewApp() {
// Arrange
LocalDateTime expectedCreatedAt = LocalDateTime.now();
given(projectRepository.findById(expectedProjectId)).willReturn(
Optional.of(expectedProject));
given(expectedProject.getId()).willReturn(expectedProjectId);
given(appRepository.save(any(App.class))).willReturn(expectedApp);
given(expectedApp.getId()).willReturn(expectedAppId);
given(expectedApp.getCreatedAt()).willReturn(expectedCreatedAt);
// Act
AppCommonResponse actualResult = appService.createApp(expectedProjectId, AppType.JAVA);
// Assert
assertAll(
() -> assertThat(actualResult)
.extracting("id", "projectId", "appType", "createdAt")
.containsExactly(expectedAppId, expectedProjectId, AppType.JAVA.name(),
expectedCreatedAt),
() -> assertThat(actualResult.token()).isNotNull()
);

}

}

@Nested
@DisplayName("App을 조회할 때")
class whenGetApp {

private final UUID expectedToken = UUID.randomUUID();
private final App expectedApp = spy(App.of(expectedProject, AppType.JAVA));

@Test
@DisplayName("토큰으로 조회할 수 있다.")
void canGetAppByToken() {
// Arrange
String expectedTokenString = expectedToken.toString();
given(appRepository.findByToken(expectedToken)).willReturn(Optional.of(expectedApp));
given(expectedApp.getToken()).willReturn(expectedToken);
// Act
AppCommonResponse actualResult = appService.getAppByToken(expectedTokenString);
// Assert
assertThat(actualResult)
.extracting("token")
.isEqualTo(expectedTokenString);
}

@Test
@DisplayName("ID로 조회할 수 있다.")
void canGetAppById() {
// Arrange
given(appRepository.findById(expectedAppId)).willReturn(Optional.of(expectedApp));
given(expectedApp.getId()).willReturn(expectedAppId);
// Act
AppCommonResponse actualResult = appService.getAppById(expectedAppId);
// Assert
assertThat(actualResult)
.extracting("id")
.isEqualTo(expectedAppId);
}

@Test
@DisplayName("ID 조회시 앱을 찾을 수 없으면 예외를 던진다.")
void willThrowExceptionWhenAppNotFoundById() {
// Arrange
given(appRepository.findById(expectedAppId)).willReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> appService.getAppById(expectedAppId))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("앱을 찾을 수 없습니다.");
}

@Test
@DisplayName("토큰 조회시 앱을 찾을 수 없으면 예외를 던진다.")
void willThrowExceptionWhenAppNotFoundByToken() {
// Arrange
String notExistToken = UUID.randomUUID().toString();
// Arrange
given(appRepository.findByToken(any(UUID.class))).willReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> appService.getAppByToken(notExistToken))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("앱을 찾을 수 없습니다.");
}
}

@Nested
@DisplayName("App을 삭제할 때")
class whenDeleteApp {

@Test
@DisplayName("프로젝트와 앱 ID로 삭제할 수 있다.")
void canDeleteAppByProjectIdAndAppId() {
// Arrange
given(appRepository.findByProject_IdAndId(expectedProjectId, expectedAppId)).willReturn(
Optional.of(expectedApp));
given(expectedApp.getId()).willReturn(expectedAppId);
// Act
Long actualResult = appService.deleteApp(expectedProjectId, expectedAppId);
// Assert
assertThat(actualResult).isEqualTo(expectedAppId);
}

@Test
@DisplayName("앱을 찾을 수 없으면 예외를 던진다.")
void willThrowExceptionWhenAppNotFound() {
// Arrange
given(appRepository.findByProject_IdAndId(expectedProjectId, expectedAppId)).willReturn(
Optional.empty());
// Act & Assert
assertThatThrownBy(() -> appService.deleteApp(expectedProjectId, expectedAppId))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("앱을 찾을 수 없습니다.");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertAll;

import info.logbat.domain.project.domain.enums.AppType;
import java.util.stream.Stream;
Expand Down Expand Up @@ -29,10 +30,13 @@ void createSuccess() {
// Act
App actualResult = App.of(EXPECTED_PROJECT, expectedAppType);
// Assert
assertThat(actualResult)
.isNotNull()
.extracting("project", "appType")
.containsExactly(EXPECTED_PROJECT, expectedAppType);
assertAll(
() -> assertThat(actualResult)
.isNotNull()
.extracting("project", "appType")
.containsExactly(EXPECTED_PROJECT, expectedAppType),
() -> assertThat(actualResult.getToken()).isNotNull()
);
}

@ParameterizedTest
Expand Down

0 comments on commit d59a0fe

Please sign in to comment.