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: 유저 조회 API 개발 #12

Merged
merged 2 commits into from
Dec 29, 2023
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
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();
}

}
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);
}
}
}