Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserUpdateNicknameRequest {

@NotBlank(message = "닉네임은 필수입니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
@AllArgsConstructor
public enum UserErrorCode implements ErrorCode {

// 1xx - User 상태 / 중복
// 10x - User 상태 / 중복
NICKNAME_DUPLICATED(HttpStatus.CONFLICT, "U-100", "이미 사용 중인 닉네임입니다."),
USER_DELETED(HttpStatus.FORBIDDEN, "U-101", "탈퇴한 사용자입니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U-102", "사용자를 찾을 수 없습니다."),
USER_RESTORE_EXPIRED(HttpStatus.BAD_REQUEST, "U-103", "계정 복구 가능 기간이 만료되었습니다."),
USER_NOT_DELETED(HttpStatus.BAD_REQUEST, "U-104", "탈퇴 상태의 계정만 복구할 수 있습니다."),
INVALID_RESTORE_TOKEN(HttpStatus.BAD_REQUEST, "U-105", "유효하지 않거나 만료된 복구 링크입니다."),

// 2xx - 인증 / 비밀번호
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "U-120", "현재 비밀번호가 일치하지 않습니다.");
// 11x - 비밀번호
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "U-110", "현재 비밀번호가 일치하지 않습니다.");

private final HttpStatus status;
private final String code;
Expand Down
50 changes: 47 additions & 3 deletions src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@
spring:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api관련 테스트를 전부 따로 넣으셨네요. 쓰지 않을 기능에 대해서 따로 환경변수를 넣지 않아도 되어서 편리할 것 같습니다

datasource:
url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
data:
redis:
host: localhost
port: 6379

spotify:
client-id: test-id
client-secret: test-secret

jwt:
secret: c2VjcmV0LWtleS1mb3ItdGVzdGluZy1wdXJwb3Nlcy1vbmx5LW5vdC1mb3ItcHJvZHVjdGlvbg==
access-token-expiration: 3600
refresh-token-expiration: 3600

mailgun:
api-key: test
domain: test
from: test@test.com

tmap:
api-key: test
kakao:
restapi-key: test
kopis:
api-key: test

oauth:
kakao:
client-id: test
client-secret: test
redirect-uri: test
google:
client-id: test
client-secret: test
redirect-uri: test

cloud:
aws:
s3:
bucket: test-bucket
Credentials:
accessKey: test
secretKey: test
credentials:
access-key: test
secret-key: test
region:
static: ap-northeast-2
stack:
auto: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package com.back.web7_9_codecrete_be.domain.users.controller;

import com.back.web7_9_codecrete_be.domain.auth.service.TokenService;
import com.back.web7_9_codecrete_be.domain.users.dto.response.UserResponse;
import com.back.web7_9_codecrete_be.domain.users.entity.User;
import com.back.web7_9_codecrete_be.domain.users.service.UserService;
import com.back.web7_9_codecrete_be.global.error.code.UserErrorCode;
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
import com.back.web7_9_codecrete_be.global.rq.Rq;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Transactional
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@MockBean
private UserService userService;

@MockBean
private TokenService tokenService;

@MockBean
private Rq rq;

private User mockUser;
private UserResponse mockUserResponse;

@BeforeEach
void setUp() {
mockUser = User.builder()
.email("test@example.com")
.nickname("testUser")
.build();

mockUserResponse = UserResponse.builder()
.email("test@example.com")
.nickname("testUser")
.profileImageUrl("https://example.com/profile.jpg")
.build();

// Rq가 사용될 때 가짜 유저 반환 설정
lenient().when(rq.getUser()).thenReturn(mockUser);
}

// 성공 테스트 시나리오

@Test
@WithMockUser
@DisplayName("내 정보 조회 성공")
void getMyInfo_Success() throws Exception {
given(userService.getMyInfo(any())).willReturn(mockUserResponse);

mockMvc.perform(get("/api/v1/users/me"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("OK"))
.andExpect(jsonPath("$.msg").value("사용자 정보 조회 성공"))
.andExpect(jsonPath("$.data.email").value("test@example.com"));
}

@Test
@WithMockUser
@DisplayName("닉네임 수정 성공")
void updateNickname_Success() throws Exception {
String jsonRequest = "{\"nickname\": \"newNickname\"}";
UserResponse updatedResponse = UserResponse.builder().nickname("newNickname").build();

given(userService.updateNickname(any(), any())).willReturn(updatedResponse);

mockMvc.perform(patch("/api/v1/users/nickname")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("OK"))
.andExpect(jsonPath("$.data.nickname").value("newNickname"));
}

@Test
@WithMockUser
@DisplayName("프로필 이미지 수정 성공")
void updateProfileImage_Success() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "profile.jpg", MediaType.IMAGE_JPEG_VALUE, "content".getBytes());

given(userService.updateProfileImage(any(), any())).willReturn("https://new-url.com");

mockMvc.perform(multipart("/api/v1/users/profile-image")
.file(file)
.with(csrf())
.with(request -> {
request.setMethod("PATCH");
return request;
}))
.andExpect(status().isOk())
.andExpect(jsonPath("$.msg").value("프로필 이미지가 변경되었습니다."));
}

@Test
@WithMockUser
@DisplayName("비밀번호 변경 성공")
void updatePassword_Success() throws Exception {

String jsonRequest = """

{

"currentPassword": "oldPassword1!",

"newPassword": "newPassword1!"

}

""";

mockMvc.perform(patch("/api/v1/users/password")

.with(csrf())

.contentType(MediaType.APPLICATION_JSON)

.content(jsonRequest))

.andExpect(status().isOk());

verify(userService).updatePassword(any(), any());

}

@Test
@WithMockUser
@DisplayName("회원 탈퇴 성공")
void deleteMyAccount_Success() throws Exception {
mockMvc.perform(delete("/api/v1/users/me")
.with(csrf()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("OK"))
.andExpect(jsonPath("$.msg").value("정상적으로 처리되었습니다."))
.andExpect(jsonPath("$.data").value("회원 탈퇴가 완료되었습니다."));
}

// 실패 테스트 시나리오
Copy link
Copy Markdown
Collaborator

@ys0221 ys0221 Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원 탈퇴 실패에 대한 테스트도 있으면 좋을 것 같습니다!!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

회원 실패 케이스에 대해서 고민 해보긴 했는데 마땅히 떠오르는 부분이 없어서 조금 더 고민해보겠습니다!


@Test
@WithMockUser
@DisplayName("닉네임 수정 실패 - 중복된 닉네임 (U-100)")
void updateNickname_Fail_Duplicated() throws Exception {
doThrow(new BusinessException(UserErrorCode.NICKNAME_DUPLICATED))
.when(userService).updateNickname(any(), any());

String jsonRequest = "{\"nickname\": \"existingNick\"}";

mockMvc.perform(patch("/api/v1/users/nickname")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andDo(print())
.andExpect(status().isConflict())
.andExpect(jsonPath("$.resultCode").value("U-100"))
.andExpect(jsonPath("$.msg").value("이미 사용 중인 닉네임입니다."));
}

@Test
@WithMockUser
@DisplayName("비밀번호 변경 실패 - 현재 비밀번호 불일치 (U-110)")
void updatePassword_Fail_InvalidPassword() throws Exception {
String jsonRequest = """
{
"currentPassword": "wrongPassword1!",
"newPassword": "newPassword123!"
}
""";

doThrow(new BusinessException(UserErrorCode.INVALID_PASSWORD))
.when(userService).updatePassword(any(), any());

mockMvc.perform(patch("/api/v1/users/password")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.resultCode").value("U-110"));
}

@Test
@WithMockUser
@DisplayName("비밀번호 변경 실패 - 유효성 검사 에러 (VALIDATION_ERROR)")
void updatePassword_Fail_ValidationError() throws Exception {
String jsonRequest = "{\"currentPassword\": \"short\", \"newPassword\": \"short\"}";

mockMvc.perform(patch("/api/v1/users/password")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.resultCode").value("VALIDATION_ERROR"));
}
}

This file was deleted.

Loading