forked from baskbuddy/zhangshun
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
159 lines
8.8 KiB
159 lines
8.8 KiB
package com.mydreamjob.talentinvitation;
|
|
|
|
import com.mydreamjob.audit.AuditService;
|
|
import com.mydreamjob.audit.SecurityAuditService;
|
|
import com.mydreamjob.auth.User;
|
|
import com.mydreamjob.auth.UserRepository;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.job.Job;
|
|
import com.mydreamjob.job.JobRepository;
|
|
import com.mydreamjob.recommendation.JobRecommendationProfile;
|
|
import com.mydreamjob.recommendation.JobRecommendationProfileRepository;
|
|
import com.mydreamjob.review.ReviewModerationService;
|
|
import com.mydreamjob.safety.BlockRelationRepository;
|
|
import com.mydreamjob.talentinvitation.dto.CreateTalentInvitationRequest;
|
|
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.util.Optional;
|
|
import java.util.Set;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
|
class TalentInvitationServiceTest {
|
|
@Mock TalentInvitationRepository invitationRepository;
|
|
@Mock TalentInvitationHistoryRepository historyRepository;
|
|
@Mock UserRepository userRepository;
|
|
@Mock CompanyRepository companyRepository;
|
|
@Mock JobRepository jobRepository;
|
|
@Mock JobRecommendationProfileRepository profileRepository;
|
|
@Mock TalentInvitationCandidateResolver candidateResolver;
|
|
@Mock TalentInvitationConversationService conversationService;
|
|
@Mock TalentInvitationExpiryService expiryService;
|
|
@Mock BlockRelationRepository blockRelationRepository;
|
|
@Mock ReviewModerationService moderationService;
|
|
@Mock AuditService auditService;
|
|
@Mock SecurityAuditService securityAuditService;
|
|
@InjectMocks TalentInvitationService service;
|
|
|
|
private final User companyUser = User.builder().id(1L).role(User.Role.COMPANY).roles(Set.of(User.Role.COMPANY)).build();
|
|
private final Company company = Company.builder().id(10L).ownerUserId(1L).verified(true).build();
|
|
private final Job publishedJob = Job.builder().id(20L).companyId(10L).status("PUBLISHED").title("后端工程师").build();
|
|
|
|
private void commonSetup() {
|
|
when(userRepository.findById(1L)).thenReturn(Optional.of(companyUser));
|
|
when(companyRepository.findByOwnerUserId(1L)).thenReturn(Optional.of(company));
|
|
when(companyRepository.findByIdForUpdate(10L)).thenReturn(Optional.of(company));
|
|
when(jobRepository.findById(20L)).thenReturn(Optional.of(publishedJob));
|
|
when(profileRepository.findByJobId(20L)).thenReturn(Optional.of(JobRecommendationProfile.builder().jobId(20L)
|
|
.enabled(true).reviewStatus(JobRecommendationProfile.APPROVED).build()));
|
|
when(candidateResolver.resolve(eq(10L), eq(20L), any(), any()))
|
|
.thenReturn(new TalentInvitationCandidateResolver.Resolution(101L, true));
|
|
when(invitationRepository.findByCompanyIdAndIdempotencyKey(eq(10L), any())).thenReturn(Optional.empty());
|
|
when(invitationRepository.findByCompanyIdAndJobIdAndStudentUserIdAndStatus(10L, 20L, 101L, TalentInvitation.PENDING))
|
|
.thenReturn(Optional.empty());
|
|
when(invitationRepository.existsInCooldown(eq(10L), eq(101L), any())).thenReturn(false);
|
|
when(invitationRepository.countJobCreatedBetween(eq(10L), eq(20L), any(), any())).thenReturn(0L);
|
|
when(invitationRepository.countCreatedBetween(eq(10L), any(), any())).thenReturn(0L);
|
|
when(moderationService.moderateCompanyReply(any())).thenReturn(
|
|
new ReviewModerationService.ModerationResult(TalentInvitation.PENDING, java.util.List.of(), java.math.BigDecimal.ZERO, null));
|
|
when(invitationRepository.saveAndFlush(any())).thenAnswer(invocation -> {
|
|
TalentInvitation value = invocation.getArgument(0);
|
|
value.setId(55L);
|
|
return value;
|
|
});
|
|
when(invitationRepository.attachConversation(anyLong(), anyLong(), anyLong(), anyLong())).thenReturn(1);
|
|
}
|
|
|
|
private CreateTalentInvitationRequest request(String key, String message, String idempotency) {
|
|
CreateTalentInvitationRequest request = new CreateTalentInvitationRequest();
|
|
request.setCandidateKey(key); request.setMessage(message); request.setIdempotencyKey(idempotency);
|
|
return request;
|
|
}
|
|
|
|
@Test
|
|
void unverifiedCompanyCannotResolveCandidateOrCreateInvitation() {
|
|
when(userRepository.findById(1L)).thenReturn(Optional.of(companyUser));
|
|
when(companyRepository.findByOwnerUserId(1L)).thenReturn(Optional.of(
|
|
Company.builder().id(10L).ownerUserId(1L).verified(false).build()));
|
|
|
|
assertThatThrownBy(() -> service.create(1L, 20L,
|
|
request("candidate-key", "欢迎了解岗位", "3c989a31-6774-4520-9700-e58416cc1890"), null))
|
|
.isInstanceOf(BusinessException.class)
|
|
.extracting("errorCode").isEqualTo(ErrorCode.COMPANY_NOT_VERIFIED.getCode());
|
|
|
|
verifyNoInteractions(jobRepository, profileRepository, candidateResolver, moderationService,
|
|
invitationRepository, historyRepository, conversationService, expiryService,
|
|
blockRelationRepository, auditService, securityAuditService);
|
|
}
|
|
|
|
@Test
|
|
void safeMessage_createsPendingInvitationAndHistory() {
|
|
commonSetup();
|
|
var response = service.create(1L, 20L,
|
|
request("candidate-key", "欢迎了解岗位", "3c989a31-6774-4520-9700-e58416cc1845"), null);
|
|
assertThat(response.getInvitationId()).isEqualTo(55L);
|
|
assertThat(response.getStatus()).isEqualTo(TalentInvitation.PENDING);
|
|
verify(historyRepository).save(any(TalentInvitationHistory.class));
|
|
verify(auditService).log(eq(1L), eq("TALENT_INVITATION_CREATED"), any(), eq(55L), any(), any());
|
|
}
|
|
|
|
@Test
|
|
void riskyMessage_isRejectedBeforeInsert() {
|
|
commonSetup();
|
|
when(moderationService.moderateCompanyReply(any())).thenReturn(
|
|
new ReviewModerationService.ModerationResult("PENDING", java.util.List.of(ReviewModerationService.REASON_CONTACT_INFO), java.math.BigDecimal.valueOf(2), "MEDIUM"));
|
|
assertThatThrownBy(() -> service.create(1L, 20L,
|
|
request("candidate-key", "请加微信wx_12345", "3c989a31-6774-4520-9700-e58416cc1846"), null))
|
|
.isInstanceOf(BusinessException.class)
|
|
.extracting("errorCode").isEqualTo(ErrorCode.TALENT_INVITATION_CONTENT_INVALID.getCode());
|
|
verify(invitationRepository, never()).saveAndFlush(any());
|
|
verify(securityAuditService).logRejected(eq(1L), eq("TALENT_INVITATION_CREATE_REJECTED"),
|
|
eq("TALENT_INVITATION"), isNull(), contains("\"jobId\":20"), any());
|
|
}
|
|
|
|
@Test
|
|
void idempotentReplayReturnsOriginalBeforeCurrentJobStatusGate() {
|
|
commonSetup();
|
|
publishedJob.setStatus("DRAFT");
|
|
TalentInvitation existing = TalentInvitation.builder().id(77L).companyId(10L).jobId(20L)
|
|
.candidateKeyDigest(sha256("candidate-key")).message("欢迎了解岗位")
|
|
.status(TalentInvitation.PENDING).idempotencyKey("3c989a31-6774-4520-9700-e58416cc1847").build();
|
|
when(invitationRepository.findByCompanyIdAndIdempotencyKey(10L, existing.getIdempotencyKey())).thenReturn(Optional.of(existing));
|
|
var response = service.create(1L, 20L,
|
|
request("candidate-key", "欢迎了解岗位", existing.getIdempotencyKey()), null);
|
|
assertThat(response.getInvitationId()).isEqualTo(77L);
|
|
verifyNoInteractions(candidateResolver, moderationService);
|
|
}
|
|
|
|
@Test
|
|
void nonPositiveJobId_isBadRequestBeforeAnyLookup() {
|
|
assertThatThrownBy(() -> service.create(1L, 0L,
|
|
request("candidate-key", "欢迎了解岗位", "3c989a31-6774-4520-9700-e58416cc1850"), null))
|
|
.isInstanceOf(BusinessException.class)
|
|
.extracting("errorCode").isEqualTo(ErrorCode.BAD_REQUEST.getCode());
|
|
verifyNoInteractions(userRepository, companyRepository, jobRepository, invitationRepository);
|
|
}
|
|
|
|
private String sha256(String value) {
|
|
try {
|
|
byte[] bytes = java.security.MessageDigest.getInstance("SHA-256").digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
|
StringBuilder result = new StringBuilder();
|
|
for (byte b : bytes) result.append(String.format("%02x", b));
|
|
return result.toString();
|
|
} catch (Exception e) { throw new AssertionError(e); }
|
|
}
|
|
}
|
|
|