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

✨ 사용자 아이디/이름 수정 API #59

Merged
merged 25 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fc81deb
test: 사용자 계정 api 내부 클래스로 분리
psychology50 Apr 25, 2024
f085777
test: 사용자 이름 수정 controller unit pre-condition 작성
psychology50 Apr 25, 2024
70e7174
test: 이름 수정 요청 controller unit test case 작성
psychology50 Apr 25, 2024
ded4d5f
fix: 일반 회원가입 계정이 아닌 예외 상수 추가 : 4004
psychology50 Apr 25, 2024
8b3ea37
feat: 이름 변경 요청 dto 정의
psychology50 Apr 25, 2024
02cea1d
feat: put_name() controller 메서드 추가
psychology50 Apr 25, 2024
4a21366
feat: update_name() usecase 추가 및 void 타입에 맞게 테스트 코드 given 수정
psychology50 Apr 25, 2024
b92b464
test: 422 예상 에러 코드 수정
psychology50 Apr 25, 2024
e7c4344
test: 사용자 계정 usecase 기존 test 내부 클래스로 분리
psychology50 Apr 25, 2024
137bf4b
test: 일반 회원가입 유저 pre-condition 제거
psychology50 Apr 25, 2024
01e0b46
test: 이름 수정 usecase test case 작성
psychology50 Apr 25, 2024
bc5d849
feat: 사용자 이름 수정 로직 구현
psychology50 Apr 25, 2024
8e38688
feat: user 도메인 이름 수정 메서드 추가
psychology50 Apr 25, 2024
7adec31
test: user_account_use_case_test 순서 지정
psychology50 Apr 25, 2024
f88bbf3
fix: 디바이스 비활성화 em.create_query() -> 메서드 호출 (기존 방식 에러 발생)
psychology50 Apr 25, 2024
8850e29
test: 사용자 닉네임 수정 controller unit test 작성
psychology50 Apr 25, 2024
814bccd
feat: 사용자 아이디 변경 요청 dto 작성
psychology50 Apr 25, 2024
d2176ee
feat: 사용자 아이디 변경 요청 api 작성
psychology50 Apr 25, 2024
905c552
feat: 사용자 아이디 변경 요청 usecase 작성
psychology50 Apr 25, 2024
0064dc0
feat: 사용자 아이디 변경 service 로직 구현
psychology50 Apr 25, 2024
e2d2159
test: nickname -> username
psychology50 Apr 25, 2024
4976259
test: 테스트 코드에서 entitymanager 주입 제거
psychology50 Apr 25, 2024
c2051ca
refactor: user account use case 사용자 조회 메서드 분리
psychology50 Apr 25, 2024
7baf4e9
fix: 이름 및 아이디 수정 요청 메서드 put -> patch
psychology50 Apr 25, 2024
87e73d9
test: put 요청 patch로 변경
psychology50 Apr 25, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.validation.constraints.NotBlank;
import kr.co.pennyway.api.apis.users.dto.DeviceDto;
import kr.co.pennyway.api.apis.users.dto.UserProfileDto;
import kr.co.pennyway.api.apis.users.dto.UserProfileUpdateDto;
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand Down Expand Up @@ -60,4 +61,10 @@ public interface UserAccountApi {
@Operation(summary = "사용자 계정 조회", description = "사용자 본인의 계정 정보를 조회합니다.")
@ApiResponse(responseCode = "200", content = @Content(schemaProperties = @SchemaProperty(name = "user", schema = @Schema(implementation = UserProfileDto.class))))
ResponseEntity<?> getMyAccount(@AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "사용자 이름 수정")
ResponseEntity<?> putName(@RequestBody @Validated UserProfileUpdateDto.NameReq request, @AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "사용자 아이디 수정")
ResponseEntity<?> putUsername(@RequestBody @Validated UserProfileUpdateDto.UsernameReq request, @AuthenticationPrincipal SecurityUserDetails user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.constraints.NotBlank;
import kr.co.pennyway.api.apis.users.api.UserAccountApi;
import kr.co.pennyway.api.apis.users.dto.DeviceDto;
import kr.co.pennyway.api.apis.users.dto.UserProfileUpdateDto;
import kr.co.pennyway.api.apis.users.usecase.UserAccountUseCase;
import kr.co.pennyway.api.common.response.SuccessResponse;
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails;
Expand All @@ -21,22 +22,41 @@
public class UserAccountController implements UserAccountApi {
private final UserAccountUseCase userAccountUseCase;

@Override
@PutMapping("/devices")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> putDevice(@RequestBody @Validated DeviceDto.RegisterReq request, @AuthenticationPrincipal SecurityUserDetails user) {
return ResponseEntity.ok(SuccessResponse.from("device", userAccountUseCase.registerDevice(user.getUserId(), request)));
}

@Override
@DeleteMapping("/devices")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> deleteDevice(@RequestParam("token") @Validated @NotBlank String token, @AuthenticationPrincipal SecurityUserDetails user) {
userAccountUseCase.unregisterDevice(user.getUserId(), token);
return ResponseEntity.ok(SuccessResponse.noContent());
}

@Override
@GetMapping("")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> getMyAccount(@AuthenticationPrincipal SecurityUserDetails user) {
return ResponseEntity.ok(SuccessResponse.from("user", userAccountUseCase.getMyAccount(user.getUserId())));
}

@Override
@PutMapping("/name")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> putName(UserProfileUpdateDto.NameReq request, SecurityUserDetails user) {
userAccountUseCase.updateName(user.getUserId(), request.name());
return ResponseEntity.ok(SuccessResponse.noContent());
}

@Override
@PutMapping("/username")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> putUsername(UserProfileUpdateDto.UsernameReq request, SecurityUserDetails user) {
userAccountUseCase.updateUsername(user.getUserId(), request.username());
return ResponseEntity.ok(SuccessResponse.noContent());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.co.pennyway.api.apis.users.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public class UserProfileUpdateDto {
@Schema(title = "이름 변경 요청 DTO")
public record NameReq(
@Schema(description = "이름", example = "페니웨이")
@NotBlank(message = "이름을 입력해주세요")
@Pattern(regexp = "^[가-힣a-z]{2,8}$", message = "2~8자의 한글, 영문 소문자만 사용 가능합니다.")
String name
) {
}

@Schema(title = "아이디 변경 요청 DTO")
public record UsernameReq(
@Schema(description = "아이디", example = "pennyway")
@NotBlank(message = "아이디를 입력해주세요")
@Pattern(regexp = "^[a-z-_.]{5,20}$", message = "5~20자의 영문 소문자, -, _, . 만 사용 가능합니다.")
String username
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.co.pennyway.api.apis.users.service;

import kr.co.pennyway.domain.domains.user.domain.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserProfileUpdateService {
@Transactional
public void updateName(User user, String newName) {
user.updateName(newName);
}

@Transactional
public void updateUsername(User user, String newUsername) {
user.updateUsername(newUsername);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import kr.co.pennyway.api.apis.users.dto.DeviceDto;
import kr.co.pennyway.api.apis.users.dto.UserProfileDto;
import kr.co.pennyway.api.apis.users.service.DeviceRegisterService;
import kr.co.pennyway.api.apis.users.service.UserProfileUpdateService;
import kr.co.pennyway.common.annotation.UseCase;
import kr.co.pennyway.domain.domains.device.domain.Device;
import kr.co.pennyway.domain.domains.device.exception.DeviceErrorCode;
Expand All @@ -23,6 +24,8 @@ public class UserAccountUseCase {
private final UserService userService;
private final DeviceService deviceService;

private final UserProfileUpdateService userProfileUpdateService;

private final DeviceRegisterService deviceRegisterService;

@Transactional
Expand Down Expand Up @@ -57,4 +60,22 @@ public UserProfileDto getMyAccount(Long userId) {

return UserProfileDto.from(user);
}

@Transactional
public void updateName(Long userId, String newName) {
User user = userService.readUser(userId).orElseThrow(
() -> new UserErrorException(UserErrorCode.NOT_FOUND)
);

userProfileUpdateService.updateName(user, newName);
}

@Transactional
public void updateUsername(Long userId, String newUsername) {
User user = userService.readUser(userId).orElseThrow(
() -> new UserErrorException(UserErrorCode.NOT_FOUND)
);

userProfileUpdateService.updateUsername(user, newUsername);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import kr.co.pennyway.api.apis.users.dto.DeviceDto;
import kr.co.pennyway.api.apis.users.dto.UserProfileUpdateDto;
import kr.co.pennyway.api.apis.users.usecase.UserAccountUseCase;
import kr.co.pennyway.api.config.supporter.WithSecurityMockUser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import kr.co.pennyway.common.exception.StatusCode;
import kr.co.pennyway.domain.domains.user.exception.UserErrorCode;
import kr.co.pennyway.domain.domains.user.exception.UserErrorException;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
Expand All @@ -16,7 +18,9 @@
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static kr.co.pennyway.common.exception.ReasonCode.REQUIRED_PARAMETERS_MISSING_IN_REQUEST_BODY;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
Expand All @@ -26,6 +30,7 @@

@WebMvcTest(controllers = {UserAccountController.class})
@ActiveProfiles("local")
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
public class UserAccountControllerUnitTest {
@Autowired
private MockMvc mockMvc;
Expand All @@ -45,25 +50,186 @@ void setUp(WebApplicationContext webApplicationContext) {
.build();
}

@DisplayName("[1] 디바이스가 정상적으로 저장되었을 때, 디바이스 pk와 등록된 토큰을 반환한다.")
@Test
@WithSecurityMockUser
void putDeviceSuccess() throws Exception {
// given
DeviceDto.RegisterReq request = new DeviceDto.RegisterReq("newToken", "newToken", "modelA", "Windows");
DeviceDto.RegisterRes expectedResponse = new DeviceDto.RegisterRes(2L, "newToken");
given(userAccountUseCase.registerDevice(1L, request)).willReturn(expectedResponse);

// when
ResultActions result = mockMvc.perform(put("/v2/users/me/devices")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)));

// then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("2000"))
.andExpect(jsonPath("$.data.device.id").value(expectedResponse.id()))
.andExpect(jsonPath("$.data.device.token").value(expectedResponse.token()))
.andDo(print());
@Nested
@Order(1)
@DisplayName("[1] 디바이스 요청 테스트")
class DeviceRequestTest {
@DisplayName("디바이스가 정상적으로 저장되었을 때, 디바이스 pk와 등록된 토큰을 반환한다.")
@Test
@WithSecurityMockUser
void putDevice() throws Exception {
// given
DeviceDto.RegisterReq request = new DeviceDto.RegisterReq("newToken", "newToken", "modelA", "Windows");
DeviceDto.RegisterRes expectedResponse = new DeviceDto.RegisterRes(2L, "newToken");
given(userAccountUseCase.registerDevice(1L, request)).willReturn(expectedResponse);

// when
ResultActions result = mockMvc.perform(put("/v2/users/me/devices")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)));

// then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("2000"))
.andExpect(jsonPath("$.data.device.id").value(expectedResponse.id()))
.andExpect(jsonPath("$.data.device.token").value(expectedResponse.token()))
.andDo(print());
}
}

@Nested
@Order(2)
@DisplayName("[2] 사용자 이름 수정 테스트")
class UpdateNameTest {
@DisplayName("사용자 이름 수정 요청 시, 유효성 검사에 실패하면 422 에러를 반환한다.")
@Test
@WithSecurityMockUser
void updateNameValidationFail() throws Exception {
// given
String newNameWithBlank = " ";
String newNameWithOverLength = "안녕하세요장페르센입니다";
String newNameWithSpecialCharacter = "hello!";
String expectedErrorCode = String.valueOf(StatusCode.UNPROCESSABLE_CONTENT.getCode() * 10 + REQUIRED_PARAMETERS_MISSING_IN_REQUEST_BODY.getCode());

// when
ResultActions result1 = performUpdateNameRequest(newNameWithBlank);
ResultActions result2 = performUpdateNameRequest(newNameWithOverLength);
ResultActions result3 = performUpdateNameRequest(newNameWithSpecialCharacter);

// then
result1.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
result2.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
result3.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
}

@DisplayName("사용자 이름 수정 요청 시, 삭제된 사용자인 경우 404 에러를 반환한다.")
@Test
@WithSecurityMockUser
void updateNameDeletedUser() throws Exception {
// given
String newName = "양재서";
willThrow(new UserErrorException(UserErrorCode.NOT_FOUND)).given(userAccountUseCase).updateName(1L, newName);

// when
ResultActions result = performUpdateNameRequest(newName);

// then
result.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(UserErrorCode.NOT_FOUND.causedBy().getCode()))
.andExpect(jsonPath("$.message").value(UserErrorCode.NOT_FOUND.getExplainError()))
.andDo(print());
}

@DisplayName("사용자 이름 수정 요청 시, 사용자 이름이 정상적으로 수정되면 200 코드를 반환한다.")
@Test
@WithSecurityMockUser
void updateNameSuccess() throws Exception {
// given
String newName = "양재서";

// when
ResultActions result = performUpdateNameRequest(newName);

// then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("2000"))
.andDo(print());
}

private ResultActions performUpdateNameRequest(String newName) throws Exception {
UserProfileUpdateDto.NameReq request = new UserProfileUpdateDto.NameReq(newName);
return mockMvc.perform(put("/v2/users/me/name")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)));
}
}

@Nested
@Order(3)
@DisplayName("[3] 사용자 닉네임 수정 테스트")
class UpdateNicknameTest {
@DisplayName("사용자 닉네임 수정 요청 시, 유효성 검사에 실패하면 422 에러를 반환한다.")
@Test
@WithSecurityMockUser
void updateNicknameValidationFail() throws Exception {
// given
String newNicknameWithBlank = " ";
String newNicknameWithOverLength = "한글이름";
String newNicknameWithSpecialCharacter = "hello!";
String newNicknameWithWhiteSpace = "jay ang";
String newNicknameWithOverLengthAndWhiteSpace = "myNameisJayangHello";
String expectedErrorCode = String.valueOf(StatusCode.UNPROCESSABLE_CONTENT.getCode() * 10 + REQUIRED_PARAMETERS_MISSING_IN_REQUEST_BODY.getCode());

// when
ResultActions result1 = performUpdateNicknameRequest(newNicknameWithBlank);
ResultActions result2 = performUpdateNicknameRequest(newNicknameWithOverLength);
ResultActions result3 = performUpdateNicknameRequest(newNicknameWithSpecialCharacter);
ResultActions result4 = performUpdateNicknameRequest(newNicknameWithWhiteSpace);
ResultActions result5 = performUpdateNicknameRequest(newNicknameWithOverLengthAndWhiteSpace);

// then
result1.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
result2.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
result3.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
result4.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
result5.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.code").value(expectedErrorCode))
.andDo(print());
}

@DisplayName("사용자 닉네임 수정 요청 시, 삭제된 사용자인 경우 404 에러를 반환한다.")
@Test
@WithSecurityMockUser
void updateNicknameDeletedUser() throws Exception {
// given
String newNickname = "jayang._.";
willThrow(new UserErrorException(UserErrorCode.NOT_FOUND)).given(userAccountUseCase).updateUsername(1L, newNickname);

// when
ResultActions result = performUpdateNicknameRequest(newNickname);

// then
result.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(UserErrorCode.NOT_FOUND.causedBy().getCode()))
.andExpect(jsonPath("$.message").value(UserErrorCode.NOT_FOUND.getExplainError()))
.andDo(print());
}

@DisplayName("사용자 닉네임 수정 요청 시, 사용자 닉네임이 정상적으로 수정되면 200 코드를 반환한다.")
@Test
@WithSecurityMockUser
void updateNicknameSuccess() throws Exception {
// given
String newNickname = "jayang._.";

// when
ResultActions result = performUpdateNicknameRequest(newNickname);

// then
result.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("2000"))
.andDo(print());
}

private ResultActions performUpdateNicknameRequest(String newNickname) throws Exception {
UserProfileUpdateDto.UsernameReq request = new UserProfileUpdateDto.UsernameReq(newNickname);
return mockMvc.perform(put("/v2/users/me/username")
.contentType("application/json")
.content(objectMapper.writeValueAsString(request)));
}
}
}
Loading
Loading