package com.mydreamjob.review; import com.mydreamjob.audit.AuditService; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import com.mydreamjob.company.Company; import com.mydreamjob.company.CompanyService; import com.mydreamjob.job.Job; import com.mydreamjob.job.JobRepository; import com.mydreamjob.review.dto.CreateCompanyReviewReplyRequest; import com.mydreamjob.review.dto.UpdateCompanyReviewReplyRequest; 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.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import java.math.BigDecimal; import java.util.List; import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class CompanyReviewReplyServiceTest { @Mock private CompanyService companyService; @Mock private JobReviewRepository reviewRepository; @Mock private JobRepository jobRepository; @Mock private CompanyReviewReplyRepository replyRepository; @Mock private CompanyReviewReplyHistoryRepository historyRepository; @Mock private ReviewModerationService moderationService; @Mock private AuditService auditService; @Mock private com.fasterxml.jackson.databind.ObjectMapper objectMapper; @InjectMocks private CompanyReviewReplyService service; private static final long USER_ID = 11L; private static final long COMPANY_ID = 21L; private static final long JOB_ID = 31L; private static final long REVIEW_ID = 41L; private final String key = UUID.randomUUID().toString(); private Company company; private JobReview review; private Job job; @BeforeEach void setUp() throws Exception { company = Company.builder().id(COMPANY_ID).ownerUserId(USER_ID).name("Acme").build(); review = JobReview.builder().id(REVIEW_ID).jobId(JOB_ID).studentUserId(99L) .rating(4).content("评价").status("PUBLISHED").build(); job = Job.builder().id(JOB_ID).companyId(COMPANY_ID).title("后端工程师").build(); when(companyService.findCompanyByOwnerUserId(USER_ID)).thenReturn(company); when(reviewRepository.findById(REVIEW_ID)).thenReturn(Optional.of(review)); when(jobRepository.findById(JOB_ID)).thenReturn(Optional.of(job)); when(replyRepository.findByCompanyIdAndIdempotencyKey(any(), any())).thenReturn(Optional.empty()); when(replyRepository.findByReviewId(REVIEW_ID)).thenReturn(Optional.empty()); when(objectMapper.writeValueAsString(any())).thenReturn("[]"); when(replyRepository.saveAndFlush(any())).thenAnswer(invocation -> { CompanyReviewReply reply = invocation.getArgument(0); if (reply.getId() == null) reply.setId(501L); return reply; }); } private ReviewModerationService.ModerationResult autoApproved() { return new ReviewModerationService.ModerationResult( CompanyReviewReply.AUTO_APPROVED, List.of(), BigDecimal.ZERO, null); } private ReviewModerationService.ModerationResult pending() { return new ReviewModerationService.ModerationResult( CompanyReviewReply.PENDING, List.of(ReviewModerationService.REASON_CONTACT_INFO), BigDecimal.valueOf(2), "MEDIUM"); } private CreateCompanyReviewReplyRequest createRequest(String content) { CreateCompanyReviewReplyRequest request = new CreateCompanyReviewReplyRequest(); request.setContent(content); request.setIdempotencyKey(key); return request; } @Test void safeTextIsAutoApprovedAndAudited() { when(moderationService.moderateCompanyReply("感谢反馈")).thenReturn(autoApproved()); var response = service.create(USER_ID, REVIEW_ID, createRequest("感谢反馈"), null); assertEquals(CompanyReviewReply.AUTO_APPROVED, response.getStatus()); verify(historyRepository, times(2)).save(any()); verify(auditService, times(2)).log(any(), any(), any(), any(), any(), any()); } @Test void riskyTextBecomesPendingWithoutExposingReasonToCompany() { when(moderationService.moderateCompanyReply("请联系 13800138000")).thenReturn(pending()); var response = service.create(USER_ID, REVIEW_ID, createRequest("请联系 13800138000"), null); assertEquals(CompanyReviewReply.PENDING, response.getStatus()); assertEquals(null, response.getRejectionReason()); verify(auditService).log(any(), eq("COMPANY_REVIEW_REPLY_CREATED"), any(), any(), any(), any()); } @Test void existingReplyCannotBeCreatedAgain() { when(replyRepository.findByReviewId(REVIEW_ID)).thenReturn(Optional.of( CompanyReviewReply.builder().id(501L).reviewId(REVIEW_ID) .companyId(COMPANY_ID).moderationStatus(CompanyReviewReply.PENDING).content("旧回复").build())); BusinessException ex = assertThrows(BusinessException.class, () -> service.create(USER_ID, REVIEW_ID, createRequest("新回复"), null)); assertEquals(ErrorCode.REVIEW_REPLY_ALREADY_EXISTS.getCode(), ex.getErrorCodeInt()); } @Test void rejectedReplyCanBeResubmittedWithExpectedVersion() { CompanyReviewReply rejected = CompanyReviewReply.builder().id(501L).reviewId(REVIEW_ID) .companyId(COMPANY_ID).content("旧回复").moderationStatus(CompanyReviewReply.REJECTED) .version(2L).build(); when(replyRepository.findByReviewId(REVIEW_ID)).thenReturn(Optional.of(rejected)); when(moderationService.moderateCompanyReply("修订后回复")).thenReturn(autoApproved()); UpdateCompanyReviewReplyRequest request = new UpdateCompanyReviewReplyRequest(); request.setContent("修订后回复"); request.setExpectedVersion(2L); var response = service.revise(USER_ID, REVIEW_ID, request, null); assertEquals("修订后回复", response.getContent()); assertEquals(CompanyReviewReply.AUTO_APPROVED, response.getStatus()); verify(historyRepository, times(2)).save(any()); verify(auditService).log(any(), eq("COMPANY_REVIEW_REPLY_RESUBMITTED"), any(), any(), any(), any()); } @Test void idempotencyKeyWithDifferentPayloadIsConflict() { when(replyRepository.findByCompanyIdAndIdempotencyKey(COMPANY_ID, key)).thenReturn(Optional.of( CompanyReviewReply.builder().id(501L).reviewId(999L).companyId(COMPANY_ID) .content("其他内容").build())); BusinessException ex = assertThrows(BusinessException.class, () -> service.create(USER_ID, REVIEW_ID, createRequest("感谢反馈"), null)); assertEquals(ErrorCode.REVIEW_REPLY_IDEMPOTENCY_CONFLICT.getCode(), ex.getErrorCodeInt()); } }