diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java index 2da65f2b..51dcb08b 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/member/DepartmentEntity.java @@ -24,6 +24,9 @@ public class DepartmentEntity extends BaseTimeEntity { @JoinColumn(name = "admin_id") private MemberEntity admin; + @Column(nullable = false) + private String code; + @Column(nullable = false) private String name; diff --git a/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java b/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java index ac62cfe1..87e6947b 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/mapper/DepartmentPersistenceMapper.java @@ -10,10 +10,8 @@ public interface DepartmentPersistenceMapper extends PersistenceMapper { @Mapping(source = "admin.memberId", target = "adminId") - @Mapping(source = "name", target = "code") Department toDomain(DepartmentEntity entity); - @Mapping(source = "code", target = "name") @Mapping(source = "adminId", target = "admin.memberId") DepartmentEntity toEntity(Department domain); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/mapper/TaskResponseMapper.java b/src/main/java/clap/server/application/mapper/TaskResponseMapper.java index f68a80ef..5c672e59 100644 --- a/src/main/java/clap/server/application/mapper/TaskResponseMapper.java +++ b/src/main/java/clap/server/application/mapper/TaskResponseMapper.java @@ -97,7 +97,7 @@ public static ApprovalTaskResponse toApprovalTaskResponse(Task approvedTask) { approvedTask.getProcessor().getNickname(), approvedTask.getReviewer().getNickname(), approvedTask.getDueDate(), - approvedTask.getLabel().getLabelName(), + approvedTask.getLabel() != null ? approvedTask.getLabel().getLabelName() : "", approvedTask.getTaskStatus() ); } diff --git a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java index 0a033b47..4c50110c 100644 --- a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java +++ b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java @@ -46,7 +46,10 @@ public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, Task task = taskService.findById(taskId); Member processor = memberService.findById(approvalTaskRequest.processorId()); Category category = categoryService.findById(approvalTaskRequest.categoryId()); - Label label = labelService.findById(approvalTaskRequest.labelId()); + Label label = null; + if (approvalTaskRequest.labelId() != null) { + label = labelService.findById(approvalTaskRequest.labelId()); + } requestedTaskUpdatePolicy.validateTaskRequested(task); task.approveTask(reviewer, processor, approvalTaskRequest.dueDate(), category, label); diff --git a/src/main/java/clap/server/domain/model/common/BaseTime.java b/src/main/java/clap/server/domain/model/common/BaseTime.java index 1815c0cb..ccf54ee4 100644 --- a/src/main/java/clap/server/domain/model/common/BaseTime.java +++ b/src/main/java/clap/server/domain/model/common/BaseTime.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; @Getter -@SuperBuilder(toBuilder = true) +@SuperBuilder @MappedSuperclass @NoArgsConstructor(access = AccessLevel.PROTECTED) public class BaseTime { diff --git a/src/main/java/clap/server/domain/model/member/Member.java b/src/main/java/clap/server/domain/model/member/Member.java index bdffb88b..3c6d7163 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -10,7 +10,7 @@ import lombok.experimental.SuperBuilder; @Getter -@SuperBuilder(toBuilder = true) +@SuperBuilder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Member extends BaseTime { diff --git a/src/test/java/clap/server/TestDataFactory.java b/src/test/java/clap/server/TestDataFactory.java new file mode 100644 index 00000000..e433b310 --- /dev/null +++ b/src/test/java/clap/server/TestDataFactory.java @@ -0,0 +1,107 @@ +package clap.server; + +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; +import clap.server.adapter.outbound.persistense.entity.task.constant.LabelColor; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.domain.model.member.Member; +import clap.server.domain.model.member.MemberInfo; +import clap.server.domain.model.task.Category; +import clap.server.domain.model.task.Label; +import clap.server.domain.model.task.Task; +import org.springframework.data.domain.PageImpl; + +import java.time.LocalDateTime; +import java.util.List; + +public class TestDataFactory { + + public static Member createAdmin() { + return new Member(1L, createAdminInfo(), null, + true, true, true, + null, MemberStatus.ACTIVE, "1111"); + } + public static Member createManagerWithReviewer() { + return new Member(2L, createManagerWithReviewerInfo(), createAdmin(), + true, true, true, + null, MemberStatus.ACTIVE, "1111"); + } + public static Member createManager() { + return new Member(3L, createManagerInfo(), createAdmin(), + true, true, true, + null, MemberStatus.ACTIVE, "1111"); + } + public static Member createUser() { + return new Member(4L, createUserInfo(), createAdmin(), + true, true, true, + null, MemberStatus.ACTIVE, "1111"); + } + + public static MemberInfo createAdminInfo(){ + return new MemberInfo("홍길동(관리자)", "atom8426@naver.com", "atom.admin", false, null, MemberRole.ROLE_ADMIN, "인프라"); + } + public static MemberInfo createManagerWithReviewerInfo(){ + return new MemberInfo("홍길동(리뷰어)", "atom8426@naver.com", "atom.manager", true, null, MemberRole.ROLE_MANAGER, "인프라"); + } + public static MemberInfo createManagerInfo(){ + return new MemberInfo("홍길동(매니저)", "atom8426@naver.com", "atom.manager", false, null, MemberRole.ROLE_MANAGER, "인프라"); + } + public static MemberInfo createUserInfo(){ + return new MemberInfo("홍길동(사용자)", "atom8426@naver.com", "atom.user", false, null, MemberRole.ROLE_USER, "인프라"); + } + + public static Category createMainCategory() { + return Category.builder() + .categoryId(1L) + .name("1차 카테고리") + .code("VM") + .isDeleted(false) + .descriptionExample("메인 카테고리 입니다.") + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + public static Category createCategory(Category mainCategory) { + return Category.builder() + .categoryId(2L) + .name("2차 카테고리") + .code("CR") + .isDeleted(false) + .descriptionExample("서브 카테고리 입니다.") + .mainCategory(mainCategory) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + } + + public static Task createTask(Long id, String taskCode, String title, TaskStatus taskStatus, Category category, LocalDateTime finishedAt, Member processor) { + return Task.builder() + .taskId(id) + .taskCode(taskCode) + .title(title) + .description(null) + .category(category) + .taskStatus(taskStatus) + .finishedAt(finishedAt) + .processor(processor) + .label(createLabel()) + .build(); + } + + public static Label createLabel() { + return Label.builder() + .labelId(1L) + .admin(null) + .labelName("레이블") + .labelColor(LabelColor.BLUE) + .isDeleted(false) + .build(); + } + + + public static PageImpl createTaskPage(List tasks) { + return new PageImpl<>(tasks); + } +} diff --git a/src/test/java/clap/server/application/service/task/ApprovalTaskServiceTest.java b/src/test/java/clap/server/application/service/task/ApprovalTaskServiceTest.java new file mode 100644 index 00000000..aeb225cb --- /dev/null +++ b/src/test/java/clap/server/application/service/task/ApprovalTaskServiceTest.java @@ -0,0 +1,116 @@ +package clap.server.application.service.task; + +import clap.server.adapter.inbound.web.dto.task.request.ApprovalTaskRequest; +import clap.server.adapter.inbound.web.dto.task.response.ApprovalTaskResponse; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.TestDataFactory; +import clap.server.application.port.inbound.domain.CategoryService; +import clap.server.application.port.inbound.domain.LabelService; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.inbound.domain.TaskService; +import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; +import clap.server.application.service.webhook.SendNotificationService; +import clap.server.domain.model.member.Member; +import clap.server.domain.model.task.Category; +import clap.server.domain.model.task.Label; +import clap.server.domain.model.task.Task; +import clap.server.domain.model.task.TaskHistory; +import clap.server.domain.policy.task.RequestedTaskUpdatePolicy; +import clap.server.exception.DomainException; +import clap.server.exception.code.TaskErrorCode; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +class ApprovalTaskServiceTest { + + @InjectMocks + private ApprovalTaskService approvalTaskService; + + @Mock + private MemberService memberService; + + @Mock + private TaskService taskService; + + @Mock + private CategoryService categoryService; + + @Mock + private LabelService labelService; + @Mock + private RequestedTaskUpdatePolicy requestedTaskUpdatePolicy; + + @Mock + private CommandTaskHistoryPort commandTaskHistoryPort; + + @Mock + private SendNotificationService sendNotificationService; + + + private Member reviewer, processor; + private Task task; + private Category category, mainCategory; + + @BeforeEach + void setUp() { + reviewer = TestDataFactory.createManagerWithReviewer(); + processor = TestDataFactory.createManager(); + mainCategory = TestDataFactory.createMainCategory(); + category = TestDataFactory.createCategory(mainCategory); + task = TestDataFactory.createTask(1L, "TC001", "제목1", TaskStatus.REQUESTED, category, null, processor); + } + + @Test + @DisplayName("작업 승인 처리") + void approvalTask() { + //given + Long reviewerId = 2L; + Long taskId = 1L; + ApprovalTaskRequest approvalTaskRequest = new ApprovalTaskRequest(2L, 2L, null, null); + + when(memberService.findReviewer(reviewerId)).thenReturn(reviewer); + when(taskService.findById(taskId)).thenReturn(task); + when(memberService.findById(approvalTaskRequest.processorId())).thenReturn(processor); + when(categoryService.findById(approvalTaskRequest.categoryId())).thenReturn(category); + when(taskService.upsert(task)).thenReturn(task); + + //when + ApprovalTaskResponse response = approvalTaskService.approvalTaskByReviewer(reviewerId, taskId, approvalTaskRequest); + + //then + assertThat(response).isNotNull(); + assertThat(response.taskId()).isEqualTo(task.getTaskId()); + assertThat(response.taskStatus()).isEqualTo(TaskStatus.IN_PROGRESS); + verify(requestedTaskUpdatePolicy).validateTaskRequested(task); + } + + @Test + @DisplayName("작업 승인 처리 중 예외 - 상태 불일치") + void approvalTask_throwsDomainException_whenTaskStatusIsNotRequested() { + //given + Long reviewerId = 2L; + Long taskId = 1L; + ApprovalTaskRequest approvalTaskRequest = new ApprovalTaskRequest(2L, 2L, null, null); + task = TestDataFactory.createTask(1L, "TC001", "제목1", TaskStatus.COMPLETED, category, null, processor); + when(taskService.findById(taskId)).thenReturn(task); + + //when + doThrow(new DomainException(TaskErrorCode.TASK_STATUS_MISMATCH)) + .when(requestedTaskUpdatePolicy).validateTaskRequested(task); + //then + assertThatThrownBy(() -> approvalTaskService.approvalTaskByReviewer(reviewerId, taskId, approvalTaskRequest)) + .isInstanceOf(DomainException.class) + .hasMessage(TaskErrorCode.TASK_STATUS_MISMATCH.getMessage()); + } +} diff --git a/src/test/java/clap/server/application/service/task/FindTaskListServiceTest.java b/src/test/java/clap/server/application/service/task/FindTaskListServiceTest.java deleted file mode 100644 index c389d366..00000000 --- a/src/test/java/clap/server/application/service/task/FindTaskListServiceTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package clap.server.application.service.task; - -import clap.server.adapter.inbound.web.dto.common.PageResponse; -import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest; -import clap.server.adapter.inbound.web.dto.task.response.FilterPendingApprovalResponse; -import clap.server.application.mapper.TaskResponseMapper; -import clap.server.application.port.inbound.domain.MemberService; -import clap.server.application.port.outbound.task.LoadTaskPort; -import clap.server.domain.model.task.Task; -import org.junit.jupiter.api.DisplayName; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; - -import java.time.LocalDateTime; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -//@ExtendWith(MockitoExtension.class) -class FindTaskListServiceTest { - - @Mock - private MemberService memberService; - @Mock - private LoadTaskPort loadTaskPort; - @InjectMocks - private FindTaskListService findTaskListService; - - private FilterTaskListRequest filterTaskListRequest; - private Pageable pageable; - private PageResponse expectedResponse; - private Page pageResponse; - - //@BeforeEach - void setUp() { - pageable = PageRequest.of(0, 20); - filterTaskListRequest = new FilterTaskListRequest( - null, List.of(2L), List.of(1L), "작업 제목", "", List.of(), "REQUESTED_AT", "DESC" - ); - Task task1 = Task.builder() - .taskId(1L) - .taskCode("TC001") - .title("작업 제목") - .dueDate(LocalDateTime.of(2025, 1, 24, 12, 30)) - .build(); - - Task task2 = Task.builder() - .taskId(2L) - .taskCode("TC002") - .title("다른 작업 제목") - .dueDate(LocalDateTime.of(2025, 1, 15, 14, 30)) - .build(); - - - pageResponse = new PageImpl<>(List.of(task1, task2), pageable, 2); - expectedResponse = PageResponse.from(pageResponse.map(TaskResponseMapper::toFilterPendingApprovalTasksResponse)); - } - - //@Test - @DisplayName("승인대기 중인 작업요청목록 조회 - 정상적인 데이터 반환") - void findPendingApprovalTasks_ReturnFilteredTasks() { - // given - Long managerId = 1L; - when(loadTaskPort.findTasksRequestedByUser(eq(managerId), eq(pageable), eq(filterTaskListRequest))) - .thenReturn(pageResponse); - - // when - PageResponse result = findTaskListService.findPendingApprovalTasks(managerId, pageable, filterTaskListRequest); - - // then - assertThat(result).isNotNull(); - assertThat(result.totalElements()).isEqualTo(2); - assertThat(result.content()).hasSize(2) - .extracting(FilterPendingApprovalResponse::taskId) - .containsExactly(1L, 2L); - } - - //@Test - @DisplayName("승인대기 중인 작업요청목록 조회 - 필터 조건에 맞는 작업 없음") - void findPendingApprovalTasks_NoTasksFound() { - // given - Long managerId = 1L; - FilterTaskListRequest filterWithNoResults = new FilterTaskListRequest( - null, List.of(999L), List.of(1000L), "없는 작업 제목", "", List.of(), "REQUESTED_AT", "DESC" - ); - when(loadTaskPort.findTasksRequestedByUser(eq(managerId), eq(pageable), eq(filterWithNoResults))) - .thenReturn(Page.empty()); - - // when - PageResponse result = findTaskListService.findPendingApprovalTasks(managerId, pageable, filterWithNoResults); - - // then - assertThat(result).isNotNull(); - assertThat(result.totalElements()).isEqualTo(0); - assertThat(result.content()).isEmpty(); - } - - //@Test - @DisplayName("승인대기 중인 작업요청목록 조회 - 필터 조건에 따른 정확한 결과 반환") - void findPendingApprovalTasks_FilterByTitle() { - // given - Long managerId = 1L; - FilterTaskListRequest filterByTitle = new FilterTaskListRequest( - null, List.of(2L), List.of(1L), "작업 제목", "", List.of(), "REQUESTED_AT", "DESC" - ); - when(loadTaskPort.findTasksRequestedByUser(eq(managerId), eq(pageable), eq(filterByTitle))) - .thenReturn(pageResponse); - - // when - PageResponse result = findTaskListService.findPendingApprovalTasks(managerId, pageable, filterByTitle); - - // then - assertThat(result).isNotNull(); - assertThat(result.totalElements()).isEqualTo(2); - assertThat(result.content()) - .extracting(FilterPendingApprovalResponse::title) - .containsExactly("작업 제목", "다른 작업 제목"); - } -} diff --git a/src/test/java/clap/server/application/service/task/FindTasksRequestedByUserTest.java b/src/test/java/clap/server/application/service/task/FindTasksRequestedByUserTest.java new file mode 100644 index 00000000..50cccf6e --- /dev/null +++ b/src/test/java/clap/server/application/service/task/FindTasksRequestedByUserTest.java @@ -0,0 +1,103 @@ +package clap.server.application.service.task; + +import clap.server.adapter.inbound.web.dto.common.PageResponse; +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest; +import clap.server.adapter.inbound.web.dto.task.response.FilterRequestedTasksResponse; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.TestDataFactory; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.outbound.task.LoadTaskPort; +import clap.server.domain.model.member.Member; +import clap.server.domain.model.task.Category; +import clap.server.domain.model.task.Task; +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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FindTasksRequestedByUserTest { + + @Mock + private MemberService memberService; + @Mock + private LoadTaskPort loadTaskPort; + @InjectMocks + private FindTaskListService findTaskListService; + private Member user; + private Page taskPage; + private Task task1, task2; + private Category category, mainCategory; + + @BeforeEach + void setUp() { + user = TestDataFactory.createUser(); + mainCategory = TestDataFactory.createMainCategory(); + category = TestDataFactory.createCategory(mainCategory); + task1 = TestDataFactory.createTask(1L, "TC001", "제목1", TaskStatus.REQUESTED, category,null, null); + task2 = TestDataFactory.createTask(2L, "TC002", "제목2", TaskStatus.COMPLETED, category, LocalDateTime.of(2025, 2, 4, 11, 30, 11), user); + taskPage = new PageImpl<>(List.of(task1, task2)); + } + + @Test + @DisplayName("요청한 작업 목록 조회") + void findRequestedByUserTasks() { + //given + Long userId = 4L; + PageRequest pageable = PageRequest.of(0, 20); + FilterTaskListRequest filterTaskListRequest = new FilterTaskListRequest(null, List.of(), List.of(), "", "", List.of(), "", ""); + when(memberService.findActiveMember(userId)).thenReturn(user); + when(loadTaskPort.findTasksRequestedByUser(userId, pageable, filterTaskListRequest)) + .thenReturn(taskPage); + //when + PageResponse result = findTaskListService.findTasksRequestedByUser(userId, pageable, filterTaskListRequest); + + //then + assertThat(result.content()).hasSize(2) + .extracting(FilterRequestedTasksResponse::taskId) + .containsExactly(1L, 2L); + + FilterRequestedTasksResponse task1Response = result.content().get(0); + assertThat(task1Response.taskId()).isEqualTo(1L); + assertThat(task1Response.taskCode()).isEqualTo("TC001"); + assertThat(task1Response.mainCategoryName()).isEqualTo("1차 카테고리"); + assertThat(task1Response.categoryName()).isEqualTo("2차 카테고리"); + assertThat(task1Response.title()).isEqualTo("제목1"); + assertThat(task1Response.processorName()).isEqualTo(""); + assertThat(task1Response.taskStatus()).isEqualTo(TaskStatus.REQUESTED); + assertThat(task1Response.finishedAt()).isNull(); + } + + @Test + @DisplayName("요청한 작업 목록 조회 - 카테고리 조건") + void findRequestedByUserTasks_FilteredWithCategory() { + // given + Long userId = 4L; + PageRequest pageable = PageRequest.of(0, 20); + FilterTaskListRequest filterTaskListRequest = new FilterTaskListRequest(null, List.of(2L), List.of(), "", "", List.of(), "", ""); + taskPage = new PageImpl<>(List.of(task2)); + when(memberService.findActiveMember(userId)).thenReturn(user); + when(loadTaskPort.findTasksRequestedByUser(user.getMemberId(), pageable, filterTaskListRequest)) + .thenReturn(taskPage); + + // when + PageResponse result = findTaskListService.findTasksRequestedByUser(userId, pageable, filterTaskListRequest); + + // then + assertThat(result.content()).hasSize(1); + assertThat(result.content()).extracting(FilterRequestedTasksResponse::categoryName) + .containsExactly( "2차 카테고리"); + } +}