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 @@ -113,5 +113,11 @@ public Optional<Member> findByNicknameAndEmail(String nickname, String email) {
public Optional<Member> findByNameAndEmail(String name, String email) {
return memberRepository.findByNameAndEmail(name, email).map(memberPersistenceMapper::toDomain);
}
}

@Override
public Optional<Member> findByEmail(String email) {
return memberRepository.findByEmail(email)
.map(memberPersistenceMapper::toDomain);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ public interface MemberRepository extends JpaRepository<MemberEntity, Long>, Me
Optional<MemberEntity> findByNicknameAndEmail(String nickname, String email);

Optional<MemberEntity> findByNameAndEmail(String name, String email);

Optional<MemberEntity> findByEmail(String email);
}

Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ public interface LoadMemberPort {
Optional<Member> findByNicknameAndEmail(String nickname, String email);

Optional<Member> findByNameAndEmail(String name, String email);

Optional<Member> findByEmail(String email);

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import clap.server.application.port.inbound.admin.RegisterMemberCSVUsecase;
import clap.server.application.port.inbound.domain.MemberService;
import clap.server.application.port.outbound.member.CommandMemberPort;
import clap.server.application.port.outbound.member.LoadMemberPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.member.Member;
import clap.server.exception.ApplicationException;
import clap.server.exception.code.MemberErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -17,13 +20,24 @@ public class RegisterMemberCSVService implements RegisterMemberCSVUsecase {
private final MemberService memberService;
private final CommandMemberPort commandMemberPort;
private final CsvParseService csvParser;
private final LoadMemberPort loadMemberPort;


@Override
@Transactional
public int registerMembersFromCsv(Long adminId, MultipartFile file) {
List<Member> members = csvParser.parseDataAndMapToMember(file);
Member admin = memberService.findActiveMember(adminId);

members.forEach(member -> {
String nickname = member.getMemberInfo().getNickname();
String email = member.getMemberInfo().getEmail();
if (loadMemberPort.findByNickname(nickname).isPresent() ||
loadMemberPort.findByEmail(email).isPresent()) {
throw new ApplicationException(MemberErrorCode.DUPLICATE_NICKNAME_OR_EMAIL);
}
});

List<Member> newMembers = members.stream()
.map(memberData -> Member.createMember(admin, memberData.getMemberInfo()))
.toList();
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/clap/server/exception/code/MemberErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ public enum MemberErrorCode implements BaseErrorCode {
CSV_PARSING_ERROR(HttpStatus.BAD_REQUEST, "MEMBER_008", "CSV 데이터 파싱 중 오류가 발생했습니다."),
MEMBER_REGISTRATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER_009", "담당자만 리뷰 권한이 있습니다."),
NAME_CANNOT_BE_EMPTY(HttpStatus.BAD_REQUEST, "MEMBER_010", "이름은 공백일 수 없습니다."),
DUPLICATE_NICKNAME(HttpStatus.BAD_REQUEST,"MEMBER_011", "중복된 닉네임입니다")
;
DUPLICATE_NICKNAME(HttpStatus.BAD_REQUEST,"MEMBER_011", "중복된 닉네임입니다"),
DUPLICATE_NICKNAME_OR_EMAIL(HttpStatus.BAD_REQUEST, "MEMBER_012", "중복된 닉네임이나 email이 존재합니다")
;

private final HttpStatus httpStatus;
private final String customCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package clap.server.application.service.admin;

import clap.server.application.port.inbound.domain.MemberService;
import clap.server.application.port.outbound.member.CommandMemberPort;
import clap.server.application.port.outbound.member.LoadMemberPort;
import clap.server.domain.model.member.Department;
import clap.server.domain.model.member.Member;
import clap.server.domain.model.member.MemberInfo;
import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole;
import clap.server.exception.ApplicationException;
import clap.server.exception.code.MemberErrorCode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

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

class RegisterMemberCSVServiceTest {

@Mock
private MemberService memberService;

@Mock
private CommandMemberPort commandMemberPort;

@Mock
private CsvParseService csvParser;

@Mock
private LoadMemberPort loadMemberPort;

@InjectMocks
private RegisterMemberCSVService registerMemberCSVService;

// 더미 Department: department는 not null이어야 하므로, 예시로 departmentId가 채워진 객체를 생성합니다.
private Department dummyDepartment;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
// 예시로 Department가 빌더를 제공한다고 가정
dummyDepartment = Department.builder().departmentId(100L).build();
}

@Test
void testRegisterMembersFromCsv_success() throws Exception {
// given
Long adminId = 1L;
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv",
"header\nrow1\nrow2".getBytes());

// CSV 파싱 결과로 반환할 회원 객체들 (각 MemberInfo에 dummyDepartment 적용)
Member csvMember1 = mock(Member.class);
Member csvMember2 = mock(Member.class);

MemberInfo dummyMemberInfo1 = MemberInfo.builder()
.name("John Doe")
.email("john@example.com")
.nickname("johnny")
.isReviewer(false)
.department(dummyDepartment)
.role(MemberRole.ROLE_USER)
.departmentRole("Dept Role")
.build();

MemberInfo dummyMemberInfo2 = MemberInfo.builder()
.name("Jane Doe")
.email("jane@example.com")
.nickname("janie")
.isReviewer(false)
.department(dummyDepartment)
.role(MemberRole.ROLE_USER)
.departmentRole("Dept Role")
.build();

when(csvMember1.getMemberInfo()).thenReturn(dummyMemberInfo1);
when(csvMember2.getMemberInfo()).thenReturn(dummyMemberInfo2);
List<Member> csvMembers = Arrays.asList(csvMember1, csvMember2);
when(csvParser.parseDataAndMapToMember(file)).thenReturn(csvMembers);

when(loadMemberPort.findByNickname(anyString())).thenReturn(Optional.empty());
when(loadMemberPort.findByEmail(anyString())).thenReturn(Optional.empty());

Member adminMember = mock(Member.class);
when(memberService.findActiveMember(adminId)).thenReturn(adminMember);

Member newMember1 = mock(Member.class);
Member newMember2 = mock(Member.class);

try (MockedStatic<Member> mockedStatic = Mockito.mockStatic(Member.class)) {
mockedStatic.when(() -> Member.createMember(eq(adminMember), eq(dummyMemberInfo1)))
.thenReturn(newMember1);
mockedStatic.when(() -> Member.createMember(eq(adminMember), eq(dummyMemberInfo2)))
.thenReturn(newMember2);

// when
int result = registerMemberCSVService.registerMembersFromCsv(adminId, file);

// then
ArgumentCaptor<List<Member>> captor = ArgumentCaptor.forClass(List.class);
verify(commandMemberPort).saveAll(captor.capture());
List<Member> savedMembers = captor.getValue();

assertEquals(2, savedMembers.size(), "CSV 파싱된 회원 수 만큼 새로운 회원이 생성");
assertEquals(2, result, "등록된 회원 수는 CSV 파일의 회원 수와 동일");

mockedStatic.verify(() -> Member.createMember(adminMember, dummyMemberInfo1), times(1));
mockedStatic.verify(() -> Member.createMember(adminMember, dummyMemberInfo2), times(1));
}
}

@Test
void testRegisterMembersFromCsv_duplicateThrowsException() throws Exception {
// given
Long adminId = 1L;
MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv",
"header\nrow1".getBytes());

Member csvMember1 = mock(Member.class);
MemberInfo dummyMemberInfo1 = MemberInfo.builder()
.name("John Doe")
.email("john@example.com")
.nickname("johnny")
.isReviewer(false)
.department(dummyDepartment)
.role(MemberRole.ROLE_USER)
.departmentRole("Dept Role")
.build();
when(csvMember1.getMemberInfo()).thenReturn(dummyMemberInfo1);
List<Member> csvMembers = Arrays.asList(csvMember1);
when(csvParser.parseDataAndMapToMember(file)).thenReturn(csvMembers);

Member adminMember = mock(Member.class);
when(memberService.findActiveMember(adminId)).thenReturn(adminMember);

// 중복 체크: 닉네임 또는 email 중 하나라도 중복이 있으면 에러 발생
when(loadMemberPort.findByNickname(dummyMemberInfo1.getNickname()))
.thenReturn(Optional.of(mock(Member.class)));

ApplicationException exception = assertThrows(ApplicationException.class, () ->
registerMemberCSVService.registerMembersFromCsv(adminId, file)
);
assertEquals(MemberErrorCode.DUPLICATE_NICKNAME_OR_EMAIL.getMessage(), exception.getMessage(),
"중복된 닉네임이나 email이 존재하면 MEMBER_012 예외가 발생해야 합니다.");
}
}