diff --git a/app/src/main/java/com/mangoboss/app/domain/repository/DocumentRepository.java b/app/src/main/java/com/mangoboss/app/domain/repository/DocumentRepository.java index 4884eb57..c01dce8b 100644 --- a/app/src/main/java/com/mangoboss/app/domain/repository/DocumentRepository.java +++ b/app/src/main/java/com/mangoboss/app/domain/repository/DocumentRepository.java @@ -6,7 +6,7 @@ import java.util.List; public interface DocumentRepository { - void save(DocumentEntity entity); + DocumentEntity save(DocumentEntity entity); DocumentEntity getByIdAndStaffId(Long id, Long staffId); diff --git a/app/src/main/java/com/mangoboss/app/domain/service/contract/ContractService.java b/app/src/main/java/com/mangoboss/app/domain/service/contract/ContractService.java index 2fbf589e..fc68d0f6 100644 --- a/app/src/main/java/com/mangoboss/app/domain/service/contract/ContractService.java +++ b/app/src/main/java/com/mangoboss/app/domain/service/contract/ContractService.java @@ -145,20 +145,20 @@ public List findAllByStoreId(final Long storeId) { return contractRepository.findAllByStoreId(storeId); } - private byte[] generateContractPdf(final ContractData contractData, final String bossSignatureKey) { + public byte[] generateContractPdf(final ContractData contractData, final String bossSignatureKey) { final String bossSignatureBase64 = fetchSignatureBase64(bossSignatureKey); final String html = contractHtmlGenerator.generateHtmlWithBossSignature(contractData, bossSignatureBase64); return pdfGenerator.generatePdfFromHtml(html); } - private byte[] generateStaffSignedContractPdf(final ContractData data, final String bossSignatureKey, final String staffSignatureKey) { + public byte[] generateStaffSignedContractPdf(final ContractData data, final String bossSignatureKey, final String staffSignatureKey) { final String bossSignatureBase64 = fetchSignatureBase64(bossSignatureKey); final String staffSignatureBase64 = fetchSignatureBase64(staffSignatureKey); final String html = contractHtmlGenerator.generateHtmlWithStaffSignature(data, bossSignatureBase64, staffSignatureBase64); return pdfGenerator.generatePdfFromHtml(html); } - private String fetchSignatureBase64(final String signatureKey) { + public String fetchSignatureBase64(final String signatureKey) { return s3FileManager.fetchAsBase64(signatureKey, ContentType.PNG.getMimeType()); } } \ No newline at end of file diff --git a/app/src/main/java/com/mangoboss/app/infra/persistence/DocumentRepositoryImpl.java b/app/src/main/java/com/mangoboss/app/infra/persistence/DocumentRepositoryImpl.java index 354b63e4..2a1a9586 100644 --- a/app/src/main/java/com/mangoboss/app/infra/persistence/DocumentRepositoryImpl.java +++ b/app/src/main/java/com/mangoboss/app/infra/persistence/DocumentRepositoryImpl.java @@ -17,8 +17,8 @@ public class DocumentRepositoryImpl implements DocumentRepository { private final DocumentJpaRepository documentJpaRepository; @Override - public void save(DocumentEntity entity) { - documentJpaRepository.save(entity); + public DocumentEntity save(DocumentEntity entity) { + return documentJpaRepository.save(entity); } @Override diff --git a/app/src/test/java/com/mangoboss/app/domain/service/attendance/context/AttendanceStrategyContextTest.java b/app/src/test/java/com/mangoboss/app/domain/service/attendance/context/AttendanceStrategyContextTest.java new file mode 100644 index 00000000..8407fd0f --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/attendance/context/AttendanceStrategyContextTest.java @@ -0,0 +1,105 @@ +package com.mangoboss.app.domain.service.attendance.context; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.domain.service.attendance.strategy.AttendanceValidationStrategy; +import com.mangoboss.app.dto.attendance.base.AttendanceBaseRequest; +import com.mangoboss.storage.store.AttendanceMethod; +import com.mangoboss.storage.store.StoreEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +class AttendanceStrategyContextTest { + + private AttendanceStrategyContext context; + private AttendanceValidationStrategy mockStrategy; + + @BeforeEach + void setUp() { + mockStrategy = mock(AttendanceValidationStrategy.class); + context = new AttendanceStrategyContext(List.of(mockStrategy)); + } + + @Test + void 전략이_정상적으로_선택되면_검증이_수행된다() { + // given + StoreEntity store = mock(StoreEntity.class); + TestRequest request = new TestRequest(); + + when(mockStrategy.supports(store, request.attendanceMethod())).thenReturn(true); + when(mockStrategy.getRequestType()).thenReturn(TestRequest.class); + + // when + context.validate(store, request); + + // then + verify(mockStrategy).executionValidate(store, request); + } + + @Test + void 지원하는_전략이_없으면_예외가_발생한다() { + // given + StoreEntity store = mock(StoreEntity.class); + TestRequest request = new TestRequest(); + + when(mockStrategy.supports(store, request.attendanceMethod())).thenReturn(false); + when(mockStrategy.getRequestType()).thenReturn(TestRequest.class); + + // when & then + assertThatThrownBy(() -> context.validate(store, request)) + .isInstanceOf(CustomException.class) + .satisfies(e -> assertThat(((CustomException) e).getErrorCode()) + .isEqualTo(CustomErrorInfo.INVALID_ATTENDANCE_REQUEST_TYPE)); + + verify(mockStrategy, never()).executionValidate(any(), any()); + } + + @Test + void request_타입이_전략에서_요구하는_타입과_다르면_예외가_발생한다() { + // given + StoreEntity store = mock(StoreEntity.class); + WrongRequest request = new WrongRequest(); + + when(mockStrategy.supports(store, request.attendanceMethod())).thenReturn(true); + when(mockStrategy.getRequestType()).thenReturn(TestRequest.class); // 타입 불일치 + + // when & then + assertThatThrownBy(() -> context.validate(store, request)) + .isInstanceOf(CustomException.class) + .satisfies(e -> assertThat(((CustomException) e).getErrorCode()) + .isEqualTo(CustomErrorInfo.INVALID_ATTENDANCE_REQUEST_TYPE)); + + verify(mockStrategy, never()).executionValidate(any(), any()); + } + + // 테스트용 request 구현체들 + private static class TestRequest implements AttendanceBaseRequest { + @Override + public AttendanceMethod attendanceMethod() { + return AttendanceMethod.GPS; + } + + @Override + public Long scheduleId() { + return 1L; + } + } + + private static class WrongRequest implements AttendanceBaseRequest { + @Override + public AttendanceMethod attendanceMethod() { + return AttendanceMethod.GPS; + } + + @Override + public Long scheduleId() { + return 2L; + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/BothValidationStrategyTest.java b/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/BothValidationStrategyTest.java new file mode 100644 index 00000000..3dae6353 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/BothValidationStrategyTest.java @@ -0,0 +1,68 @@ +package com.mangoboss.app.domain.service.attendance.strategy; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.dto.attendance.request.AttendanceBothRequest; +import com.mangoboss.storage.store.AttendanceMethod; +import com.mangoboss.storage.store.StoreEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class BothValidationStrategyTest { + + private BothValidationStrategy bothValidationStrategy; + private StoreEntity store; + + @BeforeEach + void setUp() { + bothValidationStrategy = new BothValidationStrategy(); + store = StoreEntity.builder() + .qrCode("VALID_QR") + .gpsLatitude(37.123) + .gpsLongitude(127.123) + .gpsRangeMeters(100) + .attendanceMethod(AttendanceMethod.BOTH) + .build(); + } + + @Test + void 유효한_요청이면_검증을_통과한다() { + var request = new AttendanceBothRequest(AttendanceMethod.BOTH, 1L, "VALID_QR", 37.123, 127.123, LocalDateTime.now()); + assertDoesNotThrow(() -> bothValidationStrategy.executionValidate(store, request)); + } + + @Test + void 잘못된_qr이면_검증에서_예외가_발생한다() { + var request = new AttendanceBothRequest(AttendanceMethod.BOTH, 1L, "WRONG_QR", 37.123, 127.123, LocalDateTime.now()); + var ex = assertThrows(CustomException.class, () -> bothValidationStrategy.executionValidate(store, request)); + assertEquals(CustomErrorInfo.INVALID_QR_CODE, ex.getErrorCode()); + } + + @Test + void gps_범위를_벗어나면_예외가_발생한다() { + var request = new AttendanceBothRequest(AttendanceMethod.BOTH, 1L, "VALID_QR", 36.0, 126.0, LocalDateTime.now()); + var ex = assertThrows(CustomException.class, () -> bothValidationStrategy.executionValidate(store, request)); + assertEquals(CustomErrorInfo.GPS_OUT_OF_RANGE, ex.getErrorCode()); + } + + @Test + void gps_시간차이가_10초를_초과하면_예외가_발생한다() { + var request = new AttendanceBothRequest(AttendanceMethod.BOTH, 1L, "VALID_QR", 37.123, 127.123, LocalDateTime.now().minusSeconds(20)); + var ex = assertThrows(CustomException.class, () -> bothValidationStrategy.executionValidate(store, request)); + assertEquals(CustomErrorInfo.INVALID_GPS_TIME, ex.getErrorCode()); + } + + @Test + void 지원하는_가게_설정과_출결방식이면_true를_반환한다() { + assertTrue(bothValidationStrategy.supports(store, AttendanceMethod.BOTH)); + } + + @Test + void 출결방식이나_가게설정이_다르면_false를_반환한다() { + assertFalse(bothValidationStrategy.supports(store, AttendanceMethod.QR)); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/GpsValidationStrategyTest.java b/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/GpsValidationStrategyTest.java new file mode 100644 index 00000000..51ca96f3 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/GpsValidationStrategyTest.java @@ -0,0 +1,101 @@ +package com.mangoboss.app.domain.service.attendance.strategy; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.dto.attendance.base.AttendanceBaseRequest; +import com.mangoboss.app.dto.attendance.request.AttendanceGpsRequest; +import com.mangoboss.storage.store.AttendanceMethod; +import com.mangoboss.storage.store.StoreEntity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class GpsValidationStrategyTest { + + private final GpsValidationStrategy gpsValidationStrategy = new GpsValidationStrategy(); + + private final StoreEntity store = StoreEntity.builder() + .attendanceMethod(AttendanceMethod.GPS) + .gpsLatitude(37.1234) + .gpsLongitude(127.1234) + .gpsRangeMeters(100) + .qrCode("QR") + .build(); + + @Test + void gps_범위_내_정상시간이면_예외_없음() { + AttendanceGpsRequest request = new AttendanceGpsRequest( + AttendanceMethod.GPS, + 1L, + 37.1234, 127.1234, + LocalDateTime.now() + ); + + assertDoesNotThrow(() -> gpsValidationStrategy.executionValidate(store, request)); + } + + @Test + void gps_범위_초과시_예외() { + AttendanceGpsRequest request = new AttendanceGpsRequest( + AttendanceMethod.GPS, + 1L, + 36.0, 126.0, // 거리 매우 멀 때 + LocalDateTime.now() + ); + + CustomException ex = assertThrows(CustomException.class, () -> + gpsValidationStrategy.executionValidate(store, request)); + + assertEquals(CustomErrorInfo.GPS_OUT_OF_RANGE, ex.getCustomErrorInfo()); + } + + @Test + void gps_시간_지연되면_예외() { + AttendanceGpsRequest request = new AttendanceGpsRequest( + AttendanceMethod.GPS, + 1L, + 37.1234, 127.1234, + LocalDateTime.now().minusSeconds(20) + ); + + CustomException ex = assertThrows(CustomException.class, () -> + gpsValidationStrategy.executionValidate(store, request)); + + assertEquals(CustomErrorInfo.INVALID_GPS_TIME, ex.getCustomErrorInfo()); + } + + @Test + void 지원하지않는_request_type이면_예외발생() { + AttendanceBaseRequest wrongRequest = new AttendanceBaseRequest() { + @Override + public AttendanceMethod attendanceMethod() { + return AttendanceMethod.GPS; + } + + @Override + public Long scheduleId() { + return 1L; + } + }; + + CustomException ex = assertThrows(CustomException.class, + () -> gpsValidationStrategy.executionValidate(store, wrongRequest)); + + assertEquals(CustomErrorInfo.INVALID_ATTENDANCE_REQUEST_TYPE, ex.getCustomErrorInfo()); + } + + @Test + void gps방식과_설정이_일치하면_supports는_true를_반환한다() { + assertTrue(gpsValidationStrategy.supports(store, AttendanceMethod.GPS)); + } + + @Test + void gps방식과_설정이_일치하지_않으면_supports는_false를_반환한다() { + assertFalse(gpsValidationStrategy.supports(store, AttendanceMethod.QR)); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/QrValidationStrategyTest.java b/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/QrValidationStrategyTest.java new file mode 100644 index 00000000..fc0bbe4e --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/attendance/strategy/QrValidationStrategyTest.java @@ -0,0 +1,54 @@ +package com.mangoboss.app.domain.service.attendance.strategy; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.dto.attendance.request.AttendanceQrRequest; +import com.mangoboss.storage.store.AttendanceMethod; +import com.mangoboss.storage.store.StoreEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class QrValidationStrategyTest { + + private QrValidationStrategy qrValidationStrategy; + private StoreEntity store; + + @BeforeEach + void setUp() { + qrValidationStrategy = new QrValidationStrategy(); + store = StoreEntity.builder() + .qrCode("VALID_QR") + .attendanceMethod(AttendanceMethod.QR) + .build(); + } + + @Test + void 올바른_qr이면_통과() { + var request = new AttendanceQrRequest(AttendanceMethod.QR, 1L, "VALID_QR"); + + assertDoesNotThrow(() -> qrValidationStrategy.executionValidate(store, request)); + } + + @Test + void 잘못된_qr이면_예외() { + var request = new AttendanceQrRequest(AttendanceMethod.QR, 1L, "WRONG_QR"); + + CustomException ex = assertThrows(CustomException.class, () -> + qrValidationStrategy.executionValidate(store, request) + ); + + assertEquals(CustomErrorInfo.INVALID_QR_CODE, ex.getErrorCode()); + } + + @Test + void qr방식과_설정이_일치하면_supports는_true를_반환한다() { + assertTrue(qrValidationStrategy.supports(store, AttendanceMethod.QR)); + } + + @Test + void qr방식과_설정이_일치하지_않으면_supports는_false를_반환한다() { + assertFalse(qrValidationStrategy.supports(store, AttendanceMethod.GPS)); + } +} diff --git a/app/src/test/java/com/mangoboss/app/domain/service/billing/BillingServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/billing/BillingServiceTest.java new file mode 100644 index 00000000..a1ac97b9 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/billing/BillingServiceTest.java @@ -0,0 +1,152 @@ +package com.mangoboss.app.domain.service.billing; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.common.util.JsonConverter; +import com.mangoboss.app.domain.repository.BillingRepository; +import com.mangoboss.app.dto.billing.response.BillingCardInfoResponse; +import com.mangoboss.app.dto.subscription.response.BillingCustomerKeyResponse; +import com.mangoboss.app.external.tosspayment.TossPaymentClient; +import com.mangoboss.storage.billing.BillingEntity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BillingServiceTest { + + @InjectMocks + private BillingService billingService; + + @Mock + private TossPaymentClient tossPaymentClient; + + @Mock + private BillingRepository billingRepository; + + private final Long bossId = 1L; + + @Test + void 고객키가_이미_존재하면_저장하지_않고_반환한다() { + final String customerKey = "boss-1-" + UUID.randomUUID(); + BillingEntity billing = mock(BillingEntity.class); + when(billing.getCustomerKey()).thenReturn(customerKey); + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.of(billing)); + + BillingCustomerKeyResponse response = billingService.getOrCreateCustomerKey(bossId); + + assertEquals(customerKey, response.customerKey()); + verify(billingRepository, never()).save(any()); + } + + @Test + void 고객키가_없으면_새로_생성하고_저장한다() { + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.empty()); + + BillingCustomerKeyResponse response = billingService.getOrCreateCustomerKey(bossId); + + assertNotNull(response.customerKey()); + verify(billingRepository).save(any()); + } + + @Test + void 빌링키_발급에_성공하면_정상적으로_등록한다() { + final String customerKey = "boss-1-" + UUID.randomUUID(); + final String billingKey = "billing-key"; + final String cardDataJson = "{\"issuerCode\":\"123\",\"number\":\"1234-5678-****-****\",\"cardType\":\"신용\",\"ownerType\":\"개인\"}"; + + Map mockResponse = Map.of( + "billingKey", billingKey, + "card", JsonConverter.fromJson(cardDataJson, new TypeReference>() {}) + ); + + BillingEntity billing = mock(BillingEntity.class); + + when(tossPaymentClient.issueBillingKey(eq(customerKey), anyString())).thenReturn(mockResponse); + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.of(billing)); + + billingService.issueBillingKey(bossId, customerKey, "auth-key"); + + verify(billing).registerBillingKey(eq(billingKey), anyString()); + } + + @Test + void 빌링정보가_없으면_빌링키_발급시_예외를_던진다() { + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.empty()); + + CustomException ex = assertThrows(CustomException.class, + () -> billingService.issueBillingKey(bossId, "key", "auth")); + + assertEquals(CustomErrorInfo.BILLING_NOT_FOUND, ex.getErrorCode()); + } + + @Test + void 빌링키가_있으면_삭제에_성공한다() { + BillingEntity billing = mock(BillingEntity.class); + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.of(billing)); + + billingService.deleteBillingKey(bossId); + + verify(billingRepository).delete(billing); + } + + @Test + void 빌링정보가_없으면_삭제시_예외를_던진다() { + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.empty()); + + assertThrows(CustomException.class, + () -> billingService.deleteBillingKey(bossId)); + } + + @Test + void 빌링정보가_존재하면_검증에_성공한다() { + when(billingRepository.existsByBossId(bossId)).thenReturn(true); + assertDoesNotThrow(() -> billingService.validateBillingExists(bossId)); + } + + @Test + void 빌링정보가_없으면_검증시_예외를_던진다() { + when(billingRepository.existsByBossId(bossId)).thenReturn(false); + assertThrows(CustomException.class, + () -> billingService.validateBillingExists(bossId)); + } + + @Test + void 빌링카드정보가_정상적으로_조회된다() { + final String billingKey = "billing-key"; + final String cardDataJson = "{\"issuerCode\":\"123\",\"number\":\"1234-5678-****-****\",\"cardType\":\"신용\",\"ownerType\":\"개인\"}"; + + BillingEntity billing = mock(BillingEntity.class); + when(billing.getBillingKey()).thenReturn(billingKey); + when(billing.getCardData()).thenReturn(cardDataJson); + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.of(billing)); + + BillingCardInfoResponse response = billingService.getBillingCardInfo(bossId); + + assertEquals("신용", response.cardType()); + assertEquals("개인", response.ownerType()); + assertEquals("알 수 없음", response.cardCompany()); + } + + @Test + void 카드정보가_없으면_null을_반환한다() { + BillingEntity entity = mock(BillingEntity.class); + when(entity.getBillingKey()).thenReturn(null); + when(billingRepository.findByBossId(bossId)).thenReturn(Optional.of(entity)); + + BillingCardInfoResponse response = billingService.getBillingCardInfo(bossId); + + assertNull(response.cardType()); + assertNull(response.ownerType()); + assertNull(response.cardCompany()); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractHtmlGeneratorTest.java b/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractHtmlGeneratorTest.java new file mode 100644 index 00000000..70ec91ca --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractHtmlGeneratorTest.java @@ -0,0 +1,73 @@ +package com.mangoboss.app.domain.service.contract; + +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.dto.contract.request.ContractData; +import com.mangoboss.app.dto.contract.request.WorkSchedule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ContractHtmlGeneratorTest { + + private ContractHtmlGenerator generator; + + @BeforeEach + void setUp() { + generator = new ContractHtmlGenerator(); + + ReflectionTestUtils.setField(generator, "contractTemplatePath", "templates/contract-template.html"); + } + + private ContractData createSampleContractData() { + WorkSchedule schedule = new WorkSchedule(DayOfWeek.MONDAY, LocalTime.of(9, 0), LocalTime.of(18, 0)); + return ContractData.builder() + .contractName("근로계약서") + .storeName("망고가게") + .staffName("홍길동") + .contractStart(LocalDate.of(2024, 1, 1)) + .contractEnd(LocalDate.of(2024, 12, 31)) + .bossName("김사장") + .storeAddress("서울특별시 중구") + .duty("서빙 및 청소") + .workSchedules(List.of(schedule)) + .hourlyWage(10000) + .businessNumber("123-45-67890") + .staffPhone("010-1234-5678") + .build(); + } + + @Test + void 보스_서명이_포함된_HTML을_정상적으로_생성한다() { + ContractData data = createSampleContractData(); + String result = generator.generateHtmlWithBossSignature(data, "base64-boss-signature"); + + assertThat(result).contains("base64-boss-signature"); + assertThat(result).contains(data.staffName()); + } + + @Test + void 보스_알바_서명이_모두_포함된_HTML을_정상적으로_생성한다() { + ContractData data = createSampleContractData(); + String result = generator.generateHtmlWithStaffSignature(data, "base64-boss-signature", "base64-staff-signature"); + + assertThat(result).contains("base64-boss-signature"); + assertThat(result).contains("base64-staff-signature"); + } + + @Test + void HTML_생성_중_예외가_발생하면_보스_서명_예외를_던진다() { + ContractHtmlGenerator faultyGenerator = new ContractHtmlGenerator(); + + assertThrows(CustomException.class, () -> + ReflectionTestUtils.invokeMethod(faultyGenerator, "generateHtmlWithBossSignature", createSampleContractData(), "base64") + ); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractServiceTest.java new file mode 100644 index 00000000..6059911b --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractServiceTest.java @@ -0,0 +1,352 @@ +package com.mangoboss.app.domain.service.contract; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.common.security.EncryptedFileDecoder; +import com.mangoboss.app.common.util.DigestUtil; +import com.mangoboss.app.common.util.PdfGenerator; +import com.mangoboss.app.common.util.S3FileManager; +import com.mangoboss.app.domain.repository.ContractRepository; +import com.mangoboss.app.dto.contract.request.ContractData; +import com.mangoboss.app.dto.contract.request.ContractTemplateData; +import com.mangoboss.app.dto.contract.request.WorkSchedule; +import com.mangoboss.storage.contract.ContractEntity; +import com.mangoboss.storage.metadata.ContentType; +import com.mangoboss.storage.metadata.S3FileType; +import com.mangoboss.storage.schedule.RegularGroupEntity; +import com.mangoboss.storage.staff.StaffEntity; +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.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.*; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ContractServiceTest { + + @Mock + private ContractRepository contractRepository; + + @Mock + private S3FileManager s3FileManager; + + @Mock + private ContractHtmlGenerator contractHtmlGenerator; + + @Mock + private PdfGenerator pdfGenerator; + + @Mock + private EncryptedFileDecoder encryptedFileDecoder; + + @InjectMocks + private ContractService contractService; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + Clock fixedClock = Clock.fixed(Instant.parse("2024-06-01T10:15:30Z"), ZoneId.of("UTC")); + contractService = new ContractService(contractRepository, s3FileManager, contractHtmlGenerator, pdfGenerator, encryptedFileDecoder, fixedClock); + } + + @Test + void 사장님_서명을_업로드할_수_있다() { + // given + String signatureData = "encryptedSignatureData"; + String signatureKey = "test-signature-key"; + byte[] fileBytes = new byte[]{1, 2, 3}; + + EncryptedFileDecoder.DecodedFile decodedFile = new EncryptedFileDecoder.DecodedFile("image/png", fileBytes); + when(encryptedFileDecoder.decode(signatureData)).thenReturn(decodedFile); + when(s3FileManager.generateFileKey(S3FileType.SIGNATURE, ContentType.PNG.getExtension())).thenReturn(signatureKey); + + // when + String resultKey = contractService.uploadSignature(signatureData); + + // then + verify(s3FileManager).upload(fileBytes, signatureKey, ContentType.PNG.getMimeType()); + assertThat(resultKey).isEqualTo(signatureKey); + } + + @Test + void 근로계약서를_생성할_수_있다() { + // given + Long staffId = 1L; + String bossSignatureKey = "boss-signature-key"; + ContractData contractData = mock(ContractData.class); + byte[] pdfBytes = new byte[]{1, 2, 3}; + String fileKey = "contract-file-key"; + + when(contractHtmlGenerator.generateHtmlWithBossSignature(any(), any())).thenReturn(""); + when(pdfGenerator.generatePdfFromHtml(any())).thenReturn(pdfBytes); + when(s3FileManager.generateFileKey(S3FileType.CONTRACT, ContentType.PDF.getExtension())).thenReturn(fileKey); + when(contractRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + String savedKey = contractService.createContract(staffId, bossSignatureKey, contractData).getFileKey(); + + // then + verify(s3FileManager).upload(pdfBytes, fileKey, ContentType.PDF.getMimeType()); + assertThat(savedKey).isEqualTo(fileKey); + } + + @Test + void 근로계약서를_알바생이_서명할_수_있다() { + // given + Long contractId = 1L; + String staffSignatureKey = "staff-signature-key"; + String bossSignatureKey = "boss-signature-key"; + String fileKey = "contract-file-key"; + String contractDataJson = "{}"; + byte[] pdfBytes = new byte[]{1, 2, 3}; + + ContractData contractData = mock(ContractData.class); + ContractEntity contract = mock(ContractEntity.class); + + // mocking repository + when(contractRepository.getContractById(contractId)).thenReturn(contract); + when(contract.getContractDataJson()).thenReturn(contractDataJson); + when(contract.getBossSignatureKey()).thenReturn(bossSignatureKey); + when(contract.getFileKey()).thenReturn(fileKey); + + // mocking 내부 메서드 + ContractService spyService = spy(contractService); + doReturn(contractData).when(spyService).convertFromContractDataJson(contractDataJson); + doReturn(pdfBytes).when(spyService).generateStaffSignedContractPdf(contractData, bossSignatureKey, staffSignatureKey); + doReturn(contract).when(contract).completeStaffSign(any(), any(), any(), any()); + + // 실제 실행 + ContractEntity result = spyService.signByStaff(contractId, staffSignatureKey); + + // then + verify(s3FileManager).upload(pdfBytes, fileKey, ContentType.PDF.getMimeType()); + verify(contract).completeStaffSign(eq(fileKey), eq(staffSignatureKey), any(), any()); + assertThat(result).isEqualTo(contract); + } + + @Test + void 근로계약서를_삭제할_수_있다() { + // given + Long contractId = 1L; + String fileKey = "contract-file-key"; + + ContractEntity contract = mock(ContractEntity.class); + when(contractRepository.getContractById(contractId)).thenReturn(contract); + when(contract.getFileKey()).thenReturn(fileKey); + + // when + contractService.deleteContract(contractId); + + // then + verify(s3FileManager).deleteFileFromPrivateBucket(fileKey); + verify(contractRepository).delete(contract); + } + + + @Test + void 근로계약서를_스태프ID로_조회할_수_있다() { + Long staffId = 1L; + List contracts = List.of(mock(ContractEntity.class)); + when(contractRepository.findAllByStaffId(staffId)).thenReturn(contracts); + + List result = contractService.getContractsByStaffId(staffId); + + assertThat(result).isEqualTo(contracts); + } + + @Test + void 근로계약서를_매장ID로_조회할_수_있다() { + Long storeId = 1L; + List contracts = List.of(mock(ContractEntity.class)); + when(contractRepository.findAllByStoreId(storeId)).thenReturn(contracts); + + List result = contractService.findAllByStoreId(storeId); + + assertThat(result).isEqualTo(contracts); + } + + @Test + void PDF_무결성_검증이_성공한다() { + ContractEntity contract = mock(ContractEntity.class); + byte[] pdfBytes = new byte[]{1, 2, 3}; + String hash = DigestUtil.sha256(pdfBytes); + + when(contract.getFileKey()).thenReturn("file-key"); + when(contract.getPdfHash()).thenReturn(hash); + when(s3FileManager.fetchAsBytes("file-key")).thenReturn(pdfBytes); + + assertThatCode(() -> contractService.validatePdfIntegrity(contract)) + .doesNotThrowAnyException(); + } + + @Test + void PDF_무결성_검증이_실패하면_예외를_던진다() { + ContractEntity contract = mock(ContractEntity.class); + byte[] pdfBytes = new byte[]{1, 2, 3}; + + when(contract.getFileKey()).thenReturn("file-key"); + when(contract.getPdfHash()).thenReturn("invalid-hash"); + when(s3FileManager.fetchAsBytes("file-key")).thenReturn(pdfBytes); + + assertThatThrownBy(() -> contractService.validatePdfIntegrity(contract)) + .isInstanceOf(CustomException.class); + } + + @Test + void 해당_계약서가_다른_알바생거면_예외를_던진다() { + // given + Long contractStaffId = 1L; + Long staffId = 2L; + + // when & then + assertThatThrownBy(() -> contractService.validateContractBelongsToStaff(contractStaffId, staffId)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.CONTRACT_NOT_BELONG_TO_STAFF.getMessage()); + } + + @Test + void 해당_계약서가_해당_알바생거면_예외를_던지지_않는다() { + // given + Long contractStaffId = 1L; + Long staffId = 1L; + + // when & then + assertThatCode(() -> contractService.validateContractBelongsToStaff(contractStaffId, staffId)) + .doesNotThrowAnyException(); + } + + @Test + void 계약서를_삭제할때_알바생_서명이_없으면_예외를_던지지_않는다() { + // given + ContractEntity contract = mock(ContractEntity.class); + when(contract.getStaffSignatureKey()).thenReturn(null); + + // when & then + assertThatCode(() -> contractService.validateContractNotSignedByStaff(contract)) + .doesNotThrowAnyException(); + } + + @Test + void 계약서를_삭제할때_계약서에_알바생_서명이_있으면_예외를_던진다() { + // given + ContractEntity contract = mock(ContractEntity.class); + when(contract.getStaffSignatureKey()).thenReturn("signed-key"); + + // when & then + assertThatThrownBy(() -> contractService.validateContractNotSignedByStaff(contract)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.STAFF_SIGNED_CONTRACT_CANNOT_BE_DELETED.getMessage()); + } + + @Test + void JSON문자열을_ContractData로_변환할_수_있다() { + // given + String json = """ + { + "contractStart": "2024-06-01", + "contractEnd": "2024-06-30", + "hourlyWage": 10000, + "workSchedules": [] + } + """; + + // when + ContractData result = contractService.convertFromContractDataJson(json); + + // then + assertThat(result.contractStart()).isEqualTo(LocalDate.of(2024, 6, 1)); + assertThat(result.contractEnd()).isEqualTo(LocalDate.of(2024, 6, 30)); + assertThat(result.hourlyWage()).isEqualTo(10000); + assertThat(result.workSchedules()).isEmpty(); + } + + @Test + void JSON문자열을_ContractTemplateData로_변환할_수_있다() { + // given + String json = """ + { + "hourlyWage": 12000, + "workSchedules": [] + } + """; + + // when + ContractTemplateData result = contractService.convertFromContractTemplateJson(json); + + // then + assertThat(result.hourlyWage()).isEqualTo(12000); + assertThat(result.workSchedules()).isEmpty(); + } + + @Test + void 사장과_알바생_서명으로_계약서PDF를_생성할_수_있다() { + // given + ContractData contractData = mock(ContractData.class); + String bossKey = "boss-signature-key"; + String staffKey = "staff-signature-key"; + + String bossBase64 = "boss-signature"; + String staffBase64 = "staff-signature"; + String html = "signed"; + byte[] pdfBytes = new byte[]{1, 2, 3}; + + when(s3FileManager.fetchAsBase64(bossKey, ContentType.PNG.getMimeType())).thenReturn(bossBase64); + when(s3FileManager.fetchAsBase64(staffKey, ContentType.PNG.getMimeType())).thenReturn(staffBase64); + when(contractHtmlGenerator.generateHtmlWithStaffSignature(contractData, bossBase64, staffBase64)).thenReturn(html); + when(pdfGenerator.generatePdfFromHtml(html)).thenReturn(pdfBytes); + + // when + byte[] result = contractService.generateStaffSignedContractPdf(contractData, bossKey, staffKey); + + // then + assertThat(result).isEqualTo(pdfBytes); + } + + @Test + void 계약데이터에서_정기출근그룹리스트를_추출할_수_있다() { + // given + StaffEntity staff = mock(StaffEntity.class); + LocalDate startDate = LocalDate.of(2024, 6, 1); + LocalDate endDate = LocalDate.of(2024, 6, 30); + + WorkSchedule schedule = new WorkSchedule( + DayOfWeek.MONDAY, + LocalTime.of(9, 0), + LocalTime.of(18, 0) + ); + ContractData data = ContractData.builder() + .contractName("알바 근로계약서") + .storeName("망고보스 신촌점") + .staffName("김철수") + .contractStart(LocalDate.of(2024, 6, 1)) + .contractEnd(LocalDate.of(2024, 6, 30)) + .bossName("홍사장") + .storeAddress("서울시 마포구") + .duty("서빙 및 매장 관리") + .workSchedules(List.of(schedule)) + .hourlyWage(10000) + .businessNumber("123-45-67890") + .staffPhone("010-1234-5678") + .build(); + + // when + List result = contractService.extractRegularGroupsFromContract(data, staff, endDate); + + // then + assertThat(result).hasSize(1); + RegularGroupEntity group = result.get(0); + assertThat(group.getDayOfWeek()).isEqualTo(DayOfWeek.MONDAY); + assertThat(group.getStartTime()).isEqualTo(LocalTime.of(9, 0)); + assertThat(group.getEndTime()).isEqualTo(LocalTime.of(18, 0)); + assertThat(group.getStartDate()).isEqualTo(startDate); + assertThat(group.getEndDate()).isEqualTo(endDate); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractTemplateServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractTemplateServiceTest.java new file mode 100644 index 00000000..dcaa6d26 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/contract/ContractTemplateServiceTest.java @@ -0,0 +1,87 @@ +package com.mangoboss.app.domain.service.contract; + +import com.mangoboss.app.domain.repository.ContractTemplateRepository; +import com.mangoboss.app.dto.contract.request.ContractTemplateData; +import com.mangoboss.storage.contract.ContractTemplate; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ContractTemplateServiceTest { + + @Mock + private ContractTemplateRepository contractTemplateRepository; + + @InjectMocks + private ContractTemplateService contractTemplateService; + + @Test + void 템플릿을_생성할_수_있다() { + // given + ContractTemplate template = mock(ContractTemplate.class); + when(contractTemplateRepository.save(template)).thenReturn(template); + + // when + ContractTemplate result = contractTemplateService.createContractTemplate(template); + + // then + assertThat(result).isEqualTo(template); + } + + @Test + void 템플릿을_조회할_수_있다() { + // given + Long storeId = 1L; + List templates = List.of(mock(ContractTemplate.class)); + when(contractTemplateRepository.findAllByStoreId(storeId)).thenReturn(templates); + + // when + List result = contractTemplateService.getAllContractTemplates(storeId); + + // then + assertThat(result).hasSize(1); + } + + @Test + void 템플릿을_업데이트할_수_있다() { + // given + Long storeId = 1L; + Long templateId = 1L; + String title = "updated-title"; + ContractTemplateData contractTemplateData = mock(ContractTemplateData.class); + + ContractTemplate template = mock(ContractTemplate.class); + when(contractTemplateRepository.getByIdAndStoreId(templateId, storeId)).thenReturn(template); + when(template.update(eq(title), any())).thenReturn(template); + + // when + ContractTemplate updated = contractTemplateService.updateContractTemplate(storeId, templateId, title, contractTemplateData); + + // then + assertThat(updated).isEqualTo(template); + } + + @Test + void 템플릿을_삭제할_수_있다() { + // given + Long storeId = 1L; + Long templateId = 10L; + ContractTemplate template = mock(ContractTemplate.class); + + when(contractTemplateRepository.getByIdAndStoreId(templateId, storeId)).thenReturn(template); + + // when + contractTemplateService.deleteContractTemplate(storeId, templateId); + + // then + verify(contractTemplateRepository).delete(template); + } +} diff --git a/app/src/test/java/com/mangoboss/app/domain/service/document/DocumentServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/document/DocumentServiceTest.java new file mode 100644 index 00000000..2ca75589 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/document/DocumentServiceTest.java @@ -0,0 +1,210 @@ +package com.mangoboss.app.domain.service.document; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.common.security.EncryptedFileDecoder; +import com.mangoboss.app.common.util.S3FileManager; +import com.mangoboss.app.domain.repository.DocumentRepository; +import com.mangoboss.storage.document.DocumentEntity; +import com.mangoboss.storage.document.DocumentType; +import com.mangoboss.storage.metadata.S3FileType; +import org.assertj.core.api.Assertions; +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.time.LocalDate; +import java.util.List; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DocumentServiceTest { + + @Mock + private DocumentRepository documentRepository; + + @Mock + private S3FileManager s3FileManager; + + @Mock + private EncryptedFileDecoder encryptedFileDecoder; + + @InjectMocks + private DocumentService documentService; + + @Test + void 문서를_업로드_할_수_있다() { + // given + String documentData = "encrypted-data"; + DocumentType documentType = DocumentType.ID_CARD; + LocalDate expiresAt = LocalDate.now().plusDays(30); + Long storeId = 1L; + Long staffId = 1L; + + EncryptedFileDecoder.DecodedFile decodedFile = new EncryptedFileDecoder.DecodedFile("application/pdf", new byte[]{1, 2, 3}); + when(encryptedFileDecoder.decode(documentData)).thenReturn(decodedFile); + + when(s3FileManager.generateFileKey(S3FileType.DOCUMENT, "pdf")) + .thenReturn("test-file-key"); + + DocumentEntity savedDocument = DocumentEntity.create(staffId, storeId, documentType, "test-file-key", "application/pdf", expiresAt); + when(documentRepository.save(any(DocumentEntity.class))).thenReturn(savedDocument); + + // when + documentService.uploadDocument(documentData, documentType, expiresAt, storeId, staffId); + + // then + verify(s3FileManager, times(1)).upload(any(), anyString(), eq("application/pdf")); + verify(documentRepository, times(1)).save(any(DocumentEntity.class)); + } + + @Test + void 이미_업로드된_문서면_에러를_던진다() { + // given + Long staffId = 1L; + DocumentType documentType = DocumentType.ID_CARD; + + when(documentRepository.existsByStaffIdAndDocumentType(staffId, documentType)).thenReturn(true); + + // when & then + Assertions.assertThatThrownBy(() -> documentService.validateNotAlreadyUploaded(staffId, documentType)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.DOCUMENT_ALREADY_UPLOADED.getMessage()); + } + + @Test + void 본인_문서가_아닐_경우_에러를_던진다() { + // given + Long documentStaffId = 1L; + Long staffId = 2L; + + // when & then + Assertions.assertThatThrownBy(() -> documentService.validateDocumentBelongsToStaff(documentStaffId, staffId)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.DOCUMENT_NOT_BELONG_TO_STAFF.getMessage()); + } + + @Test + void 문서를_삭제할_수_있다() { + // given + DocumentEntity document = mock(DocumentEntity.class); + when(document.getFileKey()).thenReturn("file-key"); + + // when + documentService.deleteDocument(document); + + // then + verify(s3FileManager).deleteFileFromPrivateBucket("file-key"); + verify(documentRepository).delete(document); + } + + @Test + void 매장과_알바생ID로_문서목록을_조회할_수_있다() { + // given + Long storeId = 1L, staffId = 2L; + List documents = List.of(mock(DocumentEntity.class)); + when(documentRepository.findAllByStoreIdAndStaffId(storeId, staffId)).thenReturn(documents); + + // when + List result = documentService.findAllByStoreIdAndStaffId(storeId, staffId); + + // then + Assertions.assertThat(result).isEqualTo(documents); + } + + @Test + void 매장과_문서타입으로_문서목록을_조회할_수_있다() { + // given + Long storeId = 1L; + DocumentType type = DocumentType.ID_CARD; + List documents = List.of(mock(DocumentEntity.class)); + when(documentRepository.findAllByStoreIdAndDocumentType(storeId, type)).thenReturn(documents); + + // when + List result = documentService.findAllByStoreIdAndType(storeId, type); + + // then + Assertions.assertThat(result).isEqualTo(documents); + } + + @Test + void 문서ID로_단일_문서를_조회할_수_있다() { + // given + Long documentId = 1L; + DocumentEntity document = mock(DocumentEntity.class); + when(documentRepository.getById(documentId)).thenReturn(document); + + // when + DocumentEntity result = documentService.getByDocumentId(documentId); + + // then + Assertions.assertThat(result).isEqualTo(document); + } + + @Test + void 특정_알바생의_문서로부터_파일키를_조회할_수_있다() { + // given + Long staffId = 1L; + Long documentId = 2L; + DocumentEntity document = mock(DocumentEntity.class); + when(document.getFileKey()).thenReturn("s3-key"); + when(documentRepository.getByIdAndStaffId(documentId, staffId)).thenReturn(document); + + // when + String result = documentService.getFileKeyByIdAndStaffId(staffId, documentId); + + // then + Assertions.assertThat(result).isEqualTo("s3-key"); + } + + @Test + void 아직_업로드되지_않은_문서는_예외없이_통과한다() { + // given + Long staffId = 1L; + DocumentType documentType = DocumentType.ID_CARD; + + when(documentRepository.existsByStaffIdAndDocumentType(staffId, documentType)).thenReturn(false); + + // when & then + Assertions.assertThatCode(() -> + documentService.validateNotAlreadyUploaded(staffId, documentType) + ).doesNotThrowAnyException(); + } + + @Test + void 문서가_본인_문서이면_예외없이_통과한다() { + // given + Long staffId = 1L; + Long documentStaffId = 1L; + + // when & then + Assertions.assertThatCode(() -> + documentService.validateDocumentBelongsToStaff(documentStaffId, staffId) + ).doesNotThrowAnyException(); + } + + @Test + void 지원하지_않는_파일형식이면_에러를_던진다() { + // given + String documentData = "invalid"; + DocumentType documentType = DocumentType.ID_CARD; + LocalDate expiresAt = LocalDate.now(); + Long storeId = 1L; + Long staffId = 1L; + + EncryptedFileDecoder.DecodedFile decodedFile = + new EncryptedFileDecoder.DecodedFile("application/unknown", new byte[]{1, 2, 3}); + when(encryptedFileDecoder.decode(documentData)).thenReturn(decodedFile); + + // when & then + Assertions.assertThatThrownBy(() -> + documentService.uploadDocument(documentData, documentType, expiresAt, storeId, staffId) + ).isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.UNSUPPORTED_FILE_TYPE.getMessage()); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/document/RequiredDocumentServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/document/RequiredDocumentServiceTest.java new file mode 100644 index 00000000..f7743e29 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/document/RequiredDocumentServiceTest.java @@ -0,0 +1,71 @@ +package com.mangoboss.app.domain.service.document; + +import com.mangoboss.app.domain.repository.RequiredDocumentRepository; +import com.mangoboss.app.dto.document.request.RequiredDocumentCreateRequest; +import com.mangoboss.storage.document.DocumentType; +import com.mangoboss.storage.document.RequiredDocumentEntity; +import com.mangoboss.storage.store.StoreEntity; +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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RequiredDocumentServiceTest { + + @Mock + private RequiredDocumentRepository requiredDocumentRepository; + + @InjectMocks + private RequiredDocumentService requiredDocumentService; + + @Test + void 필수_서류_상태를_업데이트할_수_있다() { + // given + StoreEntity store = mock(StoreEntity.class); + DocumentType type = DocumentType.BANK_ACCOUNT; + RequiredDocumentCreateRequest request = new RequiredDocumentCreateRequest(type, true); + + RequiredDocumentEntity entity = mock(RequiredDocumentEntity.class); + when(requiredDocumentRepository.getByStoreAndDocumentType(store, type)).thenReturn(entity); + + // when + requiredDocumentService.updateRequiredDocuments(store, List.of(request)); + + // then + verify(entity).updateRequiredStatus(true); + verify(requiredDocumentRepository).save(entity); + } + + @Test + void 가게ID로_모든_필수서류를_조회할_수_있다() { + // given + Long storeId = 1L; + List expectedList = List.of(mock(RequiredDocumentEntity.class)); + when(requiredDocumentRepository.findAllByStoreId(storeId)).thenReturn(expectedList); + + // when + List result = requiredDocumentService.findAllByStoreId(storeId); + + // then + assertEquals(expectedList, result); + } + + @Test + void 모든_DocumentType에_대해_초기화된_필수서류를_저장할_수_있다() { + // given + StoreEntity store = mock(StoreEntity.class); + + // when + requiredDocumentService.initRequiredDocuments(store); + + // then + verify(requiredDocumentRepository, times(DocumentType.values().length)).save(any()); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/faq/FaqServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/faq/FaqServiceTest.java new file mode 100644 index 00000000..ea42d40f --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/faq/FaqServiceTest.java @@ -0,0 +1,85 @@ +package com.mangoboss.app.domain.service.faq; + +import com.mangoboss.app.domain.repository.FaqRepository; +import com.mangoboss.storage.faq.FaqCategory; +import com.mangoboss.storage.faq.FaqEntity; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FaqServiceTest { + + @InjectMocks + private FaqService faqService; + + @Mock + private FaqRepository faqRepository; + + @Test + void 전체_카테고리를_조회할_때_모든_FAQ를_가져온다() { + // given + List allFaqs = List.of(mock(FaqEntity.class), mock(FaqEntity.class)); + when(faqRepository.findAll()).thenReturn(allFaqs); + + // when + List result = faqService.getFaqs(FaqCategory.ALL); + + // then + assertThat(result).isEqualTo(allFaqs); + verify(faqRepository).findAll(); + verify(faqRepository, never()).findByCategory(any()); + } + + @Test + void 서비스_카테고리를_조회할_때_SERVICE_FAQ만_가져온다() { + // given + List serviceFaqs = List.of(mock(FaqEntity.class)); + when(faqRepository.findByCategory(FaqCategory.SERVICE)).thenReturn(serviceFaqs); + + // when + List result = faqService.getFaqs(FaqCategory.SERVICE); + + // then + assertThat(result).isEqualTo(serviceFaqs); + verify(faqRepository).findByCategory(FaqCategory.SERVICE); + verify(faqRepository, never()).findAll(); + } + + @Test + void 결제_카테고리를_조회할_때_PAYMENT_FAQ만_가져온다() { + // given + List paymentFaqs = List.of(mock(FaqEntity.class)); + when(faqRepository.findByCategory(FaqCategory.PAYMENT)).thenReturn(paymentFaqs); + + // when + List result = faqService.getFaqs(FaqCategory.PAYMENT); + + // then + assertThat(result).isEqualTo(paymentFaqs); + verify(faqRepository).findByCategory(FaqCategory.PAYMENT); + verify(faqRepository, never()).findAll(); + } + + @Test + void 계정_카테고리를_조회할_때_ACCOUNT_FAQ만_가져온다() { + // given + List accountFaqs = List.of(mock(FaqEntity.class)); + when(faqRepository.findByCategory(FaqCategory.ACCOUNT)).thenReturn(accountFaqs); + + // when + List result = faqService.getFaqs(FaqCategory.ACCOUNT); + + // then + assertThat(result).isEqualTo(accountFaqs); + verify(faqRepository).findByCategory(FaqCategory.ACCOUNT); + verify(faqRepository, never()).findAll(); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/notification/DeviceTokenServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/notification/DeviceTokenServiceTest.java new file mode 100644 index 00000000..b396ef28 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/notification/DeviceTokenServiceTest.java @@ -0,0 +1,45 @@ +package com.mangoboss.app.domain.service.notification; + +import com.mangoboss.app.domain.repository.DeviceTokenRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class DeviceTokenServiceTest { + + @InjectMocks + private DeviceTokenService deviceTokenService; + + @Mock + private DeviceTokenRepository deviceTokenRepository; + + @Test + void 이미존재하는_토큰이면_업데이트만() { + Long userId = 1L; + String token = "token-123"; + + when(deviceTokenRepository.existsByUserIdAndTokenValue(userId, token)).thenReturn(true); + + deviceTokenService.registerDeviceToken(userId, token); + + verify(deviceTokenRepository).updateIsDeletedFalseAndModifiedAt(userId, token); + verify(deviceTokenRepository, never()).save(any(), any()); + } + + @Test + void 존재하지않는_토큰이면_새로저장() { + Long userId = 2L; + String token = "token-456"; + + when(deviceTokenRepository.existsByUserIdAndTokenValue(userId, token)).thenReturn(false); + + deviceTokenService.registerDeviceToken(userId, token); + + verify(deviceTokenRepository).save(userId, token); + verify(deviceTokenRepository, never()).updateIsDeletedFalseAndModifiedAt(any(), any()); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/notification/NotificationServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/notification/NotificationServiceTest.java new file mode 100644 index 00000000..368964ae --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/notification/NotificationServiceTest.java @@ -0,0 +1,187 @@ +package com.mangoboss.app.domain.service.notification; + +import com.mangoboss.app.domain.repository.DeviceTokenRepository; +import com.mangoboss.app.domain.repository.NotificationRepository; +import com.mangoboss.storage.attendance.AttendanceEditEntity; +import com.mangoboss.storage.notification.NotificationEntity; +import com.mangoboss.storage.notification.SendStatus; +import com.mangoboss.storage.schedule.SubstituteRequestEntity; +import com.mangoboss.storage.notification.NotificationType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class NotificationServiceTest { + + @InjectMocks + private NotificationService notificationService; + + @Mock + private NotificationRepository notificationRepository; + + @Mock + private DeviceTokenRepository deviceTokenRepository; + + @BeforeEach + void setup() { + notificationService = new NotificationService(notificationRepository, deviceTokenRepository); + // frontendUrl 직접 세팅 + notificationService.getClass().getDeclaredFields(); + // reflection 방식으로 @Value 설정 우회 + try { + var field = NotificationService.class.getDeclaredField("frontendUrl"); + field.setAccessible(true); + field.set(notificationService, "https://frontend.com"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + void 근로계약서알림_토큰없으면_토큰없이저장() { + when(deviceTokenRepository.findActiveTokensByUserId(1L)).thenReturn(List.of()); + + notificationService.saveContractSignNotification(1L, 10L); + + verify(notificationRepository).save(argThat(notif -> + notif.getUserId().equals(1L) && + notif.getTargetToken() == null && // ✅ 수정된 부분 + notif.getType() == NotificationType.CONTRACT && + notif.getSendStatus() == SendStatus.PENDING && + notif.getRetryCount() == 0 + )); + } + + @Test + void 근로계약서알림_토큰있으면_모든토큰에저장() { + when(deviceTokenRepository.findActiveTokensByUserId(1L)).thenReturn(List.of("t1", "t2")); + + notificationService.saveContractSignNotification(1L, 10L); + + verify(notificationRepository, times(2)).save(any(NotificationEntity.class)); + } + + @Test + void 대타승인알림_양쪽저장() { + SubstituteRequestEntity request = mock(SubstituteRequestEntity.class); + when(request.getRequesterStaffId()).thenReturn(1L); + when(request.getTargetStaffId()).thenReturn(2L); + when(request.getStoreId()).thenReturn(10L); + when(request.getRequesterStaffName()).thenReturn("홍길동"); + when(request.getTargetStaffName()).thenReturn("김영희"); + + when(deviceTokenRepository.findActiveTokensByUserId(anyLong())).thenReturn(List.of()); + + notificationService.saveSubstituteApproveNotificationForBoth(request); + + verify(notificationRepository, times(2)).save(any(NotificationEntity.class)); + } + + @Test + void 근태요청알림_사장에게보내기() { + when(deviceTokenRepository.findActiveTokensByUserId(anyLong())).thenReturn(List.of("t1")); + + notificationService.saveAttendanceEditRequestNotification(100L, 5L, "김철수"); + + verify(notificationRepository).save(argThat(notif -> + notif.getTitle().contains("근태기록") && + notif.getUserId().equals(100L) + )); + } + + @Test + void 보고사항알림_사장에게보내기() { + when(deviceTokenRepository.findActiveTokensByUserId(anyLong())).thenReturn(List.of()); + + notificationService.saveWorkReportNotificationToBoss(200L, 1L, "이몽룡"); + + verify(notificationRepository).save(argThat(notif -> + notif.getType() == NotificationType.WORK_REPORT && + notif.getUserId().equals(200L) + )); + } + + @Test + void 대타요청알림_정상저장된다() { + when(deviceTokenRepository.findActiveTokensByUserId(anyLong())).thenReturn(List.of()); + + notificationService.saveSubstituteRequestNotification(1L, 10L, "김철수"); + + verify(notificationRepository).save(argThat(notif -> + notif.getUserId().equals(1L) && + notif.getStoreId().equals(10L) && + notif.getTitle().contains("대타 요청") && + notif.getContent().contains("김철수") + )); + } + + @Test + void 대타거절알림_요청자_대상자_모두저장된다() { + SubstituteRequestEntity request = mock(SubstituteRequestEntity.class); + when(request.getRequesterStaffId()).thenReturn(1L); + when(request.getTargetStaffId()).thenReturn(2L); + when(request.getStoreId()).thenReturn(10L); + when(request.getRequesterStaffName()).thenReturn("홍길동"); + when(request.getTargetStaffName()).thenReturn("김영희"); + + when(deviceTokenRepository.findActiveTokensByUserId(anyLong())).thenReturn(List.of()); + + notificationService.saveSubstituteRejectNotificationForBoth(request); + + verify(notificationRepository, times(2)).save(argThat(notif -> + notif.getUserId().equals(1L) || notif.getUserId().equals(2L) + )); + } + + @Test + void 근태승인알림_정상저장된다() { + AttendanceEditEntity edit = mock(AttendanceEditEntity.class); + when(edit.getStaffId()).thenReturn(1L); + when(edit.getStoreId()).thenReturn(10L); + + when(deviceTokenRepository.findActiveTokensByUserId(anyLong())).thenReturn(List.of()); + + notificationService.saveAttendanceEditApproveNotification(edit); + + verify(notificationRepository).save(argThat(notif -> + notif.getUserId().equals(1L) && + notif.getTitle().contains("승인") && + notif.getType() == NotificationType.SUBSTITUTE + )); + } + + @Test + void 근태거절알림_정상저장된다() { + AttendanceEditEntity edit = mock(AttendanceEditEntity.class); + when(edit.getStaffId()).thenReturn(1L); + when(edit.getStoreId()).thenReturn(10L); + + when(deviceTokenRepository.findActiveTokensByUserId(anyLong())).thenReturn(List.of("t1")); + + notificationService.saveAttendanceEditRejectNotification(edit); + + verify(notificationRepository).save(argThat(notif -> + notif.getUserId().equals(1L) && + notif.getTitle().contains("거절") && + notif.getType() == NotificationType.SUBSTITUTE + )); + } + + @Test + void 유저와매장으로_알림목록을_조회할_수_있다() { + when(notificationRepository.findByUserIdAndStoreIdOrderByCreatedAtDesc(1L, 10L)) + .thenReturn(List.of(mock(NotificationEntity.class))); + + List result = notificationService.getNotificationsByUserAndStore(1L, 10L); + + verify(notificationRepository).findByUserIdAndStoreIdOrderByCreatedAtDesc(1L, 10L); + assertThat(result).hasSize(1); + } +} diff --git a/app/src/test/java/com/mangoboss/app/domain/service/subscription/SubscriptionServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/subscription/SubscriptionServiceTest.java new file mode 100644 index 00000000..3676c004 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/subscription/SubscriptionServiceTest.java @@ -0,0 +1,143 @@ +package com.mangoboss.app.domain.service.subscription; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.domain.repository.SubscriptionOrderRepository; +import com.mangoboss.app.domain.repository.SubscriptionRepository; +import com.mangoboss.app.dto.subscription.response.SubscriptionOrderResponse; +import com.mangoboss.storage.subscription.PlanType; +import com.mangoboss.storage.subscription.SubscriptionEntity; +import com.mangoboss.storage.subscription.SubscriptionOrderEntity; +import com.mangoboss.storage.user.UserEntity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +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 SubscriptionServiceTest { + + @InjectMocks + private SubscriptionService subscriptionService; + + @Mock + private SubscriptionRepository subscriptionRepository; + + @Mock + private SubscriptionOrderRepository subscriptionOrderRepository; + + @Test + void 구독이_있으면_삭제하고_새로_생성한다() { + // given + UserEntity boss = mock(UserEntity.class); + when(boss.getId()).thenReturn(1L); + SubscriptionEntity oldSubscription = mock(SubscriptionEntity.class); + when(subscriptionRepository.findByBossId(1L)).thenReturn(Optional.of(oldSubscription)); + + // when + subscriptionService.createOrReplaceSubscription(boss, PlanType.PREMIUM); + + // then + verify(subscriptionRepository).delete(oldSubscription); + verify(subscriptionRepository).save(any(SubscriptionEntity.class)); + verify(boss).addSubscription(any(SubscriptionEntity.class)); + } + + @Test + void 구독이_없으면_삭제없이_바로_생성된다() { + // given + UserEntity boss = mock(UserEntity.class); + when(boss.getId()).thenReturn(2L); + when(subscriptionRepository.findByBossId(2L)).thenReturn(Optional.empty()); + + // when + subscriptionService.createOrReplaceSubscription(boss, PlanType.PREMIUM); + + // then + verify(subscriptionRepository, never()).delete(any()); + verify(subscriptionRepository).save(any(SubscriptionEntity.class)); + verify(boss).addSubscription(any(SubscriptionEntity.class)); + } + + @Test + void 구독이_있으면_반환한다() { + // given + SubscriptionEntity subscription = mock(SubscriptionEntity.class); + when(subscriptionRepository.findByBossId(1L)).thenReturn(Optional.of(subscription)); + + // when + SubscriptionEntity result = subscriptionService.getSubscription(1L); + + // then + assertThat(result).isEqualTo(subscription); + } + + @Test + void 구독이_없으면_null을_반환한다() { + // given + when(subscriptionRepository.findByBossId(1L)).thenReturn(Optional.empty()); + + // when + SubscriptionEntity result = subscriptionService.getSubscription(1L); + + // then + assertThat(result).isNull(); + } + + @Test + void 구독삭제시_존재하지_않으면_예외를_던진다() { + // given + when(subscriptionRepository.findByBossId(1L)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> subscriptionService.deleteSubscription(1L)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.SUBSCRIPTION_NOT_FOUND.getMessage()); + } + + @Test + void 구독삭제시_존재하면_삭제한다() { + // given + SubscriptionEntity subscription = mock(SubscriptionEntity.class); + when(subscriptionRepository.findByBossId(1L)).thenReturn(Optional.of(subscription)); + + // when + subscriptionService.deleteSubscription(1L); + + // then + verify(subscriptionRepository).delete(subscription); + } + + @Test + void 주문내역을_반환한다() { + // given + SubscriptionOrderEntity order1 = mock(SubscriptionOrderEntity.class); + SubscriptionOrderEntity order2 = mock(SubscriptionOrderEntity.class); + when(subscriptionOrderRepository.findByBossIdOrderByCreatedAtDesc(1L)) + .thenReturn(List.of(order1, order2)); + + SubscriptionOrderResponse response1 = mock(SubscriptionOrderResponse.class); + SubscriptionOrderResponse response2 = mock(SubscriptionOrderResponse.class); + + try (MockedStatic mockStatic = mockStatic(SubscriptionOrderResponse.class)) { + mockStatic.when(() -> SubscriptionOrderResponse.fromEntity(order1)).thenReturn(response1); + mockStatic.when(() -> SubscriptionOrderResponse.fromEntity(order2)).thenReturn(response2); + + // when + List result = subscriptionService.getOrderHistory(1L); + + // then + assertThat(result).containsExactlyInAnyOrder(response1, response2); + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/task/TaskServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/task/TaskServiceTest.java new file mode 100644 index 00000000..d6fa9a3e --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/task/TaskServiceTest.java @@ -0,0 +1,307 @@ +package com.mangoboss.app.domain.service.task; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.common.util.S3FileManager; +import com.mangoboss.app.domain.repository.TaskLogRepository; +import com.mangoboss.app.domain.repository.TaskRepository; +import com.mangoboss.app.domain.repository.TaskRoutineRepository; +import com.mangoboss.storage.task.TaskEntity; +import com.mangoboss.storage.task.TaskLogEntity; +import com.mangoboss.storage.task.TaskRoutineEntity; +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.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class TaskServiceTest { + + @InjectMocks + private TaskService taskService; + + @Mock + private TaskRoutineRepository taskRoutineRepository; + + @Mock + private TaskRepository taskRepository; + + @Mock + private TaskLogRepository taskLogRepository; + + @Mock + private S3FileManager s3FileManager; + + @Test + void 루틴과_태스크를_함께_저장할_수_있다() { + //given + final TaskRoutineEntity routine = mock(TaskRoutineEntity.class); + final List tasks = List.of(mock(TaskEntity.class), mock(TaskEntity.class)); + + //when + taskService.saveTaskRoutineWithTasks(routine, tasks); + + //then + verify(taskRoutineRepository).save(routine); + verify(taskRepository).saveAll(tasks); + } + + @Test + void 태스크를_정상적으로_완료할_수_있다() { + //given + final Long storeId = 1L, taskId = 2L, staffId = 3L; + final String imageUrl = "http://s3.com/image.jpg"; + final TaskEntity task = mock(TaskEntity.class); + given(task.getId()).willReturn(taskId); + + given(taskRepository.getTaskByIdAndStoreId(taskId, storeId)).willReturn(task); + given(taskLogRepository.findByTaskIdAndStaffId(taskId, staffId)).willReturn(Optional.empty()); + + //when + taskService.completeTask(storeId, taskId, staffId, imageUrl); + + //then + verify(taskLogRepository).save(any(TaskLogEntity.class)); + } + + @Test + void 이미_완료된_태스크라면_예외를_던진다() { + //given + final Long taskId = 1L, staffId = 2L; + given(taskLogRepository.findByTaskIdAndStaffId(taskId, staffId)) + .willReturn(Optional.of(mock(TaskLogEntity.class))); + + //when & then + assertThatThrownBy(() -> taskService.validateNotAlreadyCompleted(taskId, staffId)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.ALREADY_COMPLETED_TASK.getMessage()); + } + + @Test + void 이미지가_필수인_태스크에서_이미지가_없으면_예외를_던진다() { + //given + final TaskEntity task = mock(TaskEntity.class); + given(task.isPhotoRequired()).willReturn(true); + + //when & then + assertThatThrownBy(() -> taskService.validateTaskLogImageRequirement(task, null)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.TASK_LOG_IMAGE_REQUIRED.getMessage()); + } + + @Test + void 태스크_로그를_삭제할_수_있다() { + //given + final Long storeId = 1L, taskId = 2L, staffId = 3L; + final TaskEntity task = mock(TaskEntity.class); + final TaskLogEntity log = mock(TaskLogEntity.class); + + given(taskRepository.getTaskByIdAndStoreId(taskId, storeId)).willReturn(task); + given(taskLogRepository.getTaskLogByTaskIdAndStaffId(task.getId(), staffId)).willReturn(log); + given(log.getTaskLogImageUrl()).willReturn("http://s3.com/image.jpg"); + given(s3FileManager.extractKeyFromPublicUrl(any())).willReturn("image.jpg"); + + //when + taskService.deleteTaskLog(storeId, taskId, staffId); + + //then + verify(s3FileManager).deleteFileFromPublicBucket("image.jpg"); + verify(taskLogRepository).delete(log); + } + + @Test + void 루틴삭제시_ALL옵션이라면_전체태스크와_루틴이_삭제된다() { + //given + final Long storeId = 1L, routineId = 2L; + final TaskRoutineEntity routine = mock(TaskRoutineEntity.class); + given(taskRoutineRepository.getByIdAndStoreId(routineId, storeId)).willReturn(routine); + + //when + taskService.deleteTaskRoutine(storeId, routineId, "ALL"); + + //then + verify(taskRepository).deleteAllByTaskRoutineId(routineId); + verify(taskRoutineRepository).delete(routine); + } + + @Test + void 루틴삭제시_PENDING옵션이라면_미완료태스크만_삭제된다() { + //given + final Long storeId = 1L, routineId = 2L; + final TaskRoutineEntity routine = mock(TaskRoutineEntity.class); + given(taskRoutineRepository.getByIdAndStoreId(routineId, storeId)).willReturn(routine); + + //when + taskService.deleteTaskRoutine(storeId, routineId, "PENDING"); + + //then + verify(taskRepository).deleteAllByTaskRoutineIdAndNotCompleted(routineId); + verify(taskRepository).deleteRoutineReferenceForCompletedTasks(routineId); + verify(taskRoutineRepository).delete(routine); + } + + @Test + void 업무와_업무보고를_삭제할_수_있다() { + // given + final Long storeId = 1L; + final Long taskId = 2L; + final TaskEntity task = mock(TaskEntity.class); + final TaskLogEntity log = mock(TaskLogEntity.class); + + given(taskRepository.getTaskByIdAndStoreId(taskId, storeId)).willReturn(task); + given(taskLogRepository.findTaskLogByTaskId(taskId)).willReturn(Optional.of(log)); + + // when + taskService.deleteSingleTask(storeId, taskId); + + // then + verify(taskLogRepository).delete(log); + verify(taskRepository).delete(task); + } + + @Test + void 특정_날짜에_해당하는_업무들을_조회할_수_있다() { + // given + final Long storeId = 1L; + final LocalDate date = LocalDate.now(); + final List tasks = List.of(mock(TaskEntity.class)); + + given(taskRepository.findByStoreIdAndTaskDate(storeId, date)).willReturn(tasks); + + // when + List result = taskService.getTasksByDate(storeId, date); + + // then + assertThat(result).isEqualTo(tasks); + } + + @Test + void 단일_업무를_저장할_수_있다() { + // given + final TaskEntity task = mock(TaskEntity.class); + + // when + taskService.saveSingleTask(task); + + // then + verify(taskRepository).save(task); + } + + @Test + void 업무_리스트로_업무보고를_조회할_수_있다() { + // given + final List taskIds = List.of(1L, 2L); + final List logs = List.of(mock(TaskLogEntity.class)); + + given(taskLogRepository.findByTaskIds(taskIds)).willReturn(logs); + + // when + List result = taskService.getTaskLogsByTaskIds(taskIds); + + // then + assertThat(result).isEqualTo(logs); + } + + @Test + void 매장으로_루틴을_조회할_수_있다() { + // given + final Long storeId = 1L; + final List routines = List.of(mock(TaskRoutineEntity.class)); + + given(taskRoutineRepository.findAllByStoreId(storeId)).willReturn(routines); + + // when + List result = taskService.getTaskRoutinesByStoreId(storeId); + + // then + assertThat(result).isEqualTo(routines); + } + + @Test + void 특정_태스크를_상세조회할_수_있다() { + // given + final Long taskId = 1L; + final TaskEntity task = mock(TaskEntity.class); + + given(taskRepository.getTaskById(taskId)).willReturn(task); + + // when + TaskEntity result = taskService.getTaskById(taskId); + + // then + assertThat(result).isEqualTo(task); + } + + @Test + void 특정_태스크로그를_상세조회할_수_있다() { + // given + final Long taskId = 1L; + final TaskLogEntity log = mock(TaskLogEntity.class); + given(taskLogRepository.findTaskLogByTaskId(taskId)).willReturn(Optional.of(log)); + + // when + Optional result = taskService.findTaskLogByTaskId(taskId); + + // then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(log); + } + + @Test + void 이미지가_없으면_s3삭제를_수행하지_않는다() { + // given + final Long storeId = 1L, taskId = 2L, staffId = 3L; + final TaskEntity task = mock(TaskEntity.class); + final TaskLogEntity log = mock(TaskLogEntity.class); + + given(task.getId()).willReturn(taskId); + given(taskRepository.getTaskByIdAndStoreId(taskId, storeId)).willReturn(task); + given(taskLogRepository.getTaskLogByTaskIdAndStaffId(taskId, staffId)).willReturn(log); + given(log.getTaskLogImageUrl()).willReturn(""); + + // when + taskService.deleteTaskLog(storeId, taskId, staffId); + + // then + verify(s3FileManager, org.mockito.Mockito.never()).deleteFileFromPublicBucket(any()); + verify(taskLogRepository).delete(log); + } + + @Test + void 이미지가_공백일때_예외를_던진다() { + // given + final TaskEntity task = mock(TaskEntity.class); + given(task.isPhotoRequired()).willReturn(true); + + // when & then + assertThatThrownBy(() -> taskService.validateTaskLogImageRequirement(task, " ")) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.TASK_LOG_IMAGE_REQUIRED.getMessage()); + } + + @Test + void 업무루틴삭제시_잘못된_옵션값이면_예외를_던진다() { + // given + final Long storeId = 1L; + final Long taskRoutineId = 2L; + final String invalidOption = "INVALID"; + + // when & then + assertThatThrownBy(() -> + taskService.deleteTaskRoutine(storeId, taskRoutineId, invalidOption) + ).isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.INVALID_DELETE_OPTION.getMessage()); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/task/context/TaskRoutineStrategyContextTest.java b/app/src/test/java/com/mangoboss/app/domain/service/task/context/TaskRoutineStrategyContextTest.java new file mode 100644 index 00000000..1f52451d --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/task/context/TaskRoutineStrategyContextTest.java @@ -0,0 +1,45 @@ +package com.mangoboss.app.domain.service.task.context; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.domain.service.task.strategy.TaskRoutineGenerationStrategy; +import com.mangoboss.storage.task.TaskRoutineRepeatType; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +class TaskRoutineStrategyContextTest { + + @Test + void 주어진_타입에_해당하는_전략을_찾을_수_있다() { + // given + TaskRoutineGenerationStrategy dailyStrategy = mock(TaskRoutineGenerationStrategy.class); + when(dailyStrategy.getType()).thenReturn(TaskRoutineRepeatType.DAILY); + + TaskRoutineStrategyContext context = new TaskRoutineStrategyContext(List.of(dailyStrategy)); + + // when + TaskRoutineGenerationStrategy result = context.findStrategy(TaskRoutineRepeatType.DAILY); + + // then + assertThat(result).isEqualTo(dailyStrategy); + } + + @Test + void 해당_타입의_전략이_없으면_예외가_발생한다() { + // given + TaskRoutineGenerationStrategy weeklyStrategy = mock(TaskRoutineGenerationStrategy.class); + when(weeklyStrategy.getType()).thenReturn(TaskRoutineRepeatType.WEEKLY); + + TaskRoutineStrategyContext context = new TaskRoutineStrategyContext(List.of(weeklyStrategy)); + + // when & then + assertThatThrownBy(() -> context.findStrategy(TaskRoutineRepeatType.MONTHLY)) + .isInstanceOf(CustomException.class) + .hasMessage(CustomErrorInfo.TASK_ROUTINE_STRATEGY_NOT_FOUND.getMessage()); + } +} diff --git a/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/DailyTaskRoutineGenerationStrategyTest.java b/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/DailyTaskRoutineGenerationStrategyTest.java new file mode 100644 index 00000000..1e137750 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/DailyTaskRoutineGenerationStrategyTest.java @@ -0,0 +1,74 @@ +package com.mangoboss.app.domain.service.task.strategy; + +import com.mangoboss.app.dto.task.request.TaskRoutineCreateRequest; +import com.mangoboss.storage.task.TaskEntity; +import com.mangoboss.storage.task.TaskRoutineEntity; +import com.mangoboss.storage.task.TaskRoutineRepeatType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class DailyTaskRoutineGenerationStrategyTest { + + private final DailyTaskRoutineGenerationStrategy strategy = new DailyTaskRoutineGenerationStrategy(); + + @Test + void DAILY_전략은_타입이_DAILY이다() { + assertThat(strategy.getType()).isEqualTo(TaskRoutineRepeatType.DAILY); + } + + @Test + void 일일_루틴_생성시_반복규칙없이_정상_생성된다() { + // given + TaskRoutineCreateRequest request = new TaskRoutineCreateRequest( + TaskRoutineRepeatType.DAILY, + "매일 청소", + "매일매일 해야지", + LocalDate.of(2025, 6, 1), + LocalDate.of(2025, 6, 3), + LocalTime.of(9, 0), + LocalTime.of(18, 0), + false, + null, + null + ); + + + // when + TaskRoutineEntity result = strategy.generateTaskRoutine(request, 1L); + + // then + assertThat(result.getRepeatType()).isEqualTo(TaskRoutineRepeatType.DAILY); + assertThat(result.getStartDate()).isEqualTo(LocalDate.of(2025, 6, 1)); + assertThat(result.getEndDate()).isEqualTo(LocalDate.of(2025, 6, 3)); + } + + @Test + void 태스크루틴에서_일일_태스크를_생성할_수_있다() { + // given + TaskRoutineEntity routine = TaskRoutineEntity.createDaily( + 1L, "title", "desc", + LocalDate.of(2025, 6, 1), + LocalDate.of(2025, 6, 3), + LocalTime.of(9, 0), + LocalTime.of(18, 0), + false, + null + ); + + // when + List tasks = strategy.generateTasks(routine, 1L); + + // then + assertThat(tasks).hasSize(3); + assertThat(tasks.get(0).getTaskDate()).isEqualTo(LocalDate.of(2025, 6, 1)); + assertThat(tasks.get(2).getTaskDate()).isEqualTo(LocalDate.of(2025, 6, 3)); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/MonthlyTaskRoutineGenerationStrategyTest.java b/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/MonthlyTaskRoutineGenerationStrategyTest.java new file mode 100644 index 00000000..580496b1 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/MonthlyTaskRoutineGenerationStrategyTest.java @@ -0,0 +1,80 @@ +package com.mangoboss.app.domain.service.task.strategy; + +import com.mangoboss.app.dto.task.request.RepeatRule; +import com.mangoboss.app.dto.task.request.TaskRoutineCreateRequest; +import com.mangoboss.storage.task.TaskEntity; +import com.mangoboss.storage.task.TaskRoutineEntity; +import com.mangoboss.storage.task.TaskRoutineRepeatType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class MonthlyTaskRoutineGenerationStrategyTest { + + private final MonthlyTaskRoutineGenerationStrategy strategy = new MonthlyTaskRoutineGenerationStrategy(); + + @Test + void MONTHLY_전략은_타입이_MONTHLY이다() { + // then + assertThat(strategy.getType()).isEqualTo(TaskRoutineRepeatType.MONTHLY); + } + + @Test + void MONTHLY_전략으로_루틴을_생성할_수_있다() { + // given + RepeatRule repeatRule = new RepeatRule(null, List.of(1, 10, 20)); + TaskRoutineCreateRequest request = new TaskRoutineCreateRequest( + TaskRoutineRepeatType.MONTHLY, + "월간 회의", + "매달 회의 있음", + LocalDate.of(2025, 6, 1), + LocalDate.of(2025, 8, 31), + LocalTime.of(9, 0), + LocalTime.of(10, 0), + false, + null, + repeatRule + ); + + // when + TaskRoutineEntity routine = strategy.generateTaskRoutine(request, 1L); + + // then + assertThat(routine.getRepeatDates()).containsExactly(1, 10, 20); + assertThat(routine.getRepeatType()).isEqualTo(TaskRoutineRepeatType.MONTHLY); + } + + @Test + void 반복일자에_해당하는_날짜에만_태스크가_생성된다() { + // given + List repeatDates = List.of(1, 10, 20); + + TaskRoutineEntity routine = TaskRoutineEntity.createMonthly( + 1L, + "오전 회의", + "정기 회의 루틴", + repeatDates, + LocalDate.of(2025, 6, 1), + LocalDate.of(2025, 7, 31), + LocalTime.of(9, 0), + LocalTime.of(10, 0), + false, + null + ); + + // when + List tasks = strategy.generateTasks(routine, routine.getStoreId()); + + // then + assertThat(tasks).isNotEmpty(); + assertThat(tasks).extracting(TaskEntity::getTaskDate) + .allSatisfy(date -> assertThat(repeatDates).contains(date.getDayOfMonth())); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/WeeklyTaskRoutineGenerationStrategyTest.java b/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/WeeklyTaskRoutineGenerationStrategyTest.java new file mode 100644 index 00000000..45cddc6a --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/task/strategy/WeeklyTaskRoutineGenerationStrategyTest.java @@ -0,0 +1,82 @@ +package com.mangoboss.app.domain.service.task.strategy; + +import com.mangoboss.app.dto.task.request.RepeatRule; +import com.mangoboss.app.dto.task.request.TaskRoutineCreateRequest; +import com.mangoboss.storage.task.TaskEntity; +import com.mangoboss.storage.task.TaskRoutineEntity; +import com.mangoboss.storage.task.TaskRoutineRepeatType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class WeeklyTaskRoutineGenerationStrategyTest { + + private final WeeklyTaskRoutineGenerationStrategy strategy = new WeeklyTaskRoutineGenerationStrategy(); + + @Test + void WEEKLY_전략은_타입이_WEEKLY이다() { + // then + assertThat(strategy.getType()).isEqualTo(TaskRoutineRepeatType.WEEKLY); + } + + @Test + void WEEKLY_전략으로_루틴을_생성할_수_있다() { + // given + RepeatRule repeatRule = new RepeatRule(List.of(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), null); + TaskRoutineCreateRequest request = new TaskRoutineCreateRequest( + TaskRoutineRepeatType.WEEKLY, + "주간 청소", + "정기 청소", + LocalDate.of(2025, 6, 1), + LocalDate.of(2025, 6, 30), + LocalTime.of(10, 0), + LocalTime.of(16, 0), + false, + null, + repeatRule + ); + + // when + TaskRoutineEntity routine = strategy.generateTaskRoutine(request, 1L); + + // then + assertThat(routine.getRepeatDays()).containsExactlyInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.FRIDAY); + assertThat(routine.getRepeatType()).isEqualTo(TaskRoutineRepeatType.WEEKLY); + } + + @Test + void 반복요일에_해당하는_날짜에만_태스크가_생성된다() { + // given + Set repeatDays = Set.of(DayOfWeek.MONDAY, DayOfWeek.FRIDAY); + + TaskRoutineEntity routine = TaskRoutineEntity.createWeekly( + 1L, + "청소 루틴", + "매장 청소 루틴 설명", + List.copyOf(repeatDays), + LocalDate.of(2025, 6, 1), + LocalDate.of(2025, 6, 14), + LocalTime.of(10, 0), + LocalTime.of(16, 0), + false, + null + ); + + // when + List tasks = strategy.generateTasks(routine, routine.getStoreId()); + + // then + assertThat(tasks).isNotEmpty(); + assertThat(tasks).extracting(TaskEntity::getTaskDate) + .allSatisfy(date -> assertThat(repeatDays).contains(date.getDayOfWeek())); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/user/UserServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/user/UserServiceTest.java new file mode 100644 index 00000000..e67c3672 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/user/UserServiceTest.java @@ -0,0 +1,102 @@ +package com.mangoboss.app.domain.service.user; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.domain.repository.UserRepository; +import com.mangoboss.app.dto.user.KakaoUserInfo; +import com.mangoboss.storage.user.Role; +import com.mangoboss.storage.user.UserEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +class UserServiceTest { + + private UserRepository userRepository; + private UserService userService; + + @BeforeEach + void setUp() { + userRepository = mock(UserRepository.class); + userService = new UserService(userRepository); + } + + @Test + void 유저ID로_유저를_정상적으로_조회할_수_있다() { + // given + Long userId = 1L; + UserEntity user = mock(UserEntity.class); + given(userRepository.getById(userId)).willReturn(user); + + // when + UserEntity result = userService.getUserById(userId); + + // then + assertEquals(user, result); + } + + @Test + void 카카오ID가_존재하면_기존_유저를_반환해야_한다() { + // given + KakaoUserInfo kakaoInfo = KakaoUserInfo.create(123L, "test@example.com", "홍길동", "http://img", LocalDate.of(2000, 1, 1), "010-0000-0000"); + UserEntity existingUser = mock(UserEntity.class); + given(userRepository.findByKakaoId(123L)).willReturn(Optional.of(existingUser)); + + // when + UserEntity result = userService.getOrCreateUser(kakaoInfo); + + // then + assertEquals(existingUser, result); + verify(userRepository, never()).save(any()); + } + + @Test + void 카카오ID가_존재하지_않으면_유저를_신규_생성해야_한다() { + // given + KakaoUserInfo kakaoInfo = KakaoUserInfo.create(456L, "new@example.com", "이순신", "http://img", LocalDate.of(1990, 5, 15), "010-1234-5678"); + UserEntity newUser = kakaoInfo.toEntity(Role.UNASSIGNED); + + given(userRepository.findByKakaoId(456L)).willReturn(Optional.empty()); + given(userRepository.save(any())).willReturn(newUser); + + // when + UserEntity result = userService.getOrCreateUser(kakaoInfo); + + // then + assertEquals(newUser, result); + verify(userRepository).save(any(UserEntity.class)); + } + + @Test + void UNASSIGNED_상태의_유저는_역할을_정상적으로_부여받을_수_있다() { + // given + UserEntity user = mock(UserEntity.class); + given(user.getRole()).willReturn(Role.UNASSIGNED); + + // when + userService.signUp(user, Role.BOSS); + + // then + verify(user).assignRole(Role.BOSS); + } + + @Test + void 이미_가입된_유저는_회원가입시_예외를_던져야_한다() { + // given + UserEntity user = mock(UserEntity.class); + given(user.getRole()).willReturn(Role.BOSS); + + // when + CustomException ex = assertThrows(CustomException.class, + () -> userService.signUp(user, Role.STAFF) + ); + + // then + assertEquals(CustomErrorInfo.ALREADY_SIGNED_UP, ex.getErrorCode()); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/mangoboss/app/domain/service/workreport/WorkReportServiceTest.java b/app/src/test/java/com/mangoboss/app/domain/service/workreport/WorkReportServiceTest.java new file mode 100644 index 00000000..1577db50 --- /dev/null +++ b/app/src/test/java/com/mangoboss/app/domain/service/workreport/WorkReportServiceTest.java @@ -0,0 +1,115 @@ +package com.mangoboss.app.domain.service.workreport; + +import com.mangoboss.app.common.exception.CustomErrorInfo; +import com.mangoboss.app.common.exception.CustomException; +import com.mangoboss.app.domain.repository.WorkReportRepository; +import com.mangoboss.storage.workreport.WorkReportEntity; +import com.mangoboss.storage.workreport.WorkReportTargetType; +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.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class WorkReportServiceTest { + + @InjectMocks + private WorkReportService workReportService; + + @Mock + private WorkReportRepository workReportRepository; + + @Test + void 매장ID와_날짜로_보고서목록을_조회할_수_있다() { + Long storeId = 1L; + LocalDate date = LocalDate.now(); + List mockReports = List.of(mock(WorkReportEntity.class)); + + given(workReportRepository.findByStoreIdAndDateOrderByCreatedAtDesc(storeId, date)) + .willReturn(mockReports); + + List result = workReportService.findByStoreIdAndDateOrderByCreatedAtDesc(storeId, date); + + assertEquals(mockReports, result); + } + + @Test + void 알바생용_보고서를_조회할_수_있다() { + Long storeId = 1L; + LocalDate date = LocalDate.now(); + List mockReports = List.of(mock(WorkReportEntity.class)); + + given(workReportRepository.findByStoreIdAndDateAndTargetTypeOrderByCreatedAtDesc(storeId, date, WorkReportTargetType.TO_STAFF)) + .willReturn(mockReports); + + List result = workReportService.findStaffWorkReports(storeId, date); + + assertEquals(mockReports, result); + } + + @Test + void 보고서ID와_매장ID로_단일_보고서를_조회할_수_있다() { + Long storeId = 1L; + Long reportId = 2L; + WorkReportEntity report = mock(WorkReportEntity.class); + + given(workReportRepository.getByStoreIdAndWorkReportId(storeId, reportId)).willReturn(report); + + WorkReportEntity result = workReportService.getWorkReportByStoreIdAndId(storeId, reportId); + + assertEquals(report, result); + } + + @Test + void 보고서를_생성할_수_있다() { + Long storeId = 1L; + Long staffId = 2L; + String content = "내용"; + String imageUrl = "http://test.com/image.png"; + WorkReportTargetType type = WorkReportTargetType.TO_STAFF; + + WorkReportEntity created = WorkReportEntity.create(storeId, staffId, content, imageUrl, type); + given(workReportRepository.save(any())).willReturn(created); + + WorkReportEntity result = workReportService.createWorkReport(storeId, staffId, content, imageUrl, type); + + assertEquals(created, result); + } + + @Test + void 보스용_보고서이면_접근시_예외를_던진다() { + Long storeId = 1L; + Long reportId = 2L; + WorkReportEntity report = mock(WorkReportEntity.class); + + given(workReportRepository.getByStoreIdAndWorkReportId(storeId, reportId)).willReturn(report); + given(report.getTargetType()).willReturn(WorkReportTargetType.TO_BOSS); + + CustomException ex = assertThrows(CustomException.class, + () -> workReportService.validateStaffHasAccessToWorkReport(storeId, reportId) + ); + + assertEquals(CustomErrorInfo.WORK_REPORT_ACCESS_DENIED, ex.getErrorCode()); + } + + @Test + void 알바생용_보고서이면_정상적으로_접근할_수_있다() { + Long storeId = 1L; + Long reportId = 2L; + WorkReportEntity report = mock(WorkReportEntity.class); + + given(workReportRepository.getByStoreIdAndWorkReportId(storeId, reportId)).willReturn(report); + given(report.getTargetType()).willReturn(WorkReportTargetType.TO_STAFF); + + assertDoesNotThrow(() -> workReportService.validateStaffHasAccessToWorkReport(storeId, reportId)); + } +} \ No newline at end of file diff --git a/app/src/test/resources/templates/contract-template.html b/app/src/test/resources/templates/contract-template.html new file mode 100644 index 00000000..6e8012a9 --- /dev/null +++ b/app/src/test/resources/templates/contract-template.html @@ -0,0 +1,194 @@ + + + + + 기본 근로계약서 + + + + +

근로계약서

+ +
+
${storeName} (이하 "사업주"라 함)과 ${staffName} + (이하 "근로자"라 함)은 다음과 같이 근로계약을 체결한다. +
+
+ +
+
1. 근로계약기간: ${contractStart} ~ ${contractEnd}
+
2. 근무 장소: ${storeAddress}
+
3. 업무: ${duty}
+
4. 근무 시간:
+ + + + + + + + + + ${workSchedules} + +
요일시작 시간종료 시간
+
5. 시급: ${hourlyWage} 원
+
+ +
+
근로계약서 교부
+
사업주는 근로계약을 체결함과 동시에 본 계약서를 사본하여 근로자에게 교부함 (근로기준법 제17조 이행)
+
+ +
+
기타
+ 이 계약에 정함이 없는 사항은 근로기준법령에 의함 +
+ +
+
매장명: ${storeName}
+
사업자번호: ${businessNumber}
+
매장 주소: ${storeAddress}
+
대표자명: ${bossName}
+
근로자 성명: ${staffName}
+
연락처: ${staffPhone}
+
+ +
+
+
사장님 서명
+
+ 사장님 서명 +
+
+
+
근로자 서명
+
+ 근로자 서명 +
+
+
+ + + + + \ No newline at end of file diff --git a/app/src/test/resources/templates/fonts/NotoSansKR-Bold.ttf b/app/src/test/resources/templates/fonts/NotoSansKR-Bold.ttf new file mode 100644 index 00000000..6cf639eb Binary files /dev/null and b/app/src/test/resources/templates/fonts/NotoSansKR-Bold.ttf differ diff --git a/app/src/test/resources/templates/fonts/NotoSansKR-Regular.ttf b/app/src/test/resources/templates/fonts/NotoSansKR-Regular.ttf new file mode 100644 index 00000000..1b14d324 Binary files /dev/null and b/app/src/test/resources/templates/fonts/NotoSansKR-Regular.ttf differ