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
@@ -0,0 +1,33 @@
package org.ezcode.codetest.application.usermanagement.user.service;

import org.ezcode.codetest.application.usermanagement.user.dto.response.GrantAdminRoleResponse;
import org.ezcode.codetest.domain.user.exception.AdminException;
import org.ezcode.codetest.domain.user.exception.code.AdminExceptionCode;
import org.ezcode.codetest.domain.user.model.entity.AuthUser;
import org.ezcode.codetest.domain.user.model.entity.User;
import org.ezcode.codetest.domain.user.model.enums.UserRole;
import org.ezcode.codetest.domain.user.service.UserDomainService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AdminService {
private final UserDomainService userDomainService;

@Transactional
public GrantAdminRoleResponse grantAdminRole(AuthUser authUser, Long userId) {
if (authUser.getId().equals(userId)) {
throw new AdminException(AdminExceptionCode.GRANT_ADMIN_SELF);
}
User user = userDomainService.getUserById(userId);
if (user.getRole().equals(UserRole.ADMIN)) {
throw new AdminException(AdminExceptionCode.ALREADY_ADMIN_USER);
}
user.modifyUserRole(UserRole.ADMIN);

return new GrantAdminRoleResponse("ADMIN 권한을 부여합니다");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,4 @@ public UserProfileImageResponse deleteUserProfileImage(AuthUser authUser) {

return new UserProfileImageResponse(null);
}

@Transactional
public GrantAdminRoleResponse grantAdminRole(AuthUser authUser, Long userId) {
if (authUser.getId().equals(userId)) {
throw new UserException(UserExceptionCode.GRANT_ADMIN_SELF);
}
User user = userDomainService.getUserById(userId);
if (user.getRole().equals(UserRole.ADMIN)) {
throw new UserException(UserExceptionCode.ALREADY_ADMIN_USER);
}
user.modifyUserRole(UserRole.ADMIN);

return new GrantAdminRoleResponse("ADMIN 권한을 부여합니다");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.ezcode.codetest.domain.user.exception;

import org.ezcode.codetest.common.base.exception.ResponseCode;
import org.ezcode.codetest.domain.user.exception.code.AdminExceptionCode;
import org.springframework.http.HttpStatus;

import lombok.Getter;

@Getter
public class AdminException extends RuntimeException {
private final AdminExceptionCode responseCode;
private final HttpStatus httpStatus;
private final String message;

public AdminException(AdminExceptionCode responseCode) {
this.responseCode = responseCode;
this.httpStatus = responseCode.getStatus();
this.message = responseCode.getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.ezcode.codetest.domain.user.exception.code;

import org.ezcode.codetest.common.base.exception.ResponseCode;
import org.springframework.http.HttpStatus;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum AdminExceptionCode implements ResponseCode {
GRANT_ADMIN_SELF(false, HttpStatus.BAD_REQUEST, "본인에게 ADMIN 권한을 부여할 수 없습니다."),
ALREADY_ADMIN_USER(false, HttpStatus.BAD_REQUEST, "이미 ADMIN 권한을 가진 유저입니다.");

private final boolean success;
private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ public enum UserExceptionCode implements ResponseCode {
NOT_MATCH_CODE(false, HttpStatus.BAD_REQUEST, "이메일 인증 코드가 일치하지 않습니다."),
NO_GITHUB_INFO(false, HttpStatus.BAD_REQUEST, "깃허브 정보가 없습니다."),
NO_GITHUB_REPO(false, HttpStatus.BAD_REQUEST, "해당하는 Repository를 찾을 수 없습니다."),
GRANT_ADMIN_SELF(false, HttpStatus.BAD_REQUEST, "본인에게 ADMIN 권한을 부여할 수 없습니다."),
ALREADY_ADMIN_USER(false, HttpStatus.BAD_REQUEST, "이미 ADMIN 권한을 가진 유저입니다.");


;
private final boolean success;
private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@
import org.ezcode.codetest.domain.user.exception.UserException;
import org.ezcode.codetest.domain.user.exception.code.UserExceptionCode;
import org.ezcode.codetest.domain.user.model.entity.UserAuthType;
import org.ezcode.codetest.domain.user.model.entity.UserGithubInfo;
import org.ezcode.codetest.domain.user.model.enums.Adjective;
import org.ezcode.codetest.domain.user.model.enums.AuthType;
import org.ezcode.codetest.domain.user.model.enums.Noun;
import org.ezcode.codetest.domain.user.exception.AuthException;
import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode;
import org.ezcode.codetest.domain.user.model.entity.User;
import org.ezcode.codetest.domain.user.repository.UserAuthTypeRepository;
import org.ezcode.codetest.domain.user.repository.UserGithubInfoRepository;
import org.ezcode.codetest.domain.user.repository.UserRepository;
import org.ezcode.codetest.common.security.util.PasswordEncoder;
import org.springframework.stereotype.Service;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.ezcode.codetest.presentation.usermanagement;

import org.ezcode.codetest.application.usermanagement.user.dto.response.GrantAdminRoleResponse;
import org.ezcode.codetest.application.usermanagement.user.service.AdminService;
import org.ezcode.codetest.domain.user.model.entity.AuthUser;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/api/admin")
@RequiredArgsConstructor
@Tag(name = "관리자(Admin) 전용 기능", description = "관리자 권한을 가진 유저만 접근 가능한 기능입니다")
public class AdminController {
private final AdminService adminService;

@Operation(summary = "관리자로 전환", description = "관리자 권한을 가지고 있는 유저는 다른 유저의 권한을 관리자로 수정할 수 있습니다.")
@PostMapping("/users/{userId}/grant-admin")
public ResponseEntity<GrantAdminRoleResponse> grantAdminRole(
@AuthenticationPrincipal AuthUser authUser,
@PathVariable Long userId
){
return ResponseEntity.status(HttpStatus.OK).body(adminService.grantAdminRole(authUser, userId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,4 @@ public ResponseEntity<WithdrawUserResponse> withdraw(
return ResponseEntity.status(HttpStatus.OK).body(userService.withdrawUser(authUser));
}

@Operation(summary = "유저 권한 전환", description = "관리자 권한을 가지고 있는 유저는 다른 유저의 권한을 수정할 수 있습니다.")
@PostMapping("/admin/users/{userId}/grant-admin")
public ResponseEntity<GrantAdminRoleResponse> grantAdminRole(
@AuthenticationPrincipal AuthUser authUser,
@PathVariable Long userId
){
return ResponseEntity.status(HttpStatus.OK).body(userService.grantAdminRole(authUser, userId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package org.ezcode.codetest.domain.user;

import org.ezcode.codetest.common.security.util.PasswordEncoder;
import org.ezcode.codetest.domain.user.exception.AuthException;
import org.ezcode.codetest.domain.user.exception.UserException;
import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode;
import org.ezcode.codetest.domain.user.exception.code.UserExceptionCode;
import org.ezcode.codetest.domain.user.model.entity.User;
import org.ezcode.codetest.domain.user.model.entity.UserAuthType;
import org.ezcode.codetest.domain.user.model.enums.AuthType;
import org.ezcode.codetest.domain.user.model.enums.Tier;
import org.ezcode.codetest.domain.user.model.enums.UserRole;
import org.ezcode.codetest.domain.user.repository.UserAuthTypeRepository;
import org.ezcode.codetest.domain.user.repository.UserRepository;
import org.ezcode.codetest.domain.user.service.UserDomainService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class UserDomainServiceTest {

@Mock
private UserRepository userRepository;

@Mock
private UserAuthTypeRepository userAuthTypeRepository;

@Mock
private PasswordEncoder passwordEncoder;


@InjectMocks
private UserDomainService userDomainService;

// 테스트 유저 정보 설정
private final User testUser = new User(
"[email protected]",
"hashedPassword",
"testUser",
"TestNick",
30,
Tier.NEWBIE,
UserRole.USER,
false, // isDeleted
true, // verified
"https://github.com/test",
false // gitPushStatus
) {
public Long getId() { return 1L; }
public int getReviewToken() { return 5; }
public int getZeroReviewToken() { return 0; }
};
private final UserAuthType testAuthType = new UserAuthType(testUser, AuthType.EMAIL);
Comment on lines +46 to +63
Copy link

@coderabbitai coderabbitai bot Jul 1, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

테스트 데이터 생성 방식의 일관성을 개선하세요.

익명 내부 클래스를 사용하여 테스트 유저를 생성하고 있는데, 이는 199-208라인의 zeroTokenUser와 일관성이 맞지 않습니다. 헬퍼 메서드를 사용하여 통일된 방식으로 테스트 데이터를 생성하는 것이 좋겠습니다.

다음과 같이 헬퍼 메서드를 추가하여 일관성을 개선하세요:

+    private User createTestUserWithReviewToken(int tokenCount) {
+        User user = spy(new User(
+            "[email protected]", "hashedPassword", "testUser", "TestNick",
+            30, Tier.NEWBIE, UserRole.USER,
+            false, true, "https://github.com/test", false
+        ));
+        when(user.getId()).thenReturn(1L);
+        when(user.getReviewToken()).thenReturn(tokenCount);
+        return user;
+    }

-    private final User testUser = new User(
-        "[email protected]",
-        "hashedPassword",
-        "testUser",
-        "TestNick",
-        30,
-        Tier.NEWBIE,
-        UserRole.USER,
-        false,  // isDeleted
-        true,   // verified
-        "https://github.com/test",
-        false   // gitPushStatus
-    ) {
-        public Long getId() { return 1L; }
-        public int getReviewToken() { return 5; }
-        public int getZeroReviewToken() { return 0; }
-    };
+    private final User testUser = createTestUserWithReviewToken(5);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final User testUser = new User(
"[email protected]",
"hashedPassword",
"testUser",
"TestNick",
30,
Tier.NEWBIE,
UserRole.USER,
false, // isDeleted
true, // verified
"https://github.com/test",
false // gitPushStatus
) {
public Long getId() { return 1L; }
public int getReviewToken() { return 5; }
public int getZeroReviewToken() { return 0; }
};
private final UserAuthType testAuthType = new UserAuthType(testUser, AuthType.EMAIL);
// add this helper at the top of your test class
private User createTestUserWithReviewToken(int tokenCount) {
User user = spy(new User(
"[email protected]",
"hashedPassword",
"testUser",
"TestNick",
30,
Tier.NEWBIE,
UserRole.USER,
false, // isDeleted
true, // verified
"https://github.com/test",
false // gitPushStatus
));
when(user.getId()).thenReturn(1L);
when(user.getReviewToken()).thenReturn(tokenCount);
return user;
}
// replace the old anonymous‐subclass instantiation
private final User testUser = createTestUserWithReviewToken(5);
private final UserAuthType testAuthType = new UserAuthType(testUser, AuthType.EMAIL);
🤖 Prompt for AI Agents
In src/test/java/org/ezcode/codetest/domain/user/UserDomainServiceTest.java
around lines 46 to 63, the testUser is created using an anonymous inner class,
which is inconsistent with the zeroTokenUser creation at lines 199-208. Refactor
testUser creation by adding and using a helper method to instantiate User
objects uniformly, ensuring consistent test data creation across the class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

구차나..

Copy link

Choose a reason for hiding this comment

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

@minjee2758 네, 알겠습니다! 현재 테스트 코드 스타일을 유지하시는 것으로 하겠습니다. 테스트가 잘 작동하고 있으니 괜찮습니다. 🙂


🧠 Learnings used
Learnt from: minjee2758
PR: ezcode-my/backend#43
File: src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java:77-89
Timestamp: 2025-06-10T02:13:33.890Z
Learning: 이 프로젝트에서는 입력 검증을 DTO 레벨에서 수행하고 있으며, User 엔티티의 팩토리 메서드에서는 별도의 입력 검증을 하지 않는다.

Learnt from: chat26666
PR: ezcode-my/backend#67
File: src/main/java/org/ezcode/codetest/domain/game/service/CharacterEquipService.java:26-34
Timestamp: 2025-06-16T16:07:34.354Z
Learning: 이 프로젝트에서는 UseCase 레벨에서 트랜잭션 경계를 관리하므로, 도메인 서비스 클래스들(예: CharacterEquipService)에는 @Transactional 어노테이션이 필요하지 않습니다. 트랜잭션은 UseCase에서 열리므로 데이터 일관성이 보장됩니다.

Learnt from: NCookies
PR: ezcode-my/backend#17
File: src/main/java/org/ezcode/codetest/presentation/problemmanagement/ProblemAdminController.java:25-25
Timestamp: 2025-06-02T06:57:37.929Z
Learning: 이 프로젝트에서는 아직 Spring Security가 적용되지 않아서 @PreAuthorize 어노테이션이 주석 처리되어 있음. Spring Security 도입 전까지는 권한 검증 어노테이션들이 비활성화 상태임.

Learnt from: chat26666
PR: ezcode-my/backend#67
File: src/main/java/org/ezcode/codetest/domain/game/service/GameEncounterDomainService.java:131-134
Timestamp: 2025-06-17T15:06:54.036Z
Learning: GameEncounterDomainService의 getRandomEnemyCharacter 메소드에서 findRandomCharacter 결과가 빈 리스트일 경우에 대한 예외 처리는 불필요함. 시스템에 항상 테스트 계정이 존재하도록 설계되어 있어 빈 결과가 발생하지 않음.

Learnt from: thezz9
PR: ezcode-my/backend#30
File: src/main/java/org/ezcode/codetest/domain/problem/model/dto/SubmissionData.java:42-53
Timestamp: 2025-06-04T13:12:16.729Z
Learning: 사용자 thezz9는 개발 시 기능적 정확성을 먼저 달성한 후 세부 리팩터링을 나중에 하는 접근 방식을 선호한다. 테스트 단계에서는 하드코딩된 값도 허용한다.

Learnt from: thezz9
PR: ezcode-my/backend#30
File: src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java:44-46
Timestamp: 2025-06-04T13:12:17.636Z
Learning: SubmissionService에서 "2 10" 입력값과 "12" 기대출력이 하드코딩되어 있는 것은 테스트 단계의 의도적인 구현이며, 추후 Problem 엔티티의 실제 테스트케이스로 대체 예정입니다.

Learnt from: chat26666
PR: ezcode-my/backend#64
File: src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/RandomEncounterRepositoryImpl.java:10-16
Timestamp: 2025-06-15T04:37:22.609Z
Learning: chat26666은 Repository 구현체를 만들 때 뼈대(skeleton)를 먼저 생성하고 나중에 메서드를 구현하는 개발 방식을 사용한다. 이런 경우 아직 미구현 상태인 것이 의도적인 것일 수 있다.

Learnt from: thezz9
PR: ezcode-my/backend#114
File: src/main/java/org/ezcode/codetest/infrastructure/github/GitHubApiClient.java:159-165
Timestamp: 2025-06-28T02:03:38.654Z
Learning: User thezz9's service (ezcode) targets Korean users exclusively ("찐 한국인밖에 안 옴"), so Korean language hardcoding in commit messages and UI elements is intentional and appropriate for their use case.


// 1. 이메일 존재 여부 테스트
@Test
void checkEmailUnique_shouldPassWhenEmailAvailable() {
when(userRepository.findByEmail("[email protected]")).thenReturn(Optional.empty());
assertDoesNotThrow(() -> userDomainService.checkEmailUnique("[email protected]"));
}

@Test
void checkEmailUnique_shouldThrowWhenEmailExistsWithAuthType() {
when(userRepository.findByEmail("[email protected]")).thenReturn(Optional.of(testUser));
when(userAuthTypeRepository.getUserAuthType(testUser)).thenReturn(List.of(AuthType.EMAIL));

AuthException exception = assertThrows(AuthException.class,
() -> userDomainService.checkEmailUnique("[email protected]"));
assertEquals(AuthExceptionCode.ALREADY_EXIST_USER, exception.getResponseCode());
}

// 2. 유저 생성 테스트
@Test
void createUser_shouldCallRepository() {
userDomainService.createUser(testUser);
verify(userRepository).createUser(testUser);
}

@Test
void createUserAuthType_shouldCallRepository() {
userDomainService.createUserAuthType(testAuthType);
verify(userAuthTypeRepository).createUserAuthType(testAuthType);
}

// 3. 유저 존재 테스트
@Test
void getUser_shouldReturnUserWhenExists() {
when(userRepository.findByEmail("[email protected]")).thenReturn(Optional.of(testUser));
User result = userDomainService.getUser("[email protected]");
assertEquals(testUser, result);
}

@Test
void getUser_shouldThrowWhenNotFound() {
when(userRepository.findByEmail("[email protected]")).thenReturn(Optional.empty());
assertThrows(AuthException.class, () -> userDomainService.getUser("[email protected]"));
}

// 4. 비번 검증 테스트
@Test
void userPasswordCheck_shouldPassWhenValid() {
when(userRepository.findByEmail("[email protected]")).thenReturn(Optional.of(testUser));
when(passwordEncoder.matches("correct", "hashedPassword")).thenReturn(true);

assertDoesNotThrow(() ->
userDomainService.userPasswordCheck("[email protected]", "correct"));
}

@Test
void userPasswordCheck_shouldThrowWhenInvalid() {
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false);

when(userRepository.findByEmail("[email protected]")).thenReturn(Optional.of(testUser));

AuthException exception = assertThrows(AuthException.class,
() -> userDomainService.userPasswordCheck("[email protected]", "wrong"));

assertEquals(AuthExceptionCode.PASSWORD_NOT_MATCH, exception.getResponseCode());
}


// 5. 비번 인코딩 테스트
@Test
void encodePassword_shouldReturnEncodedValue() {
when(passwordEncoder.encode("rawPassword")).thenReturn("encodedPassword");
assertEquals("encodedPassword", userDomainService.encodePassword("rawPassword"));
}

// 6. 인증 타입 테스트
@Test
void getUserAuthTypes_shouldReturnAuthTypes() {
List<AuthType> expectedTypes = List.of(AuthType.EMAIL, AuthType.GOOGLE);
when(userAuthTypeRepository.getUserAuthType(testUser)).thenReturn(expectedTypes);

assertEquals(expectedTypes, userDomainService.getUserAuthTypes(testUser));
}

// 7. 비번 검증 테스트
@Test
void passwordComparison_shouldThrowWhenSame() {
when(passwordEncoder.matches("newPass", "oldHashed")).thenReturn(true);
assertThrows(AuthException.class,
() -> userDomainService.passwordComparison("newPass", "oldHashed"));
}

@Test
void passwordComparison_shouldPassWhenDifferent() {
when(passwordEncoder.matches("newPass", "oldHashed")).thenReturn(false);
assertDoesNotThrow(() ->
userDomainService.passwordComparison("newPass", "oldHashed"));
}

// 8. 탈퇴 회원 테스트
@Test
void isDeletedUser_shouldThrowWhenDeleted() {
User deletedUser = new User("[email protected]","Aa12345**", "username",
"[email protected]", 100, Tier.CODER, UserRole.USER, true, true, "gitUrl.com", true);
assertThrows(AuthException.class, () -> userDomainService.isDeletedUser(deletedUser));
}

@Test
void isDeletedUser_shouldPassWhenActive() {
assertDoesNotThrow(() -> userDomainService.isDeletedUser(testUser));
}

}