Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ out/
### .env file ###
*.env

### test file ###
/src/test/resources/*
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

src/test/resources 전체 무시는 테스트 구성 누락을 유발합니다

@ActiveProfiles("test")로 구동되는 통합 테스트가 안정적으로 돌아가려면 application-test.yml 같은 프로필 자원이 반드시 레포지터리에 포함되어야 합니다. 하지만 /src/test/resources/*를 통째로 무시하면 필수 설정 파일을 커밋할 수 없어 다른 개발자나 CI 환경에서 동일한 테스트를 실행할 수 없게 됩니다. 생성물만 제외하려면 구체적인 패턴(예: *.generated.json 등)으로 한정하거나 이 규칙을 제거해 주세요.

🤖 Prompt for AI Agents
.gitignore around lines 42-43: the rule "/src/test/resources/*" is too broad and
prevents committing necessary test profile resources like application-test.yml;
remove this blanket ignore or narrow it to specific generated artifacts (for
example patterns like /src/test/resources/*.generated.json or a specific folder
for build outputs) so that essential test config files remain in the repo while
still excluding only generated/test-output files.

14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,18 @@ dependencies {

tasks.named('test') {
useJUnitPlatform()
// 테스트 JVM도 확실히 UTF-8
jvmArgs '-Dfile.encoding=UTF-8'
testLogging {
events "FAILED", "SKIPPED", "STANDARD_ERROR"
exceptionFormat "FULL"
showCauses true; showExceptions true; showStackTraces true
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

tasks.withType(Javadoc).configureEach {
options.encoding = 'UTF-8'
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
Expand Down Expand Up @@ -117,48 +114,59 @@ private void updateUserNickName(User user, String newNickName) {

private void updateUserClubs(User user, List<Long> newClubIdList) {

// null이면 빈 리스트로 간주 => 모두 탈퇴 처리
Set<Long> requested = newClubIdList == null ? Set.of()
Long userId = user.getId();

// 1) 요청 정규화 (null => 빈 집합, 중복 제거)
Set<Long> targetClubIds = newClubIdList == null ? Set.of()
: newClubIdList.stream()
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new)); // 순서 유지 필요시
.collect(Collectors.toCollection(LinkedHashSet::new));

Long userId = user.getId();
// === 전체 요청 clubId 기준 존재 여부 검증 ===
validateClubExistence(targetClubIds);

// 현재 가입된 clubId 목록
Set<Long> current = new LinkedHashSet<>(memberRepository.findClubIdsByUserId(userId));
// 2) 현재 가입된 clubId 목록 조회
Set<Long> currentClubIds = new HashSet<>(memberRepository.findClubIdsByUserId(userId));

// 계산: 추가/삭제 집합
Set<Long> toAdd = new LinkedHashSet<>(requested);
toAdd.removeAll(current);
// 3) 추가할 것과 제거할 것 계산
Set<Long> toAdd = new HashSet<>(targetClubIds);
toAdd.removeAll(currentClubIds); // 새로 추가할 것만 남김

Set<Long> toRemove = new LinkedHashSet<>(current);
toRemove.removeAll(requested);
Set<Long> toRemove = new HashSet<>(currentClubIds);
toRemove.removeAll(targetClubIds); // 요청에 없는 건 제거

// 삭제 먼저 (없으면 no-op)
// 4) 제거 실행
if (!toRemove.isEmpty()) {
memberRepository.deleteByUserIdAndClubIdIn(userId, toRemove);
}

// 추가할 Club의 존재성 검증
// 5) 추가할 Club 존재 여부 검증
if (!toAdd.isEmpty()) {
List<Club> clubs = clubRepository.findAllById(toAdd);

if (clubs.size() != toAdd.size()) {
// 어떤 ID는 존재X
throw new BusinessException(ErrorCode.CLUB_NOT_FOUND);
}

// Member 엔티티 생성
// 6) 추가 실행
List<Member> newMembers = clubs.stream()
.map(club -> Member.builder()
.user(user)
.club(club)
.build())
.toList();

// 저장 (유니크 제약 (user_id, club_id) 있어도 toAdd는 중복이 아님)
memberRepository.saveAll(newMembers);
}
}

private void validateClubExistence(Set<Long> clubIds) {
if (clubIds == null || clubIds.isEmpty()) {
return; // 요청이 없으면 패스
}

List<Club> clubs = clubRepository.findAllById(clubIds);
if (clubs.size() != clubIds.size()) {
throw new BusinessException(ErrorCode.CLUB_NOT_FOUND);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,136 +6,180 @@
import com.WhoIsRoom.WhoIs_Server.domain.member.repository.MemberRepository;
import com.WhoIsRoom.WhoIs_Server.domain.user.dto.request.MyPageUpdateRequest;
import com.WhoIsRoom.WhoIs_Server.domain.user.dto.response.MyPageResponse;
import com.WhoIsRoom.WhoIs_Server.domain.user.model.Role;
import com.WhoIsRoom.WhoIs_Server.domain.user.model.User;
import com.WhoIsRoom.WhoIs_Server.domain.user.repository.UserRepository;
import com.WhoIsRoom.WhoIs_Server.domain.user.dto.response.ClubResponse;

import com.WhoIsRoom.WhoIs_Server.global.common.exception.BusinessException;
import com.WhoIsRoom.WhoIs_Server.global.common.response.ErrorCode;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.*;

import static org.assertj.core.api.Assertions.*;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.when;

@Slf4j
@ExtendWith(MockitoExtension.class)
@SpringBootTest
@ActiveProfiles("test")
@Transactional
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // H2로 대체 금지
class UserServiceTest {

@Mock private UserRepository userRepository;
@Mock private ClubRepository clubRepository;
@Mock private MemberRepository memberRepository;
@Autowired private UserService userService;
@Autowired private UserRepository userRepository;
@Autowired private ClubRepository clubRepository;
@Autowired
private MemberRepository memberRepository;

@InjectMocks
private UserService userService;
@PersistenceContext
EntityManager em;

private User user;
private List<Club> clubs;
private Club c1, c2, c3, c4;

@BeforeEach
void setUp() {
System.out.println("\n[TEST] ========== setUp ==========");
user = User.builder()
.nickName("조익성")
.email("konkuk@gmail.com")
.password("1234")
// 깨끗한 상태로 시작(FK 제약 있으면 순서 중요)
memberRepository.deleteAllInBatch();
clubRepository.deleteAllInBatch();
userRepository.deleteAllInBatch();

// ⚠️ User, Club 엔티티의 @NotNull 필드가 있다면 실제 필드 모두 채워줘야 함
user = userRepository.save(User.builder()
.nickName("oldNick")
.role(Role.MEMBER)
.email("oldEmail")
.password("oldPassword")
.build());

c1 = clubRepository.save(Club.builder().name("C1").clubNumber("1").build());
c2 = clubRepository.save(Club.builder().name("C2").clubNumber("2").build());
c3 = clubRepository.save(Club.builder().name("C3").clubNumber("3").build());
c4 = clubRepository.save(Club.builder().name("C4").clubNumber("4").build());

em.flush();
em.clear();
}

@Test
@DisplayName("성공: 추가만 (현재 {c1,c2} → 요청 {c1,c2,c3})")
void addOnly() {
memberRepository.save(Member.builder().user(user).club(c1).build());
memberRepository.save(Member.builder().user(user).club(c2).build());
em.flush(); em.clear();

MyPageUpdateRequest req = MyPageUpdateRequest.builder()
.nickName("oldNick")
.clubList(List.of(c1.getId(), c2.getId(), c3.getId()))
.build();
user.setId(1L);

Club club1 = Club.builder().name("메이커스팜").build(); club1.setId(1L);
Club club2 = Club.builder().name("목방").build(); club2.setId(2L);
Club club3 = Club.builder().name("건대교지편집위원회").build(); club3.setId(3L);
Club club4 = Club.builder().name("국어국문학과").build(); club4.setId(4L);
MyPageResponse resp = userService.updateMyPage(user.getId(), req);

clubs = List.of(club1, club2, club3, club4);
assertThat(resp.getClubList())
.extracting(ClubResponse::getId)
.containsExactlyInAnyOrder(c1.getId(), c2.getId(), c3.getId());

System.out.println("[TEST] userId=" + user.getId() + ", nick=" + user.getNickName());
System.out.println("[TEST] clubs=" + clubs.stream()
.map(c -> c.getId() + ":" + c.getName()).toList());
System.out.println("[TEST] =============================\n");
// DB 상태도 확인
var after = memberRepository.findByUserId(user.getId());
assertThat(after).extracting(m -> m.getClub().getId())
.containsExactlyInAnyOrder(c1.getId(), c2.getId(), c3.getId());
}

@Test
@DisplayName("닉네임과 클럽 목록을 업데이트하고 응답 DTO를 반환한다")
void updateMyPage_success() {
Long userId = user.getId();
@DisplayName("성공: 삭제만 (현재 {c1,c2,c3} → 요청 {c2,c3})")
void removeOnly() {
memberRepository.save(Member.builder().user(user).club(c1).build());
memberRepository.save(Member.builder().user(user).club(c2).build());
memberRepository.save(Member.builder().user(user).club(c3).build());
em.flush(); em.clear();

MyPageUpdateRequest req = MyPageUpdateRequest.builder()
.nickName("oldNick")
.clubList(List.of(c2.getId(), c3.getId()))
.build();

MyPageResponse resp = userService.updateMyPage(user.getId(), req);

assertThat(resp.getClubList())
.extracting(ClubResponse::getId)
.containsExactlyInAnyOrder(c2.getId(), c3.getId());

var after = memberRepository.findByUserId(user.getId());
assertThat(after).extracting(m -> m.getClub().getId())
.containsExactlyInAnyOrder(c2.getId(), c3.getId());
}

@Test
@DisplayName("성공: 추가+삭제 (현재 {c1,c2,c3} → 요청 {c2,c4})")
void addAndRemove() {
memberRepository.save(Member.builder().user(user).club(c1).build());
memberRepository.save(Member.builder().user(user).club(c2).build());
memberRepository.save(Member.builder().user(user).club(c3).build());
em.flush(); em.clear();

MyPageUpdateRequest req = MyPageUpdateRequest.builder()
.nickName("oldNick")
.clubList(List.of(c2.getId(), c4.getId()))
.build();

MyPageResponse resp = userService.updateMyPage(user.getId(), req);

assertThat(resp.getClubList())
.extracting(ClubResponse::getId)
.containsExactlyInAnyOrder(c2.getId(), c4.getId());

var after = memberRepository.findByUserId(user.getId());
assertThat(after).extracting(m -> m.getClub().getId())
.containsExactlyInAnyOrder(c2.getId(), c4.getId());
}

@Test
@DisplayName("성공: 전체 탈퇴 (요청 null)")
void leaveAll() {
memberRepository.save(Member.builder().user(user).club(c1).build());
memberRepository.save(Member.builder().user(user).club(c2).build());
em.flush(); em.clear();

MyPageUpdateRequest req = MyPageUpdateRequest.builder()
.nickName("oldNick")
.clubList(null) // 전체 탈퇴로 간주
.build();

MyPageResponse resp = userService.updateMyPage(user.getId(), req);

assertThat(resp.getClubList()).isEmpty();
assertThat(memberRepository.findByUserId(user.getId())).isEmpty();
}

@Test
@DisplayName("실패: 존재하지 않는 Club ID 포함 → CLUB_NOT_FOUND")
void clubNotFound() {
Long notExistId = 9999L;

memberRepository.save(Member.builder().user(user).club(c1).build());
em.flush(); em.clear();

MyPageUpdateRequest request = MyPageUpdateRequest.builder()
.nickName("조익성")
.clubList(List.of(1L, 2L, 3L, 4L))
MyPageUpdateRequest req = MyPageUpdateRequest.builder()
.nickName("oldNick")
.clubList(List.of(c1.getId(), notExistId))
.build();

// --- 스텁 + 로그 ---
when(userRepository.findById(userId))
.thenAnswer(inv -> {
System.out.println("[TEST] userRepository.findById(" + userId + ")");
return Optional.of(user);
});

when(memberRepository.findClubIdsByUserId(userId))
.thenAnswer(inv -> {
System.out.println("[TEST] memberRepository.findClubIdsByUserId(" + userId + ") -> [2]");
return List.of(2L);
});

when(clubRepository.findAllById(ArgumentMatchers.<Long>anyIterable()))
.thenAnswer(invocation -> {
Iterable<Long> ids = invocation.getArgument(0);
List<Long> idList = new ArrayList<>();
ids.forEach(idList::add);
System.out.println("[TEST] clubRepository.findAllById called with ids=" + idList);
var result = clubs.stream()
.filter(c -> idList.contains(c.getId()))
.collect(Collectors.toList());
System.out.println("[TEST] clubRepository.findAllById returns ids=" +
result.stream().map(Club::getId).toList());
return result;
});

when(memberRepository.saveAll(anyCollection()))
.thenAnswer(invocation -> {
@SuppressWarnings("unchecked")
var c = (java.util.Collection<Member>) invocation.getArgument(0);
System.out.println("[TEST] memberRepository.saveAll called size=" + c.size() +
", clubIds=" + c.stream().map(m -> m.getClub().getId()).toList());
return new ArrayList<>(c);
});

when(memberRepository.findByUserId(userId))
.thenAnswer(inv -> {
System.out.println("[TEST] memberRepository.findByUserId(" + userId + ")");
var list = clubs.stream()
.map(c -> Member.builder().user(user).club(c).build())
.collect(Collectors.toList());
System.out.println("[TEST] memberRepository.findByUserId returns clubIds=" +
list.stream().map(m -> m.getClub().getId()).toList());
return list;
});

// --- 실행 ---
System.out.println("\n[TEST] ===== call userService.updateMyPage =====");
MyPageResponse response = userService.updateMyPage(userId, request);
System.out.println("[TEST] ===== returned MyPageResponse =====");
System.out.println("[TEST] resp.nick=" + response.getNickName());
System.out.println("[TEST] resp.clubs=" + response.getClubList().stream()
.map(c -> c.getId() + ":" + c.getName()).toList());
System.out.println("[TEST] ===================================\n");

// --- 검증 ---
assertThat(response.getNickName()).isEqualTo("조익성");
assertThat(response.getClubList()).hasSize(4);
assertThat(response.getClubList())
.extracting("name")
.containsExactlyInAnyOrder("메이커스팜", "목방", "건대교지편집위원회", "국어국문학과");
assertThatThrownBy(() -> userService.updateMyPage(user.getId(), req))
.isInstanceOf(BusinessException.class)
.extracting("errorCode")
.isEqualTo(ErrorCode.CLUB_NOT_FOUND);
}
}