diff --git a/gradle/spring.gradle b/gradle/spring.gradle index d15c8f9b..0702e629 100644 --- a/gradle/spring.gradle +++ b/gradle/spring.gradle @@ -13,6 +13,7 @@ allprojects { dependencies { implementation "org.springframework.boot:spring-boot-starter" implementation 'org.springframework.boot:spring-boot-starter-web' + implementation "org.springframework.boot:spring-boot-starter-webflux" implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation "org.springframework.boot:spring-boot-starter-actuator" diff --git a/src/main/java/net/teumteum/core/error/ErrorResponse.java b/src/main/java/net/teumteum/core/error/ErrorResponse.java new file mode 100644 index 00000000..b9b1323e --- /dev/null +++ b/src/main/java/net/teumteum/core/error/ErrorResponse.java @@ -0,0 +1,19 @@ +package net.teumteum.core.error; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ErrorResponse { + + private String message; + + public static ErrorResponse of(Throwable exception) { + return new ErrorResponse(exception.getMessage()); + } +} diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java new file mode 100644 index 00000000..03d643d3 --- /dev/null +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -0,0 +1,33 @@ +package net.teumteum.user.controller; + +import lombok.RequiredArgsConstructor; +import net.teumteum.core.error.ErrorResponse; +import net.teumteum.user.domain.response.UserGetResponse; +import net.teumteum.user.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/users") +public class UserController { + + private final UserService userService; + + @GetMapping("/{userId}") + @ResponseStatus(HttpStatus.OK) + public UserGetResponse getUserById(@PathVariable("userId") Long userId) { + return userService.getUserById(userId); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ErrorResponse handleIllegalArgumentException(IllegalArgumentException illegalArgumentException) { + return ErrorResponse.of(illegalArgumentException); + } +} diff --git a/src/main/java/net/teumteum/user/domain/ActivityArea.java b/src/main/java/net/teumteum/user/domain/ActivityArea.java index c52938c7..6d5a3440 100644 --- a/src/main/java/net/teumteum/user/domain/ActivityArea.java +++ b/src/main/java/net/teumteum/user/domain/ActivityArea.java @@ -4,6 +4,7 @@ import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; +import jakarta.persistence.FetchType; import java.util.ArrayList; import java.util.List; import lombok.AllArgsConstructor; @@ -19,6 +20,6 @@ public class ActivityArea { @Column(name = "city") private String city; - @ElementCollection + @ElementCollection(fetch = FetchType.EAGER) private List street = new ArrayList<>(); } diff --git a/src/main/java/net/teumteum/user/domain/User.java b/src/main/java/net/teumteum/user/domain/User.java index 60cad5a9..4a9fe273 100644 --- a/src/main/java/net/teumteum/user/domain/User.java +++ b/src/main/java/net/teumteum/user/domain/User.java @@ -6,6 +6,9 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.PrePersist; import java.util.ArrayList; @@ -25,6 +28,7 @@ public class User extends TimeBaseEntity { @Id @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", length = 10) @@ -59,7 +63,7 @@ public class User extends TimeBaseEntity { @Embedded private Job job; - @ElementCollection + @ElementCollection(fetch = FetchType.EAGER) private List interests = new ArrayList<>(); @Embedded diff --git a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java new file mode 100644 index 00000000..06369c23 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java @@ -0,0 +1,70 @@ +package net.teumteum.user.domain.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import net.teumteum.user.domain.User; + +public record UserGetResponse( + Long id, + String name, + String birth, + Long characterId, + int mannerTemperature, + String authenticated, + ActivityArea activityArea, + String mbti, + String status, + String goal, + Job job, + List interests +) { + + public static UserGetResponse of(User user) { + return new UserGetResponse( + user.getId(), + user.getName(), + user.getBirth(), + user.getCharacterId(), + user.getMannerTemperature(), + user.getOauth().getAuthenticated(), + ActivityArea.of(user), + user.getMbti(), + user.getStatus().name(), + user.getGoal(), + Job.of(user), + user.getInterests() + ); + } + + public record ActivityArea( + String city, + List streets + ) { + + public static ActivityArea of(User user) { + return new ActivityArea( + user.getActivityArea().getCity(), + user.getActivityArea().getStreet() + ); + } + + } + + public record Job( + String name, + boolean certificated, + @JsonProperty("class") + String jobClass, + String detailClass + ) { + + public static Job of(User user) { + return new Job( + user.getJob().getName(), + user.getJob().isCertificated(), + user.getJob().getJobClass(), + user.getJob().getDetailJobClass() + ); + } + } +} diff --git a/src/main/java/net/teumteum/user/service/UserService.java b/src/main/java/net/teumteum/user/service/UserService.java new file mode 100644 index 00000000..bc2413e7 --- /dev/null +++ b/src/main/java/net/teumteum/user/service/UserService.java @@ -0,0 +1,23 @@ +package net.teumteum.user.service; + +import lombok.RequiredArgsConstructor; +import net.teumteum.user.domain.UserRepository; +import net.teumteum.user.domain.response.UserGetResponse; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + + public UserGetResponse getUserById(Long userId) { + var existUser = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("userId에 해당하는 user를 찾을 수 없습니다. \"" + userId + "\"")); + + return UserGetResponse.of(existUser); + } + +} diff --git a/src/test/java/net/teumteum/user/domain/UserFixture.java b/src/test/java/net/teumteum/user/domain/UserFixture.java index 644fe66d..7a04d4da 100644 --- a/src/test/java/net/teumteum/user/domain/UserFixture.java +++ b/src/test/java/net/teumteum/user/domain/UserFixture.java @@ -5,6 +5,12 @@ public class UserFixture { + public static User getNullIdUser() { + return newUserByBuilder(UserBuilder.builder() + .id(null) + .build()); + } + public static User getUserWithId(Long id) { return newUserByBuilder(UserBuilder.builder() .id(id) diff --git a/src/test/java/net/teumteum/user/integration/Api.java b/src/test/java/net/teumteum/user/integration/Api.java new file mode 100644 index 00000000..805e8fac --- /dev/null +++ b/src/test/java/net/teumteum/user/integration/Api.java @@ -0,0 +1,27 @@ +package net.teumteum.user.integration; + +import org.springframework.boot.test.context.TestComponent; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +@TestComponent +class Api { + + private final WebTestClient webTestClient; + + public Api(ApplicationContext applicationContext) { + var controllers = applicationContext.getBeansWithAnnotation(Controller.class).values(); + webTestClient = WebTestClient.bindToController(controllers.toArray()).build(); + } + + ResponseSpec getUser(String token, Long userId) { + return webTestClient.get() + .uri("/users/" + userId) + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); + } + +} diff --git a/src/test/java/net/teumteum/user/integration/IntegrationTest.java b/src/test/java/net/teumteum/user/integration/IntegrationTest.java new file mode 100644 index 00000000..47c48dab --- /dev/null +++ b/src/test/java/net/teumteum/user/integration/IntegrationTest.java @@ -0,0 +1,19 @@ +package net.teumteum.user.integration; + +import net.teumteum.Application; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ContextConfiguration(classes = {Application.class, Api.class, Repository.class}) +abstract class IntegrationTest { + + @Autowired + protected Api api; + + @Autowired + protected Repository repository; + +} diff --git a/src/test/java/net/teumteum/user/integration/Repository.java b/src/test/java/net/teumteum/user/integration/Repository.java new file mode 100644 index 00000000..6f2dce43 --- /dev/null +++ b/src/test/java/net/teumteum/user/integration/Repository.java @@ -0,0 +1,24 @@ +package net.teumteum.user.integration; + +import lombok.RequiredArgsConstructor; +import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserFixture; +import net.teumteum.user.domain.UserRepository; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +class Repository { + + private final UserRepository userRepository; + + User saveAndGetUser() { + var user = UserFixture.getNullIdUser(); + return userRepository.saveAndFlush(user); + } + + void clear() { + userRepository.deleteAll(); + } + +} diff --git a/src/test/java/net/teumteum/user/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/user/integration/UserIntegrationTest.java new file mode 100644 index 00000000..ce150798 --- /dev/null +++ b/src/test/java/net/teumteum/user/integration/UserIntegrationTest.java @@ -0,0 +1,61 @@ +package net.teumteum.user.integration; + +import net.teumteum.core.error.ErrorResponse; +import net.teumteum.user.domain.response.UserGetResponse; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("유저 통합테스트의") +class UserIntegrationTest extends IntegrationTest { + + private static final String VALID_TOKEN = "VALID_TOKEN"; + private static final String INVALID_TOKEN = "IN_VALID_TOKEN"; + + @AfterEach + @BeforeEach + void clearAll() { + repository.clear(); + } + + @Nested + @DisplayName("유저 조회 API는") + class Find_user_api { + + @Test + @DisplayName("존재하는 유저의 id가 주어지면, 유저 정보를 응답한다.") + void Return_user_info_if_exist_user_id_received() { + // given + var user = repository.saveAndGetUser(); + var expected = UserGetResponse.of(user); + + // when + var result = api.getUser(VALID_TOKEN, user.getId()); + + // then + Assertions.assertThat( + result.expectStatus().isOk() + .expectBody(UserGetResponse.class) + .returnResult().getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + @DisplayName("존재하지 않는 유저의 id가 주어지면, 400 Bad Request를 응답한다.") + void Return_400_bad_request_if_not_exists_user_id_received() { + // given + var notExistUserId = 1L; + + // when + var result = api.getUser(VALID_TOKEN, notExistUserId); + + // then + result.expectStatus().isBadRequest() + .expectBody(ErrorResponse.class); + } + } +}