Skip to content

Commit

Permalink
feat: 유저 조회 API 개발 (#12)
Browse files Browse the repository at this point in the history
* feat: 유저 조회 API를 개발한다

* feat: ErrorResponse를 정의한다
  • Loading branch information
devxb authored Dec 29, 2023
1 parent 59ae582 commit f06e137
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 2 deletions.
1 change: 1 addition & 0 deletions gradle/spring.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
19 changes: 19 additions & 0 deletions src/main/java/net/teumteum/core/error/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
33 changes: 33 additions & 0 deletions src/main/java/net/teumteum/user/controller/UserController.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 2 additions & 1 deletion src/main/java/net/teumteum/user/domain/ActivityArea.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +20,6 @@ public class ActivityArea {
@Column(name = "city")
private String city;

@ElementCollection
@ElementCollection(fetch = FetchType.EAGER)
private List<String> street = new ArrayList<>();
}
6 changes: 5 additions & 1 deletion src/main/java/net/teumteum/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -59,7 +63,7 @@ public class User extends TimeBaseEntity {
@Embedded
private Job job;

@ElementCollection
@ElementCollection(fetch = FetchType.EAGER)
private List<String> interests = new ArrayList<>();

@Embedded
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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()
);
}
}
}
23 changes: 23 additions & 0 deletions src/main/java/net/teumteum/user/service/UserService.java
Original file line number Diff line number Diff line change
@@ -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);
}

}
6 changes: 6 additions & 0 deletions src/test/java/net/teumteum/user/domain/UserFixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions src/test/java/net/teumteum/user/integration/Api.java
Original file line number Diff line number Diff line change
@@ -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();
}

}
19 changes: 19 additions & 0 deletions src/test/java/net/teumteum/user/integration/IntegrationTest.java
Original file line number Diff line number Diff line change
@@ -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;

}
24 changes: 24 additions & 0 deletions src/test/java/net/teumteum/user/integration/Repository.java
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit f06e137

Please sign in to comment.