From 52c5addaac91e0ef64ae432ff925f0ab94f95f42 Mon Sep 17 00:00:00 2001 From: wooh Date: Sun, 28 Jun 2026 23:52:30 +0900 Subject: [PATCH] =?UTF-8?q?[Fix]=20=EA=B2=B0=EC=A0=9C=20=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=20=EC=8A=B9=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=95=88?= =?UTF-8?q?=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동일 결제 승인 동시 요청 테스트의 비결정적인 성공/실패 개수 검증 제거 - 이미 완료된 동일 paymentKey 요청이 기존 결과를 반환할 수 있는 현재 정책을 반영 - 실패 응답이 발생하는 경우 PAYMENT_ALREADY_PROCESSED 예외인지 검증 - 크레딧 충전과 충전 거래 내역이 한 번만 생성되는 핵심 보장 조건 유지 - 전체 테스트 통과 확인 --- .../JobPostingExtensionIngestService.java | 12 ++- .../JobPostingExtensionIngestServiceTest.java | 73 +++++++++++++++++++ .../payment/service/PaymentServiceTest.java | 9 ++- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestService.java b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestService.java index 0b695c3..4c7f900 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestService.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestService.java @@ -15,6 +15,7 @@ public class JobPostingExtensionIngestService { private final JobPostingIngestService jobPostingIngestService; + private final JobPostingService jobPostingService; private final MockApplyService mockApplyService; public JobPostingExtensionIngestResponse ingest(User user, JobPostingExtensionIngestRequest request) { @@ -25,10 +26,13 @@ public JobPostingExtensionIngestResponse ingest(User user, JobPostingExtensionIn MockApplyCreateResponse mockApply = null; if (ingest.isSavedToDatabase() && ingest.getSaved() != null) { - mockApply = mockApplyService.createMockApplyFromJobPosting( - user, - ingest.getSaved().getJobPostingId() - ); + Long jobPostingId = ingest.getSaved().getJobPostingId(); + try { + mockApply = mockApplyService.createMockApplyFromJobPosting(user, jobPostingId); + } catch (RuntimeException e) { + jobPostingService.deleteJobPosting(user, jobPostingId); + throw e; + } } return JobPostingExtensionIngestResponse.of( diff --git a/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestServiceTest.java index ffe3e8d..8315097 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/jobposting/service/JobPostingExtensionIngestServiceTest.java @@ -20,6 +20,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -31,6 +32,9 @@ class JobPostingExtensionIngestServiceTest { @Mock private JobPostingIngestService jobPostingIngestService; + @Mock + private JobPostingService jobPostingService; + @Mock private MockApplyService mockApplyService; @@ -118,4 +122,73 @@ void ingestSkipsMockApplyWhenJobPostingNotSaved() { assertThat(response.savedToDatabase()).isFalse(); assertThat(response.mockApply()).isNull(); } + + @Test + @DisplayName("공고 저장 성공 응답에 저장 공고가 없으면 모의 서류 지원을 생성하지 않는다") + void ingestSkipsMockApplyWhenSavedJobPostingIsNull() { + JobPostingExtensionIngestRequest request = new JobPostingExtensionIngestRequest( + "https://www.wanted.co.kr/wd/123", + "WANTED", + "채용 공고 원문" + ); + JobPostingIngestResponse ingest = new JobPostingIngestResponse( + true, + "저장 성공", + null, + null, + null, + null, + null + ); + + when(jobPostingIngestService.ingestAndCreate(eq(user), org.mockito.ArgumentMatchers.any(JobPostingIngestRequest.class))) + .thenReturn(ingest); + + JobPostingExtensionIngestResponse response = jobPostingExtensionIngestService.ingest(user, request); + + verify(mockApplyService, never()).createMockApplyFromJobPosting(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any()); + assertThat(response.savedToDatabase()).isTrue(); + assertThat(response.mockApply()).isNull(); + } + + @Test + @DisplayName("모의 서류 지원 생성 실패 시 저장된 공고를 보상 삭제한다") + void ingestDeletesSavedJobPostingWhenMockApplyCreationFails() { + JobPostingExtensionIngestRequest request = new JobPostingExtensionIngestRequest( + "https://www.wanted.co.kr/wd/123", + "WANTED", + "채용 공고 원문" + ); + JobPostingResponse saved = JobPostingResponse.builder() + .jobPostingId(10L) + .userId(1L) + .companyId(2L) + .companyName("테스트 회사") + .detailClassificationId(3L) + .detailClassificationName("백엔드 개발") + .task("주요 업무") + .requirement("자격 요건") + .preferred("우대 사항") + .build(); + JobPostingIngestResponse ingest = new JobPostingIngestResponse( + true, + "저장 성공", + null, + null, + null, + null, + saved + ); + RuntimeException failure = new RuntimeException("mock apply create failed"); + + when(jobPostingIngestService.ingestAndCreate(eq(user), org.mockito.ArgumentMatchers.any(JobPostingIngestRequest.class))) + .thenReturn(ingest); + when(mockApplyService.createMockApplyFromJobPosting(user, 10L)) + .thenThrow(failure); + + assertThatThrownBy(() -> jobPostingExtensionIngestService.ingest(user, request)) + .isSameAs(failure); + + verify(jobPostingService).deleteJobPosting(user, 10L); + } } diff --git a/src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java index a94488f..37d489f 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/payment/service/PaymentServiceTest.java @@ -209,8 +209,13 @@ void confirmConcurrentlyChargesOnlyOnce() throws Exception { } }); - assertThat(results).filteredOn(Result::success).hasSize(1); - assertThat(results).filteredOn(result -> !result.success()).hasSize(1); + assertThat(results).filteredOn(Result::success).isNotEmpty(); + assertThat(results) + .filteredOn(result -> !result.success()) + .allSatisfy(result -> assertThat(result.exception()) + .isInstanceOf(GeneralException.class) + .extracting("code") + .isEqualTo(GeneralErrorCode.PAYMENT_ALREADY_PROCESSED)); assertThat(userRepository.findById(user.getId()).orElseThrow().getCredit()).isEqualTo(2); assertThat(creditTransactionRepository.findAllByUserIdAndTypeOrderByCreatedAtDescIdDesc( user.getId(),