diff --git a/.env.sample b/.env.sample index d4f3ba5a..3505b912 100644 --- a/.env.sample +++ b/.env.sample @@ -1,4 +1,7 @@ MYSQL_USERNAME=root MYSQL_PASSWORD=1234 MYSQL_DATABASE=TALKKA_DB -MYSQL_URL=jdbc:mysql://localhost:3306/TALKKA_DB?createDatabaseIfNotExist=true \ No newline at end of file +MYSQL_URL=jdbc:mysql://localhost:3306/TALKKA_DB?createDatabaseIfNotExist=true + +NAVER_CLIENT_ID=CLIENT_ID +NAVER_CLINET_SECRET=CLIENT_SECRET \ No newline at end of file diff --git a/.gitignore b/.gitignore index ced7d7f2..4b47186d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,8 @@ out/ ### OS ### .DS_Store -./data/mysql-data \ No newline at end of file +./data/mysql-data + +.env +.env.* +!.env.sample \ No newline at end of file diff --git a/server/src/main/java/com/talkka/server/ServerApplication.java b/server/src/main/java/com/talkka/server/ServerApplication.java index 7d4d3d99..58fe4641 100644 --- a/server/src/main/java/com/talkka/server/ServerApplication.java +++ b/server/src/main/java/com/talkka/server/ServerApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication -@EnableJpaAuditing public class ServerApplication { public static void main(String[] args) { diff --git a/server/src/main/java/com/talkka/server/common/dto/ApiRespDto.java b/server/src/main/java/com/talkka/server/common/dto/ApiRespDto.java index 4b299369..cc6376b7 100644 --- a/server/src/main/java/com/talkka/server/common/dto/ApiRespDto.java +++ b/server/src/main/java/com/talkka/server/common/dto/ApiRespDto.java @@ -2,6 +2,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; @@ -11,6 +12,7 @@ @ToString @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode public class ApiRespDto { private int statusCode; private String message; diff --git a/server/src/main/java/com/talkka/server/common/exception/handler/RestControllerAdvice.java b/server/src/main/java/com/talkka/server/common/exception/handler/RestControllerAdvice.java index ba90a639..130b56a5 100644 --- a/server/src/main/java/com/talkka/server/common/exception/handler/RestControllerAdvice.java +++ b/server/src/main/java/com/talkka/server/common/exception/handler/RestControllerAdvice.java @@ -1,6 +1,8 @@ package com.talkka.server.common.exception.handler; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -10,6 +12,19 @@ @ControllerAdvice public class RestControllerAdvice { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception) { + ApiRespDto responseDto = ApiRespDto.builder() + .statusCode(400) + .message(exception.getBindingResult().getAllErrors().get(0).getDefaultMessage()) + .build(); + + return new ResponseEntity<>( + responseDto, + HttpStatus.BAD_REQUEST + ); + } @ExceptionHandler(HttpBaseException.class) public ResponseEntity> handleHttpException(HttpBaseException exception) { diff --git a/server/src/main/java/com/talkka/server/config/JpaConfig.java b/server/src/main/java/com/talkka/server/config/JpaConfig.java new file mode 100644 index 00000000..8fd3f274 --- /dev/null +++ b/server/src/main/java/com/talkka/server/config/JpaConfig.java @@ -0,0 +1,9 @@ +package com.talkka.server.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/server/src/main/java/com/talkka/server/user/controller/UserController.java b/server/src/main/java/com/talkka/server/user/controller/UserController.java new file mode 100644 index 00000000..635cbc2b --- /dev/null +++ b/server/src/main/java/com/talkka/server/user/controller/UserController.java @@ -0,0 +1,122 @@ +package com.talkka.server.user.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.talkka.server.common.dto.ApiRespDto; +import com.talkka.server.common.enums.StatusCode; +import com.talkka.server.oauth.domain.OAuth2UserInfo; +import com.talkka.server.user.dto.UserCreateDto; +import com.talkka.server.user.dto.UserCreateReqDto; +import com.talkka.server.user.dto.UserDto; +import com.talkka.server.user.dto.UserRespDto; +import com.talkka.server.user.dto.UserUpdateReqDto; +import com.talkka.server.user.enums.Grade; +import com.talkka.server.user.service.UserService; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + private final UserService userService; + + @GetMapping("/{user_id}") + public ResponseEntity> getUser( + @PathVariable("user_id") Long userId + ) { + UserRespDto userRespDto = UserRespDto.of(userService.getUser(userId)); + return ResponseEntity.ok( + ApiRespDto.builder() + .statusCode(StatusCode.OK.getCode()) + .message(StatusCode.OK.getMessage()) + .data(userRespDto) + .build() + ); + } + + // 현재는 AuthController 쪽에서 처리하고 있으나, 추후 FE 연결 이후 사용할 것임. + @PostMapping("") + public ResponseEntity> createUser( + @AuthenticationPrincipal OAuth2UserInfo userInfo, + @RequestBody @Valid UserCreateReqDto userCreateReqDto) { + UserCreateDto userCreateDto = new UserCreateDto( + userInfo.getName(), + userInfo.getEmail(), + userCreateReqDto.getNickname(), + userInfo.getProvider(), + userInfo.getAccessToken(), + Grade.USER + ); + UserRespDto userRespDto = UserRespDto.of(userService.createUser(userCreateDto)); + return ResponseEntity.ok( + ApiRespDto.builder() + .statusCode(StatusCode.OK.getCode()) + .message(StatusCode.OK.getMessage()) + .data(userRespDto) + .build() + ); + } + + @PutMapping("/{user_id}") + public ResponseEntity> updateUser(@PathVariable("user_id") Long userId, + @RequestBody @Valid UserUpdateReqDto userUpdateReqDto) { + UserRespDto userRespDto = UserRespDto.of( + userService.updateUser(userId, userUpdateReqDto)); + return ResponseEntity.ok( + ApiRespDto.builder() + .statusCode(StatusCode.OK.getCode()) + .message(StatusCode.OK.getMessage()) + .data(userRespDto) + .build() + ); + } + + @DeleteMapping("/{user_id}") + public ResponseEntity> deleteUser(@PathVariable("user_id") Long userId) { + Long deletedUserId = userService.deleteUser(userId); + return ResponseEntity.ok( + ApiRespDto.builder() + .statusCode(StatusCode.OK.getCode()) + .message(StatusCode.OK.getMessage()) + .data(null) + .build() + ); + } + + @GetMapping("/me") + public ResponseEntity> getMe(@AuthenticationPrincipal OAuth2UserInfo userInfo) { + UserRespDto userRespDto = UserRespDto.of(userService.getUser(userInfo.getUserId())); + return ResponseEntity.ok( + ApiRespDto.builder() + .statusCode(StatusCode.OK.getCode()) + .message(StatusCode.OK.getMessage()) + .data(userRespDto) + .build() + ); + } + + @PutMapping("/me") + public ResponseEntity> updateMe(@AuthenticationPrincipal OAuth2UserInfo userInfo, + @RequestBody @Valid UserUpdateReqDto userUpdateReqDto) { + UserDto userDto = userService.updateUser(userInfo.getUserId(), userUpdateReqDto); + UserRespDto userRespDto = UserRespDto.of(userDto); + return ResponseEntity.ok( + ApiRespDto.builder() + .statusCode(StatusCode.OK.getCode()) + .message(StatusCode.OK.getMessage()) + .data(userRespDto) + .build() + ); + } +} diff --git a/server/src/main/java/com/talkka/server/user/dao/UserEntity.java b/server/src/main/java/com/talkka/server/user/dao/UserEntity.java index 386819ab..f6aa7b87 100644 --- a/server/src/main/java/com/talkka/server/user/dao/UserEntity.java +++ b/server/src/main/java/com/talkka/server/user/dao/UserEntity.java @@ -20,18 +20,17 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.ToString; @Entity(name = "users") @Getter -@Setter @Builder -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @ToString @EntityListeners(AuditingEntityListener.class) @@ -70,6 +69,7 @@ public class UserEntity { private LocalDateTime updatedAt; @OneToMany(mappedBy = "writer") + @ToString.Exclude private List busReviews; @Override @@ -86,4 +86,10 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hashCode(userId); } + + public void updateUser(String nickname) { + if (nickname != null && !nickname.isEmpty()) { + this.nickname = nickname; + } + } } diff --git a/server/src/main/java/com/talkka/server/user/dto/UserCreateDto.java b/server/src/main/java/com/talkka/server/user/dto/UserCreateDto.java index 305b8944..d44feec4 100644 --- a/server/src/main/java/com/talkka/server/user/dto/UserCreateDto.java +++ b/server/src/main/java/com/talkka/server/user/dto/UserCreateDto.java @@ -5,16 +5,12 @@ import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.ToString; @Getter @Builder @ToString -@EqualsAndHashCode -@NoArgsConstructor @AllArgsConstructor public class UserCreateDto { private String name; diff --git a/server/src/main/java/com/talkka/server/user/dto/UserCreateReqDto.java b/server/src/main/java/com/talkka/server/user/dto/UserCreateReqDto.java new file mode 100644 index 00000000..dec1d1d8 --- /dev/null +++ b/server/src/main/java/com/talkka/server/user/dto/UserCreateReqDto.java @@ -0,0 +1,23 @@ +package com.talkka.server.user.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@Builder +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserCreateReqDto { + @NotNull + @Size(min = 2, max = 20, message = "닉네임은 2자 이상 20자 이하로 입력해주세요.") + @Pattern(regexp = "^[a-zA-Z0-9가-힣]*$", message = "닉네임은 영문 대소문자, 한글, 숫자로만 입력해주세요.") + private String nickname; +} diff --git a/server/src/main/java/com/talkka/server/user/dto/UserDto.java b/server/src/main/java/com/talkka/server/user/dto/UserDto.java index 570e44df..7beb5ecb 100644 --- a/server/src/main/java/com/talkka/server/user/dto/UserDto.java +++ b/server/src/main/java/com/talkka/server/user/dto/UserDto.java @@ -1,58 +1,56 @@ package com.talkka.server.user.dto; import java.time.LocalDateTime; -import java.util.List; +import java.util.ArrayList; -import com.talkka.server.review.dao.BusReviewEntity; import com.talkka.server.user.dao.UserEntity; import com.talkka.server.user.enums.Grade; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.ToString; @Getter @Builder @ToString -@EqualsAndHashCode -@NoArgsConstructor @AllArgsConstructor public class UserDto { private Long userId; + private String name; + private String email; private String nickname; private String oauthProvider; private String accessToken; private Grade grade; private LocalDateTime createdAt; private LocalDateTime updatedAt; - private List busReviews; public static UserDto of(UserEntity userEntity) { - return UserDto.builder() - .userId(userEntity.getUserId()) - .nickname(userEntity.getNickname()) - .oauthProvider(userEntity.getOauthProvider()) - .accessToken(userEntity.getAccessToken()) - .grade(userEntity.getGrade()) - .createdAt(userEntity.getCreatedAt()) - .updatedAt(userEntity.getUpdatedAt()) - .busReviews(userEntity.getBusReviews()) - .build(); + return new UserDto( + userEntity.getUserId(), + userEntity.getName(), + userEntity.getEmail(), + userEntity.getNickname(), + userEntity.getOauthProvider(), + userEntity.getAccessToken(), + userEntity.getGrade(), + userEntity.getCreatedAt(), + userEntity.getUpdatedAt() + ); } public UserEntity toEntity() { - return UserEntity.builder() - .userId(userId) - .nickname(nickname) - .oauthProvider(oauthProvider) - .accessToken(accessToken) - .grade(grade) - .createdAt(createdAt) - .updatedAt(updatedAt) - .busReviews(busReviews) - .build(); + return new UserEntity( + userId, + name, + email, + nickname, + oauthProvider, + accessToken, + grade, + createdAt, + updatedAt, + new ArrayList<>()); } } diff --git a/server/src/main/java/com/talkka/server/user/dto/UserRespDto.java b/server/src/main/java/com/talkka/server/user/dto/UserRespDto.java index 0f7faa7d..f0193019 100644 --- a/server/src/main/java/com/talkka/server/user/dto/UserRespDto.java +++ b/server/src/main/java/com/talkka/server/user/dto/UserRespDto.java @@ -1,27 +1,28 @@ package com.talkka.server.user.dto; -import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.ToString; @Getter @Builder @ToString -@NoArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor public class UserRespDto { private Long userId; + private String name; + private String email; private String nickname; private String oauthProvider; public static UserRespDto of(UserDto userDto) { - return UserRespDto.builder() - .userId(userDto.getUserId()) - .nickname(userDto.getNickname()) - .oauthProvider(userDto.getOauthProvider()) - .build(); + return new UserRespDto( + userDto.getUserId(), + userDto.getName(), + userDto.getEmail(), + userDto.getNickname(), + userDto.getOauthProvider() + ); } } diff --git a/server/src/main/java/com/talkka/server/user/dto/UserUpdateReqDto.java b/server/src/main/java/com/talkka/server/user/dto/UserUpdateReqDto.java index 87be54da..f26af1ac 100644 --- a/server/src/main/java/com/talkka/server/user/dto/UserUpdateReqDto.java +++ b/server/src/main/java/com/talkka/server/user/dto/UserUpdateReqDto.java @@ -1,5 +1,9 @@ package com.talkka.server.user.dto; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -9,8 +13,11 @@ @Getter @Builder @ToString -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class UserUpdateReqDto { + @NotNull + @Size(min = 2, max = 20, message = "닉네임은 2자 이상 20자 이하로 입력해주세요.") + @Pattern(regexp = "^[a-zA-Z0-9가-힣]*$", message = "닉네임은 영문 대소문자, 한글, 숫자로만 입력해주세요.") private String nickname; } diff --git a/server/src/main/java/com/talkka/server/user/service/UserService.java b/server/src/main/java/com/talkka/server/user/service/UserService.java index 7af47716..07d4e7ba 100644 --- a/server/src/main/java/com/talkka/server/user/service/UserService.java +++ b/server/src/main/java/com/talkka/server/user/service/UserService.java @@ -1,6 +1,7 @@ package com.talkka.server.user.service; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.talkka.server.common.exception.http.BadRequestException; import com.talkka.server.common.exception.http.NotFoundException; @@ -18,51 +19,42 @@ public class UserService { private final UserRepository userRepository; public UserDto getUser(Long userId) { - final UserEntity user = userRepository.findById(userId) + UserEntity user = userRepository.findById(userId) .orElseThrow(() -> new NotFoundException("존재하지 않는 유저입니다.")); return UserDto.of(user); } public UserDto createUser(UserCreateDto userCreateDto) { - final UserEntity user = userCreateDto.toEntity(); + UserEntity user = userCreateDto.toEntity(); if (this.isDuplicatedNickname(user.getNickname())) { throw new BadRequestException("중복된 닉네임 입니다."); } - final UserEntity savedUser = userRepository.save(user); + UserEntity savedUser = userRepository.save(user); return UserDto.of(savedUser); } + @Transactional public UserDto updateUser(Long userId, UserUpdateReqDto reqDto) { - final UserEntity user = userRepository.findById(userId) + UserEntity user = userRepository.findById(userId) .orElseThrow(() -> new BadRequestException("존재하지 않는 유저입니다.")); - // 중복 닉네임 체크 + if (!reqDto.getNickname().equals(user.getNickname()) && this.isDuplicatedNickname(reqDto.getNickname())) { throw new BadRequestException("중복된 닉네임 입니다."); } - final UserEntity updatedUser = UserEntity.builder() // refactoring 필요함. - .userId(userId) - .nickname(reqDto.getNickname()) - .oauthProvider(user.getOauthProvider()) - .accessToken(user.getAccessToken()) - .grade(user.getGrade()) - .createdAt(user.getCreatedAt()) - .updatedAt(user.getUpdatedAt()) - .busReviews(user.getBusReviews()) - .build(); - final UserEntity savedUser = userRepository.save(updatedUser); - - return UserDto.of(savedUser); + user.updateUser(reqDto.getNickname()); + return UserDto.of(user); } - public void deleteUser(Long userId) { - final boolean isExist = userRepository.existsById(userId); + public Long deleteUser(Long userId) { + boolean isExist = userRepository.existsById(userId); if (!isExist) { throw new BadRequestException("존재하지 않는 유저입니다."); } userRepository.deleteById(userId); + return userId; } public boolean isDuplicatedNickname(String nickname) { diff --git a/server/src/test/java/com/talkka/server/user/controller/UserControllerTest.java b/server/src/test/java/com/talkka/server/user/controller/UserControllerTest.java new file mode 100644 index 00000000..86d57e9e --- /dev/null +++ b/server/src/test/java/com/talkka/server/user/controller/UserControllerTest.java @@ -0,0 +1,467 @@ +package com.talkka.server.user.controller; + +import static org.mockito.BDDMockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +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.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.talkka.server.common.enums.StatusCode; +import com.talkka.server.common.exception.http.BadRequestException; +import com.talkka.server.common.exception.http.NotFoundException; +import com.talkka.server.oauth.domain.NaverOAuth2User; +import com.talkka.server.oauth.domain.OAuth2UserInfo; +import com.talkka.server.user.dto.UserCreateDto; +import com.talkka.server.user.dto.UserCreateReqDto; +import com.talkka.server.user.dto.UserDto; +import com.talkka.server.user.dto.UserRespDto; +import com.talkka.server.user.dto.UserUpdateReqDto; +import com.talkka.server.user.enums.Grade; +import com.talkka.server.user.service.UserService; + +@WebMvcTest(UserController.class) +@MockBean({ + JpaMetamodelMappingContext.class, +}) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @Mock + private OAuth2UserInfo oAuth2User; + + @BeforeEach + public void setUp() { + Map attributes = new HashMap<>(); + Long userId = 1L; + attributes.put("userId", userId); + attributes.put("name", "testUser"); + attributes.put("nickname", "testUser"); + attributes.put("email", "test@example.com"); + attributes.put("oauthProvider", "naver"); + attributes.put("accessToken", "token"); + attributes.put("oauth2Id", "test"); + oAuth2User = new NaverOAuth2User(attributes, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + } + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Nested + @DisplayName("GET /api/users/{user_id}") + public class UsersUserIdTest { + + @Test + public void 유저_아이디의_정보를_요청하면_유저_정보를_반환한다() throws Exception { + // given + final Long userId = 1L; + final UserDto userDto = new UserDto( + userId, + "name", + "nickname", + "email", + "oauthProvider", + "accessToken", + Grade.USER, + LocalDateTime.now(), + LocalDateTime.now() + ); + final UserRespDto expect = UserRespDto.of(userDto); + given(userService.getUser(userId)).willReturn(userDto); + // when + // then + mockMvc.perform(get("/api/users/{user_id}", userId) + .with(oauth2Login().oauth2User(oAuth2User))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status_code").value(StatusCode.OK.getCode())) + .andExpect(jsonPath("$.message").value(StatusCode.OK.getMessage())) + .andExpect(jsonPath("$.data.user_id").value(expect.getUserId())) + .andExpect(jsonPath("$.data.nickname").value(expect.getNickname())) + .andExpect(jsonPath("$.data.oauth_provider").value(expect.getOauthProvider())); + } + + @Test + public void 존재하지않는_유저아이디를_입력하면_404을_응답한다() throws Exception { + // given + final Long userId = 1L; + given(userService.getUser(userId)).willThrow(new NotFoundException("존재하지 않는 유저입니다.")); + // when + // then + mockMvc.perform(get("/api/users/{user_id}", userId) + .with(oauth2Login().oauth2User(oAuth2User))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status_code").value(404)) + .andExpect(jsonPath("$.message").value("존재하지 않는 유저입니다.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + } + + @Nested + @DisplayName("POST /api/users") + public class CreateUserTest { + @Test + void 유저가_생성을_요청하면_생성된_유저의_정보를_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("nickname"); + final UserCreateDto userCreateDto = new UserCreateDto( + "testUser", + "test@example.com", + "naver", + userCreateReqDto.getNickname(), + "token", + Grade.USER + ); + final UserDto expectedData = new UserDto( + 1L, + userCreateDto.getName(), + userCreateDto.getEmail(), + userCreateDto.getNickname(), + userCreateDto.getOauthProvider(), + "token", + Grade.USER, + LocalDateTime.now(), + LocalDateTime.now() + ); + given(userService.createUser(any(UserCreateDto.class))).willReturn(expectedData); + // when + // then + // ApiRespDto + mockMvc.perform(post("/api/users") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status_code").value(StatusCode.OK.getCode())) + .andExpect(jsonPath("$.message").value(StatusCode.OK.getMessage())) + .andExpect(jsonPath("$.data.user_id").exists()) + .andExpect(jsonPath("$.data.name").value(userCreateDto.getName())) + .andExpect(jsonPath("$.data.email").value(userCreateDto.getEmail())) + .andExpect(jsonPath("$.data.nickname").value(userCreateDto.getNickname())) + .andExpect(jsonPath("$.data.oauth_provider").value(userCreateDto.getOauthProvider())); + } + + @Test + void 유저가_생성요청한_닉네임이_중복될경우_400을_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("nickname"); + given(userService.createUser(any())).willThrow(new BadRequestException("중복된 닉네임 입니다.")); + // when + // then + mockMvc.perform(post("/api/users") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("중복된 닉네임 입니다.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 유저의_닉네임이_길이가_짧으면_400을_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("0"); + // when + // then + mockMvc.perform(post("/api/users") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("닉네임은 2자 이상 20자 이하로 입력해주세요.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 유저의_닉네임_길이가_길면_400을_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("123456789012345678901"); + // when + // then + mockMvc.perform(post("/api/users") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("닉네임은 2자 이상 20자 이하로 입력해주세요.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 유저의_닉네임이_양식에_맞지_않으면_400을_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("nickname!"); + // when + // then + mockMvc.perform(post("/api/users") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("닉네임은 영문 대소문자, 한글, 숫자로만 입력해주세요.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + } + + @Nested + @DisplayName("PUT /api/users/{user_id}") + public class UpdateUserTest { + @Test + void 유저가_수정요청을_할경우_수정된_유저의_정보를_반환한다() throws Exception { + // given + final UserUpdateReqDto userUpdateReqDto = new UserUpdateReqDto("nickname"); + final UserDto userDto = new UserDto( + 1L, + "name", + userUpdateReqDto.getNickname(), + "email", + "oauthProvider", + "accessToken", + Grade.USER, + LocalDateTime.now(), + LocalDateTime.now() + ); + given(userService.updateUser(anyLong(), any(UserUpdateReqDto.class))).willReturn(userDto); + // when + // then + mockMvc.perform(put("/api/users/1") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userUpdateReqDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status_code").value(StatusCode.OK.getCode())) + .andExpect(jsonPath("$.message").value(StatusCode.OK.getMessage())) + .andExpect(jsonPath("$.data.user_id").value(userDto.getUserId())) + .andExpect(jsonPath("$.data.name").value(userDto.getName())) + .andExpect(jsonPath("$.data.email").value(userDto.getEmail())) + .andExpect(jsonPath("$.data.nickname").value(userDto.getNickname())) + .andExpect(jsonPath("$.data.oauth_provider").value(userDto.getOauthProvider())); + } + + @Test + void 유저가_중복된_닉네임으로_수정요청을_할_경우_400을_반환한다() throws Exception { + // given + final UserUpdateReqDto userUpdateReqDto = new UserUpdateReqDto("nickname"); + given(userService.updateUser(anyLong(), any(UserUpdateReqDto.class))) + .willThrow(new BadRequestException("중복된 닉네임 입니다.")); + // when + // then + mockMvc.perform(put("/api/users/1") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userUpdateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("중복된 닉네임 입니다.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 유저의_닉네임이_길이가_짧으면_400을_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("0"); + // when + // then + mockMvc.perform(put("/api/users/1") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("닉네임은 2자 이상 20자 이하로 입력해주세요.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 유저의_닉네임_길이가_길면_400을_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("123456789012345678901"); + // when + // then + mockMvc.perform(put("/api/users/1") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("닉네임은 2자 이상 20자 이하로 입력해주세요.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 유저의_닉네임이_양식에_맞지_않으면_400을_반환한다() throws Exception { + // given + final UserCreateReqDto userCreateReqDto = new UserCreateReqDto("nickname!"); + // when + // then + mockMvc.perform(put("/api/users/1") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userCreateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("닉네임은 영문 대소문자, 한글, 숫자로만 입력해주세요.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + } + + @Nested + @DisplayName("DELETE /api/users/{user_id}") + public class DeleteUserTest { + @Test + void 유저가_삭제요청을_할경우_성공메세지를_반환한다() throws Exception { + // given + final Long userId = 1L; + // when + // then + mockMvc.perform(delete("/api/users/{user_id}", userId) + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status_code").value(StatusCode.OK.getCode())) + .andExpect(jsonPath("$.message").value(StatusCode.OK.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 존재하지않는_유저아이디를_입력하면_400을_응답한다() throws Exception { + // given + final Long userId = 1L; + given(userService.deleteUser(any(Long.class))).willThrow(new BadRequestException("존재하지 않는 유저입니다.")); + // when + // then + mockMvc.perform(delete("/api/users/{user_id}", userId) + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("존재하지 않는 유저입니다.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + } + + @Nested + @DisplayName("GET /users/me") + public class GetMeTest { + @Test + void 내정보를_요청하면_내정보를_반환한다() throws Exception { + // given + final Long userId = 1L; + final UserDto userDto = new UserDto( + userId, + "name", + "nickname", + "email", + "oauthProvider", + "accessToken", + Grade.USER, + LocalDateTime.now(), + LocalDateTime.now() + ); + final UserRespDto expect = UserRespDto.of(userDto); + given(userService.getUser(userId)).willReturn(userDto); + // when + // then + mockMvc.perform(get("/api/users/me") + .with(oauth2Login().oauth2User(oAuth2User))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status_code").value(StatusCode.OK.getCode())) + .andExpect(jsonPath("$.message").value(StatusCode.OK.getMessage())) + .andExpect(jsonPath("$.data.user_id").value(expect.getUserId())) + .andExpect(jsonPath("$.data.email").value(expect.getEmail())) + .andExpect(jsonPath("$.data.name").value(expect.getName())) + .andExpect(jsonPath("$.data.nickname").value(expect.getNickname())) + .andExpect(jsonPath("$.data.oauth_provider").value(expect.getOauthProvider())); + } + } + + @Nested + @DisplayName("PUT /users/me") + public class UpdateMeTest { + @Test + void 내정보를_수정하면_수정된_내정보를_반환한다() throws Exception { + // given + final UserUpdateReqDto userUpdateReqDto = new UserUpdateReqDto("nickname"); + final UserDto userDto = new UserDto( + 1L, + "name", + userUpdateReqDto.getNickname(), + "email", + "oauthProvider", + "accessToken", + Grade.USER, + LocalDateTime.now(), + LocalDateTime.now() + ); + given(userService.updateUser(any(Long.class), any(UserUpdateReqDto.class))).willReturn(userDto); + // when + // then + mockMvc.perform(put("/api/users/me") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userUpdateReqDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status_code").value(StatusCode.OK.getCode())) + .andExpect(jsonPath("$.message").value(StatusCode.OK.getMessage())) + .andExpect(jsonPath("$.data.user_id").value(userDto.getUserId())) + .andExpect(jsonPath("$.data.name").value(userDto.getName())) + .andExpect(jsonPath("$.data.email").value(userDto.getEmail())) + .andExpect(jsonPath("$.data.nickname").value(userDto.getNickname())) + .andExpect(jsonPath("$.data.oauth_provider").value(userDto.getOauthProvider())); + } + + @Test + void 내정보를_수정요청한_닉네임이_중복될경우_400을_반환한다() throws Exception { + // given + final UserUpdateReqDto userUpdateReqDto = new UserUpdateReqDto("nickname"); + given(userService.updateUser(anyLong(), any(UserUpdateReqDto.class))) + .willThrow(new BadRequestException("중복된 닉네임 입니다.")); + // when + // then + mockMvc.perform(put("/api/users/me") + .with(oauth2Login().oauth2User(oAuth2User)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userUpdateReqDto))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status_code").value(400)) + .andExpect(jsonPath("$.message").value("중복된 닉네임 입니다.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + } +} \ No newline at end of file diff --git a/server/src/test/java/com/talkka/server/user/service/UserServiceTest.java b/server/src/test/java/com/talkka/server/user/service/UserServiceTest.java index 8f138d1f..f7f03147 100644 --- a/server/src/test/java/com/talkka/server/user/service/UserServiceTest.java +++ b/server/src/test/java/com/talkka/server/user/service/UserServiceTest.java @@ -32,15 +32,17 @@ class UserServiceTest { private UserRepository userRepository; private UserDto userDtoFixture(Long userId) { - return UserDto.builder() - .userId(userId) - .nickname("nickname" + userId) - .oauthProvider("oauthProvider") - .accessToken("accessToken") - .grade(Grade.USER) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) - .build(); + return new UserDto( + userId, + "name", + "email", + "nickname", + "oauthProvider", + "accessToken", + Grade.USER, + LocalDateTime.now(), + LocalDateTime.now() + ); } @Nested @@ -49,10 +51,10 @@ public class GetUserTest { @Test public void 유저의_아이디를_받아_유저를_반환한다() { // given - final UserDto userDto = userDtoFixture(1L); + UserDto userDto = userDtoFixture(1L); given(userRepository.findById(1L)).willReturn(Optional.of(userDto.toEntity())); // when - final var result = userService.getUser(1L); + var result = userService.getUser(1L); // then assertThat(result).isEqualTo(result); } @@ -60,7 +62,7 @@ public class GetUserTest { @Test void 존재하지_않는_유저를_조회할_경우_Exception을_throw_한다() { // given - final Class exceptionClass = NotFoundException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 + Class exceptionClass = NotFoundException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 given(userRepository.findById(1L)).willReturn(Optional.empty()); // when // then @@ -74,32 +76,42 @@ public class CreateUserTest { @Test void 제안된_요청에_따라_유저를_생성한다() { // given - final UserCreateDto userCreateDto = UserCreateDto.builder() - .oauthProvider("oauthProvider") - .accessToken("accessToken") - .grade(Grade.USER) - .nickname("nickname") - .build(); - final UserDto resultDto = userDtoFixture(1L); + UserCreateDto userCreateDto = new UserCreateDto( + "name", + "email", + "nickname", + "oauthProvider", + "accessToken", + Grade.USER + ); + UserDto resultDto = userDtoFixture(1L); given(userRepository.save(any(UserEntity.class))).willReturn(resultDto.toEntity()); // when - final var result = userService.createUser(userCreateDto); + var result = userService.createUser(userCreateDto); // then - assertThat(result).isEqualTo(resultDto); + assertThat(result.getUserId()).isEqualTo(1L); + assertThat(result.getName()).isEqualTo(userCreateDto.getName()); + assertThat(result.getEmail()).isEqualTo(userCreateDto.getEmail()); + assertThat(result.getNickname()).isEqualTo(userCreateDto.getNickname()); + assertThat(result.getOauthProvider()).isEqualTo(userCreateDto.getOauthProvider()); + assertThat(result.getAccessToken()).isEqualTo(userCreateDto.getAccessToken()); + assertThat(result.getGrade()).isEqualTo(userCreateDto.getGrade()); } @Test void 중복된_닉네임이_이미_존재하는_경우_Exception을_throw_한다() { // given - final UserDto userDto = userDtoFixture(1L); - final UserCreateDto userCreateDto = UserCreateDto.builder() - .oauthProvider(userDto.getOauthProvider()) - .accessToken(userDto.getAccessToken()) - .grade(userDto.getGrade()) - .nickname(userDto.getNickname()) - .build(); - final Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 - given(userService.isDuplicatedNickname(userDto.getNickname())).willReturn(true); + UserDto userDto = userDtoFixture(1L); + UserCreateDto userCreateDto = new UserCreateDto( + "name", + "email", + "oauthProvider", + userDto.getNickname(), + "accessToken", + Grade.USER + ); + Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 + given(userService.isDuplicatedNickname(any())).willReturn(true); // when // then assertThatThrownBy(() -> userService.createUser(userCreateDto)).isInstanceOf(exceptionClass); @@ -112,10 +124,10 @@ public class isDuplicatedNicknameTest { @Test void 닉네임이_중복되지_않는경우_false를_반환한다() { // given - final String nickname = "nickname"; + String nickname = "nickname"; given(userRepository.existsByNickname(nickname)).willReturn(false); // when - final var result = userService.isDuplicatedNickname(nickname); + var result = userService.isDuplicatedNickname(nickname); // then assertThat(result).isFalse(); } @@ -123,10 +135,10 @@ public class isDuplicatedNicknameTest { @Test void 닉네임이_중복되는경우_true를_반환한다() { // given - final String nickname = "nickname"; + String nickname = "nickname"; given(userRepository.existsByNickname(nickname)).willReturn(true); // when - final var result = userService.isDuplicatedNickname(nickname); + var result = userService.isDuplicatedNickname(nickname); // then assertThat(result).isTrue(); } @@ -138,36 +150,32 @@ public class UpdateUserTest { @Test void 유저가_수정할_정보를_요청하면_수정한_정보를_반환한다() { // given - final UserUpdateReqDto reqDto = UserUpdateReqDto.builder() + UserUpdateReqDto reqDto = UserUpdateReqDto.builder() .nickname("nickname2") .build(); - final UserDto findDto = userDtoFixture(1L); - final UserEntity findEntity = findDto.toEntity(); - final UserEntity updatedEntity = UserEntity.builder() - .userId(1L) - .nickname(reqDto.getNickname()) - .oauthProvider(findEntity.getOauthProvider()) - .accessToken(findEntity.getAccessToken()) - .grade(findEntity.getGrade()) - .createdAt(findEntity.getCreatedAt()) - .updatedAt(findEntity.getUpdatedAt()) - .build(); + UserDto findDto = userDtoFixture(1L); + UserEntity findEntity = findDto.toEntity(); + LocalDateTime now = LocalDateTime.now(); given(userRepository.findById(1L)).willReturn(Optional.of(findEntity)); - given(userRepository.save(updatedEntity)).willReturn(updatedEntity); // when - final var result = userService.updateUser(1L, reqDto); + var result = userService.updateUser(1L, reqDto); // then - assertThat(result).isEqualTo(UserDto.of(updatedEntity)); + assertThat(result.getUserId()).isEqualTo(1L); + assertThat(result.getNickname()).isEqualTo("nickname2"); + assertThat(result.getOauthProvider()).isEqualTo(findDto.getOauthProvider()); + assertThat(result.getAccessToken()).isEqualTo(findDto.getAccessToken()); + assertThat(result.getGrade()).isEqualTo(Grade.USER); + assertThat(result.getCreatedAt()).isEqualTo(findDto.getCreatedAt()); } @Test void 존재하지_않는_유저를_수정할_경우_Exception을_throw_한다() { // given - final UserUpdateReqDto reqDto = UserUpdateReqDto.builder() + UserUpdateReqDto reqDto = UserUpdateReqDto.builder() .nickname("nickname2") .build(); - final Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 + Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 given(userRepository.findById(1L)).willReturn(Optional.empty()); // when // then @@ -177,12 +185,10 @@ public class UpdateUserTest { @Test void 중복된_닉네임으로_수정할_경우_Exception을_throw_한다() { // given - final UserUpdateReqDto reqDto = UserUpdateReqDto.builder() - .nickname("nickname2") - .build(); - final Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 - final UserDto findDto = userDtoFixture(1L); - final UserEntity findEntity = findDto.toEntity(); + UserUpdateReqDto reqDto = new UserUpdateReqDto("nickname2"); + Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 + UserDto findDto = userDtoFixture(1L); + UserEntity findEntity = findDto.toEntity(); given(userRepository.findById(1L)).willReturn(Optional.of(findEntity)); given(userService.isDuplicatedNickname(reqDto.getNickname())).willReturn(true); // when @@ -200,15 +206,16 @@ public class DeleteUserTest { // given given(userRepository.existsById(1L)).willReturn(true); // when - userService.deleteUser(1L); + Long result = userService.deleteUser(1L); // then then(userRepository).should().deleteById(1L); + assertThat(result).isEqualTo(1L); } @Test void 존재하지_않는_유저를_삭제할_경우_Exception을_throw_한다() { // given - final Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 + Class exceptionClass = BadRequestException.class; // 추후 변경될 가능성이 있어, 변수로 따로 지정함 given(userRepository.existsById(1L)).willReturn(false); // when // then diff --git a/server/src/test/resources/application.yaml b/server/src/test/resources/application.yaml new file mode 100644 index 00000000..e3ca91ba --- /dev/null +++ b/server/src/test/resources/application.yaml @@ -0,0 +1,40 @@ +spring: + application: + name: talkka-server + jackson: + property-naming-strategy: SNAKE_CASE + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${MYSQL_URL} + username: ${MYSQL_USERNAME} + password: ${MYSQL_PASSWORD} + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.MySQL8Dialect + show-sql: true + security: + oauth2: + client: + registration: + naver: + client-id: ${NAVER_CLIENT_ID} + client-secret: ${NAVER_CLINET_SECRET} + redirect_uri: http://localhost:8080/login/oauth2/code/naver + client-name: Naver + authorization-grant-type: authorization_code + scope: + - name + - email + provider: + naver: + authorization-uri: https://nid.naver.com/oauth2.0/authorize + token-uri: https://nid.naver.com/oauth2.0/token + user-info-uri: https://openapi.naver.com/v1/nid/me + user-name-attribute: response + thymeleaf: + prefix: classpath:/templates/ + suffix: .html \ No newline at end of file