From 7dd2a64519d886153bc6fc1df538ecca214b87b6 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Tue, 4 Feb 2025 01:49:17 +0900 Subject: [PATCH 1/4] =?UTF-8?q?CLAP-253=20refactor:Usecase=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/RegisterMemberCsvController.java | 12 +++---- .../admin/RegisterMemberCSVUsecase.java | 7 ++++ .../inbound/admin/RegisterMemberUsecase.java | 2 -- .../service/admin/CsvParseService.java | 4 +-- .../admin/RegisterMemberCSVService.java | 32 +++++++++++++++++++ .../service/admin/RegisterMemberService.java | 13 +------- 6 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 src/main/java/clap/server/application/port/inbound/admin/RegisterMemberCSVUsecase.java create mode 100644 src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java index dc06b605..1f4390e1 100644 --- a/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java +++ b/src/main/java/clap/server/adapter/inbound/web/admin/RegisterMemberCsvController.java @@ -1,7 +1,7 @@ package clap.server.adapter.inbound.web.admin; import clap.server.adapter.inbound.security.service.SecurityUserDetails; -import clap.server.application.port.inbound.admin.RegisterMemberUsecase; +import clap.server.application.port.inbound.admin.RegisterMemberCSVUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -17,10 +17,10 @@ @WebAdapter @RequestMapping("/api/managements") public class RegisterMemberCsvController { - private final RegisterMemberUsecase registerMemberUsecase; + private final RegisterMemberCSVUsecase registerMemberCSVUsecase; - public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase) { - this.registerMemberUsecase = registerMemberUsecase; + public RegisterMemberCsvController(RegisterMemberCSVUsecase registerMemberCSVUsecase) { + this.registerMemberCSVUsecase = registerMemberCSVUsecase; } @Operation(summary = "CSV 파일로 회원 등록 API") @@ -29,7 +29,7 @@ public RegisterMemberCsvController(RegisterMemberUsecase registerMemberUsecase) public ResponseEntity registerMembersFromCsv( @AuthenticationPrincipal SecurityUserDetails userInfo, @RequestParam("file") MultipartFile file) { - int addedCount = registerMemberUsecase.registerMembersFromCsv(userInfo.getUserId(), file); + int addedCount = registerMemberCSVUsecase.registerMembersFromCsv(userInfo.getUserId(), file); return ResponseEntity.ok(addedCount + "명의 회원이 등록되었습니다."); } -} +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberCSVUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberCSVUsecase.java new file mode 100644 index 00000000..c3e33ccb --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberCSVUsecase.java @@ -0,0 +1,7 @@ +package clap.server.application.port.inbound.admin; + +import org.springframework.web.multipart.MultipartFile; + +public interface RegisterMemberCSVUsecase { + int registerMembersFromCsv(Long adminId, MultipartFile file); +} diff --git a/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java index 08972e2e..a143bd5a 100644 --- a/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/admin/RegisterMemberUsecase.java @@ -5,6 +5,4 @@ public interface RegisterMemberUsecase { void registerMember(Long adminId, RegisterMemberRequest request); - - int registerMembersFromCsv(Long adminId, MultipartFile file); } diff --git a/src/main/java/clap/server/application/service/admin/CsvParseService.java b/src/main/java/clap/server/application/service/admin/CsvParseService.java index 6fd8deb7..d9f9742a 100644 --- a/src/main/java/clap/server/application/service/admin/CsvParseService.java +++ b/src/main/java/clap/server/application/service/admin/CsvParseService.java @@ -1,6 +1,7 @@ package clap.server.application.service.admin; import clap.server.application.port.outbound.member.LoadDepartmentPort; +import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Department; import clap.server.domain.model.member.Member; import clap.server.domain.model.member.MemberInfo; @@ -10,7 +11,6 @@ import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; @@ -24,7 +24,7 @@ @Slf4j -@Service +@ApplicationService @RequiredArgsConstructor public class CsvParseService { diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java new file mode 100644 index 00000000..1b9068e6 --- /dev/null +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberCSVService.java @@ -0,0 +1,32 @@ +package clap.server.application.service.admin; + +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.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@ApplicationService +@RequiredArgsConstructor +public class RegisterMemberCSVService implements RegisterMemberCSVUsecase { + private final MemberService memberService; + private final CommandMemberPort commandMemberPort; + private final CsvParseService csvParser; + + @Override + @Transactional + public int registerMembersFromCsv(Long adminId, MultipartFile file) { + List members = csvParser.parse(file); + Member admin = memberService.findActiveMember(adminId); + members.forEach(member -> { + member.register(admin); + commandMemberPort.save(member); + }); + return members.size(); + } +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java index 115e9410..4019b897 100644 --- a/src/main/java/clap/server/application/service/admin/RegisterMemberService.java +++ b/src/main/java/clap/server/application/service/admin/RegisterMemberService.java @@ -14,7 +14,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; + import java.util.List; @@ -41,15 +41,4 @@ public void registerMember(Long adminId, RegisterMemberRequest request) { commandMemberPort.save(member); } - @Override - @Transactional - public int registerMembersFromCsv(Long adminId, MultipartFile file) { - List members = csvParser.parse(file); - Member admin = memberService.findActiveMember(adminId); - members.forEach(member -> { - member.register(admin); - commandMemberPort.save(member); - }); - return members.size(); - } } From 7030671cc5c11a7e2ad49691c772022cb04d31d5 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Tue, 4 Feb 2025 02:03:02 +0900 Subject: [PATCH 2/4] =?UTF-8?q?CLAP-253=20fix:CSV=20parsing=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95(=ED=97=A4=EB=8D=94=20row=20?= =?UTF-8?q?=EA=B1=B4=EB=84=88=EB=9B=B0=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/application/service/admin/CsvParseService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/clap/server/application/service/admin/CsvParseService.java b/src/main/java/clap/server/application/service/admin/CsvParseService.java index d9f9742a..b6fd6646 100644 --- a/src/main/java/clap/server/application/service/admin/CsvParseService.java +++ b/src/main/java/clap/server/application/service/admin/CsvParseService.java @@ -33,6 +33,11 @@ public class CsvParseService { public List parse(MultipartFile file) { List members = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { + // 첫 번째 줄은 헤더로 간주하고 다음 줄부터 파싱 + String headerLine = reader.readLine(); + if (headerLine == null) { + throw ApplicationException.from(MemberErrorCode.INVALID_CSV_FORMAT); + } String line; while ((line = reader.readLine()) != null) { String[] fields = line.split(","); From 2b54a64f78c9cfde73a3fa8f38de84eb595a3671 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Tue, 4 Feb 2025 02:14:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?CLAP-253=20test:=EB=8B=A8=EC=9C=84=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/admin/CsvParseServiceTest.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java diff --git a/src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java b/src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java new file mode 100644 index 00000000..8f6580a9 --- /dev/null +++ b/src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java @@ -0,0 +1,109 @@ +package clap.server.application.service.admin; + +import clap.server.application.service.admin.CsvParseService; +import clap.server.application.port.outbound.member.LoadDepartmentPort; +import clap.server.domain.model.member.Department; +import clap.server.domain.model.member.Member; +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.Mockito; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class CsvParseServiceTest { + + private LoadDepartmentPort loadDepartmentPort; + private CsvParseService csvParseService; + + @BeforeEach + public void setup() { + loadDepartmentPort = Mockito.mock(LoadDepartmentPort.class); + csvParseService = new CsvParseService(loadDepartmentPort); + } + + /** + * 정상 케이스: + * CSV 파일에 헤더와 하나의 데이터 행이 있는 경우 올바르게 파싱되는지 검증. + * name, nickname, departmentId, departmentRole, email, role, isReviewer -> 실제 CSV 파일의 columm 순서 + */ + @Test + public void testParseValidCsv() { + // CSV 파일 (첫 번째 줄은 헤더) + String csvContent = "name,nickname,departmentId,departmentRole,email,role,isReviewer\n" + + "양시훈,leo,1,dev,sihun123@gmail.com,ROLE_USER,false\n"; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", csvContent.getBytes()); + + Department dummyDepartment = Department.builder().departmentId(1L).name("dev").build(); + when(loadDepartmentPort.findById(1L)).thenReturn(Optional.of(dummyDepartment)); + + // CSV 파싱 + java.util.List members = csvParseService.parse(file); + + // 검증 + assertNotNull(members); + assertEquals(1, members.size()); + Member member = members.get(0); + assertEquals("양시훈", member.getMemberInfo().getName()); + assertEquals("leo", member.getMemberInfo().getNickname()); + assertEquals("sihun123@gmail.com", member.getMemberInfo().getEmail()); + assertEquals("dev", member.getMemberInfo().getDepartment().getName()); + assertEquals("ROLE_USER", member.getMemberInfo().getRole().name()); + assertFalse(member.getMemberInfo().isReviewer()); + } + + /** + * 잘못된 컬럼 개수 케이스: + * CSV 파일에 데이터 행의 컬럼 수가 7개가 아니면 예외가 발생하는지 검증 + */ + @Test + public void testParseCsvWithInvalidColumnCount() { + // 6개 컬럼만 있는 데이터 + String csvContent = "name,nickname,departmentId,departmentRole,email,role,isReviewer\n" + + "김민수,minsoo,2,Infra,minsoo@naver.com,ROLE_ADMIN\n"; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", csvContent.getBytes()); + + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + csvParseService.parse(file); + }); + assertEquals(MemberErrorCode.INVALID_CSV_FORMAT.getCustomCode(), exception.getCode().getCustomCode()); + } + + /** + * 빈 파일 케이스: + * CSV 파일이 완전히 비어 있으면 헤더를 읽지 못해 예외가 발생하는지 검증 + */ + @Test + public void testParseEmptyFileThrowsException() { + String csvContent = ""; + MultipartFile file = new MockMultipartFile("file", "empty.csv", "text/csv", csvContent.getBytes()); + + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + csvParseService.parse(file); + }); + assertEquals(MemberErrorCode.INVALID_CSV_FORMAT.getCustomCode(), exception.getCode().getCustomCode()); + } + + /** + * IOException 발생 케이스: + * MultipartFile의 getInputStream() 호출 시 IOException이 발생하면 예외가 발생하는지 검증 + */ + @Test + public void testParseIOExceptionThrowsException() throws IOException { + // getInputStream() 호출 시 IOException을 던지는 mock MultipartFile 생성 + MultipartFile file = Mockito.mock(MultipartFile.class); + when(file.getInputStream()).thenThrow(new IOException("IO error")); + + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + csvParseService.parse(file); + }); + assertEquals(MemberErrorCode.CSV_PARSING_ERROR.getCustomCode(), exception.getCode().getCustomCode()); + } +} From 486d6ebe1db060bab33197f14ebdafb1109ed913 Mon Sep 17 00:00:00 2001 From: Sihun23 Date: Tue, 4 Feb 2025 14:30:09 +0900 Subject: [PATCH 4/4] =?UTF-8?q?CLAP-253=20test:Usecase=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/admin/CsvParseServiceTest.java | 109 --------------- .../admin/RegisterMemberCSVServiceTest.java | 125 ++++++++++++++++++ 2 files changed, 125 insertions(+), 109 deletions(-) delete mode 100644 src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java create mode 100644 src/test/java/clap/server/application/service/admin/RegisterMemberCSVServiceTest.java diff --git a/src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java b/src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java deleted file mode 100644 index 8f6580a9..00000000 --- a/src/test/java/clap/server/application/service/admin/CsvParseServiceTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package clap.server.application.service.admin; - -import clap.server.application.service.admin.CsvParseService; -import clap.server.application.port.outbound.member.LoadDepartmentPort; -import clap.server.domain.model.member.Department; -import clap.server.domain.model.member.Member; -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.Mockito; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; - -public class CsvParseServiceTest { - - private LoadDepartmentPort loadDepartmentPort; - private CsvParseService csvParseService; - - @BeforeEach - public void setup() { - loadDepartmentPort = Mockito.mock(LoadDepartmentPort.class); - csvParseService = new CsvParseService(loadDepartmentPort); - } - - /** - * 정상 케이스: - * CSV 파일에 헤더와 하나의 데이터 행이 있는 경우 올바르게 파싱되는지 검증. - * name, nickname, departmentId, departmentRole, email, role, isReviewer -> 실제 CSV 파일의 columm 순서 - */ - @Test - public void testParseValidCsv() { - // CSV 파일 (첫 번째 줄은 헤더) - String csvContent = "name,nickname,departmentId,departmentRole,email,role,isReviewer\n" + - "양시훈,leo,1,dev,sihun123@gmail.com,ROLE_USER,false\n"; - MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", csvContent.getBytes()); - - Department dummyDepartment = Department.builder().departmentId(1L).name("dev").build(); - when(loadDepartmentPort.findById(1L)).thenReturn(Optional.of(dummyDepartment)); - - // CSV 파싱 - java.util.List members = csvParseService.parse(file); - - // 검증 - assertNotNull(members); - assertEquals(1, members.size()); - Member member = members.get(0); - assertEquals("양시훈", member.getMemberInfo().getName()); - assertEquals("leo", member.getMemberInfo().getNickname()); - assertEquals("sihun123@gmail.com", member.getMemberInfo().getEmail()); - assertEquals("dev", member.getMemberInfo().getDepartment().getName()); - assertEquals("ROLE_USER", member.getMemberInfo().getRole().name()); - assertFalse(member.getMemberInfo().isReviewer()); - } - - /** - * 잘못된 컬럼 개수 케이스: - * CSV 파일에 데이터 행의 컬럼 수가 7개가 아니면 예외가 발생하는지 검증 - */ - @Test - public void testParseCsvWithInvalidColumnCount() { - // 6개 컬럼만 있는 데이터 - String csvContent = "name,nickname,departmentId,departmentRole,email,role,isReviewer\n" + - "김민수,minsoo,2,Infra,minsoo@naver.com,ROLE_ADMIN\n"; - MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", csvContent.getBytes()); - - ApplicationException exception = assertThrows(ApplicationException.class, () -> { - csvParseService.parse(file); - }); - assertEquals(MemberErrorCode.INVALID_CSV_FORMAT.getCustomCode(), exception.getCode().getCustomCode()); - } - - /** - * 빈 파일 케이스: - * CSV 파일이 완전히 비어 있으면 헤더를 읽지 못해 예외가 발생하는지 검증 - */ - @Test - public void testParseEmptyFileThrowsException() { - String csvContent = ""; - MultipartFile file = new MockMultipartFile("file", "empty.csv", "text/csv", csvContent.getBytes()); - - ApplicationException exception = assertThrows(ApplicationException.class, () -> { - csvParseService.parse(file); - }); - assertEquals(MemberErrorCode.INVALID_CSV_FORMAT.getCustomCode(), exception.getCode().getCustomCode()); - } - - /** - * IOException 발생 케이스: - * MultipartFile의 getInputStream() 호출 시 IOException이 발생하면 예외가 발생하는지 검증 - */ - @Test - public void testParseIOExceptionThrowsException() throws IOException { - // getInputStream() 호출 시 IOException을 던지는 mock MultipartFile 생성 - MultipartFile file = Mockito.mock(MultipartFile.class); - when(file.getInputStream()).thenThrow(new IOException("IO error")); - - ApplicationException exception = assertThrows(ApplicationException.class, () -> { - csvParseService.parse(file); - }); - assertEquals(MemberErrorCode.CSV_PARSING_ERROR.getCustomCode(), exception.getCode().getCustomCode()); - } -} diff --git a/src/test/java/clap/server/application/service/admin/RegisterMemberCSVServiceTest.java b/src/test/java/clap/server/application/service/admin/RegisterMemberCSVServiceTest.java new file mode 100644 index 00000000..0d8bdf43 --- /dev/null +++ b/src/test/java/clap/server/application/service/admin/RegisterMemberCSVServiceTest.java @@ -0,0 +1,125 @@ +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.domain.model.member.Member; +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.Mockito; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class RegisterMemberCSVServiceTest { + + private RegisterMemberCSVService registerMemberCSVService; + private MemberService memberService; + private CommandMemberPort commandMemberPort; + private CsvParseService csvParseService; + + + @BeforeEach + void setup() { + memberService = Mockito.mock(MemberService.class); + commandMemberPort = Mockito.mock(CommandMemberPort.class); + csvParseService = Mockito.mock(CsvParseService.class); + registerMemberCSVService = new RegisterMemberCSVService(memberService, commandMemberPort, csvParseService); + } + + /** + * 정상적인 회원 등록 테스트 + * - 주어진 CSV 파일을 정상적으로 파싱하여 회원이 등록되는지 검증 + */ + @Test + void testRegisterMembersFromCsvSuccess() { + Long adminId = 1L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + Member admin = Mockito.mock(Member.class); + List parsedMembers = List.of(Mockito.mock(Member.class), Mockito.mock(Member.class)); + + when(memberService.findActiveMember(adminId)).thenReturn(admin); + when(csvParseService.parse(file)).thenReturn(parsedMembers); + + int addedCount = registerMemberCSVService.registerMembersFromCsv(adminId, file); + + assertEquals(2, addedCount); + verify(commandMemberPort, times(2)).save(any(Member.class)); + verify(parsedMembers.get(0), times(1)).register(admin); + verify(parsedMembers.get(1), times(1)).register(admin); + } + + /** + * ❌ 관리자 찾기 실패 (MEMBER_NOT_FOUND) + */ + @Test + void testRegisterMembersFromCsvThrowsWhenAdminNotFound() { + Long adminId = 99L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + when(memberService.findActiveMember(adminId)).thenThrow(new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + registerMemberCSVService.registerMembersFromCsv(adminId, file); + }); + + // 검증: 발생한 예외가 `MEMBER_NOT_FOUND`인지 확인 + assertEquals(MemberErrorCode.MEMBER_NOT_FOUND.getCustomCode(), exception.getCode().getCustomCode()); + verifyNoInteractions(commandMemberPort); // 회원 저장 로직이 실행안됨 + } + + /** + * ❌ CSV 파싱 실패 (CSV_PARSING_ERROR) + */ + @Test + void testRegisterMembersFromCsvThrowsWhenCsvParsingFails() { + Long adminId = 1L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + // ✅ Mock 객체 설정: CSV 파싱 과정에서 예외 발생하도록 설정 + when(csvParseService.parse(file)).thenThrow(new ApplicationException(MemberErrorCode.CSV_PARSING_ERROR)); + + // 🔹 유스케이스 실행 및 예외 검증 + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + registerMemberCSVService.registerMembersFromCsv(adminId, file); + }); + + // ✅ 검증: 발생한 예외가 `CSV_PARSING_ERROR`인지 확인 + assertEquals(MemberErrorCode.CSV_PARSING_ERROR.getCustomCode(), exception.getCode().getCustomCode()); + verifyNoInteractions(commandMemberPort); // ❗ 회원 저장 로직이 실행되지 않아야 함 + } + + /** + * ❌ 회원 등록 실패 (MEMBER_REGISTRATION_FAILED) + * + */ + @Test + void testRegisterMembersFromCsvThrowsWhenSavingMemberFails() { + Long adminId = 1L; + MultipartFile file = new MockMultipartFile("file", "members.csv", "text/csv", "dummy-content".getBytes()); + + Member admin = Mockito.mock(Member.class); + Member failingMember = Mockito.mock(Member.class); + List parsedMembers = List.of(failingMember, Mockito.mock(Member.class)); + + // 특정 회원 등록 중 예외 발생 + when(memberService.findActiveMember(adminId)).thenReturn(admin); + when(csvParseService.parse(file)).thenReturn(parsedMembers); + doThrow(new ApplicationException(MemberErrorCode.MEMBER_REGISTRATION_FAILED)) + .when(commandMemberPort).save(failingMember); + + // Usecase 실행 + ApplicationException exception = assertThrows(ApplicationException.class, () -> { + registerMemberCSVService.registerMembersFromCsv(adminId, file); + }); + + assertEquals(MemberErrorCode.MEMBER_REGISTRATION_FAILED.getCustomCode(), exception.getCode().getCustomCode()); + verify(commandMemberPort, times(1)).save(failingMember); // ❗ 실패한 회원만 저장 시도해야 함 + } +}