feat: add V034-V035 flyway migrations and phase 04-07 test classes
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
-- Phase 05: 敏感佐证访问审计记录具体目标和访问结果。
|
||||
ALTER TABLE sensitive_access_logs
|
||||
ADD COLUMN IF NOT EXISTS access_target VARCHAR(128),
|
||||
ADD COLUMN IF NOT EXISTS access_result VARCHAR(32);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sensitive_access_target
|
||||
ON sensitive_access_logs(data_type, access_target, created_at DESC);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- SensitiveAccessLog 继承 BaseEntity,补齐自动填充和软删除字段。
|
||||
ALTER TABLE sensitive_access_logs
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.danyang.match.platform.local_eligibility;
|
||||
|
||||
import com.danyang.match.platform.local_eligibility.dto.LocalEligibilityEvidenceRequest;
|
||||
import com.danyang.match.platform.local_eligibility.entity.LocalEligibility;
|
||||
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityEvidenceMapper;
|
||||
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityMapper;
|
||||
import com.danyang.match.platform.local_eligibility.service.LocalEligibilityService;
|
||||
import com.danyang.match.platform.review.service.ReviewService;
|
||||
import com.danyang.match.platform.shared.config.FeatureFlagConfig;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import com.danyang.match.platform.photo.mapper.UserPhotoMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LocalEligibilityServiceTest {
|
||||
|
||||
@Mock private LocalEligibilityMapper eligibilityMapper;
|
||||
@Mock private LocalEligibilityEvidenceMapper evidenceMapper;
|
||||
@Mock private UserMapper userMapper;
|
||||
@Mock private ReviewService reviewService;
|
||||
@Mock private StringRedisTemplate redisTemplate;
|
||||
@Mock private UserPhotoMapper userPhotoMapper;
|
||||
private FeatureFlagConfig featureFlags;
|
||||
private LocalEligibilityService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
featureFlags = new FeatureFlagConfig();
|
||||
service = new LocalEligibilityService(
|
||||
eligibilityMapper, evidenceMapper, userMapper, reviewService,
|
||||
redisTemplate, featureFlags, userPhotoMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsEvidenceWhenConfiguredMaximumHasBeenReached() {
|
||||
LocalEligibility eligibility = new LocalEligibility();
|
||||
eligibility.setId("eligibility-1");
|
||||
eligibility.setUserId("user-1");
|
||||
eligibility.setVerificationStatus("needs_evidence");
|
||||
when(eligibilityMapper.selectOne(any())).thenReturn(eligibility);
|
||||
when(evidenceMapper.selectCount(any())).thenReturn(5L);
|
||||
|
||||
LocalEligibilityEvidenceRequest request = new LocalEligibilityEvidenceRequest();
|
||||
request.setEvidenceType("employment_proof");
|
||||
request.setFileId("photos/user-1/local_eligibility/proof.jpg");
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> service.uploadEvidence("user-1", request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsEvidenceFileThatDoesNotBelongToCurrentUser() {
|
||||
LocalEligibility eligibility = eligibility("user-1");
|
||||
when(eligibilityMapper.selectOne(any())).thenReturn(eligibility);
|
||||
when(evidenceMapper.selectCount(any())).thenReturn(0L);
|
||||
when(userPhotoMapper.findByUserIdOrderBySort("user-1")).thenReturn(java.util.List.of());
|
||||
|
||||
assertThrows(SecurityException.class,
|
||||
() -> service.uploadEvidence("user-1", request("employment_proof", "photos/other/proof.jpg")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsupportedEvidenceType() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.uploadEvidence("user-1", request("bank_statement", "photos/user-1/proof.jpg")));
|
||||
}
|
||||
|
||||
private LocalEligibility eligibility(String userId) {
|
||||
LocalEligibility eligibility = new LocalEligibility();
|
||||
eligibility.setId("eligibility-1");
|
||||
eligibility.setUserId(userId);
|
||||
eligibility.setVerificationStatus("needs_evidence");
|
||||
return eligibility;
|
||||
}
|
||||
|
||||
private LocalEligibilityEvidenceRequest request(String type, String fileId) {
|
||||
LocalEligibilityEvidenceRequest request = new LocalEligibilityEvidenceRequest();
|
||||
request.setEvidenceType(type);
|
||||
request.setFileId(fileId);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package com.danyang.match.platform.match.service;
|
||||
|
||||
import com.danyang.match.platform.local_eligibility.entity.LocalEligibility;
|
||||
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityMapper;
|
||||
import com.danyang.match.platform.match.config.MatchRuleConfigProperties;
|
||||
import com.danyang.match.platform.match.mapper.RecommendationRecordMapper;
|
||||
import com.danyang.match.platform.profile.entity.UserPreference;
|
||||
import com.danyang.match.platform.profile.entity.UserProfile;
|
||||
import com.danyang.match.platform.profile.mapper.UserPreferenceMapper;
|
||||
import com.danyang.match.platform.profile.mapper.UserPrivacySettingsMapper;
|
||||
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
||||
import com.danyang.match.platform.relationship.entity.UserRelation;
|
||||
import com.danyang.match.platform.relationship.mapper.UserRelationMapper;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.user.entity.User;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RecommendationEligibilityPhase06Test {
|
||||
@Mock UserProfileMapper profileMapper;
|
||||
@Mock UserPreferenceMapper preferenceMapper;
|
||||
@Mock RecommendationRecordMapper recordMapper;
|
||||
@Mock StringRedisTemplate redisTemplate;
|
||||
@Mock UserRelationMapper relationMapper;
|
||||
@Mock UserMapper userMapper;
|
||||
@Mock UserPrivacySettingsMapper privacyMapper;
|
||||
@Mock LocalEligibilityMapper eligibilityMapper;
|
||||
MatchRuleConfigProperties config;
|
||||
RecommendationEngineService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
config = new MatchRuleConfigProperties();
|
||||
service = new RecommendationEngineService(profileMapper, preferenceMapper, recordMapper,
|
||||
config, redisTemplate, relationMapper, userMapper, privacyMapper, eligibilityMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pausedRequesterCannotReceiveRecommendation() {
|
||||
when(userMapper.findById("user-a")).thenReturn(Optional.of(user("paused", true)));
|
||||
assertFalse(service.isStillEligible("user-a", "user-b"));
|
||||
verifyNoInteractions(profileMapper, relationMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulTargetCannotReturnFromCacheOrRealtimeEligibility() {
|
||||
stubEligibleParticipant("user-a");
|
||||
when(userMapper.findById("user-b")).thenReturn(Optional.of(user("matched_successfully", true)));
|
||||
|
||||
assertFalse(service.isStillEligible("user-a", "user-b"));
|
||||
verifyNoInteractions(profileMapper, relationMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingLocalEligibilityIsRejectedWhenGateEnabled() {
|
||||
config.setLocalEligibilityRequired(true);
|
||||
when(userMapper.findById("user-a")).thenReturn(Optional.of(user("searching", true)));
|
||||
when(privacyMapper.findByUserId("user-a")).thenReturn(Optional.empty());
|
||||
LocalEligibility pending = new LocalEligibility();
|
||||
pending.setVerificationStatus("pending_review");
|
||||
when(eligibilityMapper.findByUserId("user-a")).thenReturn(Optional.of(pending));
|
||||
|
||||
assertFalse(service.isStillEligible("user-a", "user-b"));
|
||||
verify(userMapper, never()).findById("user-b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reverseBlockAlwaysRejectsOtherwiseEligiblePair() {
|
||||
stubEligibleParticipant("user-a");
|
||||
stubEligibleParticipant("user-b");
|
||||
UserProfile target = profile("user-b", 1994);
|
||||
when(profileMapper.findByUserId("user-b")).thenReturn(Optional.of(target));
|
||||
when(relationMapper.findByRelation("user-a", "user-b", Constants.RELATION_TYPE_BLOCKED))
|
||||
.thenReturn(Optional.empty());
|
||||
when(relationMapper.findByRelation("user-b", "user-a", Constants.RELATION_TYPE_BLOCKED))
|
||||
.thenReturn(Optional.of(new UserRelation()));
|
||||
|
||||
assertFalse(service.isStillEligible("user-a", "user-b"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void eitherSideRequiredAgeFailureRejectsPair() {
|
||||
UserPreference a = new UserPreference();
|
||||
a.setAgePriority("required");
|
||||
a.setMinAge(25);
|
||||
a.setMaxAge(30);
|
||||
UserPreference b = new UserPreference();
|
||||
UserProfile profileA = profile("user-a", 1995);
|
||||
UserProfile profileB = profile("user-b", 1985);
|
||||
|
||||
assertFalse(service.checkBidirectionalRequired(a, profileB, b, profileA));
|
||||
}
|
||||
|
||||
private void stubEligibleParticipant(String id) {
|
||||
when(userMapper.findById(id)).thenReturn(Optional.of(user("searching", true)));
|
||||
when(privacyMapper.findByUserId(id)).thenReturn(Optional.empty());
|
||||
}
|
||||
|
||||
private User user(String relationshipStatus, boolean recommendationEnabled) {
|
||||
User user = new User();
|
||||
user.setAccountStatus(Constants.ACCOUNT_STATUS_NORMAL);
|
||||
user.setRelationshipStatus(relationshipStatus);
|
||||
user.setRecommendationEnabled(recommendationEnabled);
|
||||
return user;
|
||||
}
|
||||
|
||||
private UserProfile profile(String userId, int birthYear) {
|
||||
UserProfile profile = new UserProfile();
|
||||
profile.setUserId(userId);
|
||||
profile.setBirthDate(LocalDate.of(birthYear, 1, 1));
|
||||
profile.setProfileStatus(Constants.PROFILE_STATUS_APPROVED);
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.danyang.match.platform.relationship;
|
||||
|
||||
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
|
||||
import com.danyang.match.platform.match.mapper.RecommendationRecordMapper;
|
||||
import com.danyang.match.platform.messaging.service.MessagingService;
|
||||
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
||||
import com.danyang.match.platform.relationship.entity.UserRelation;
|
||||
import com.danyang.match.platform.relationship.mapper.MatchMapper;
|
||||
import com.danyang.match.platform.relationship.mapper.UserRelationMapper;
|
||||
import com.danyang.match.platform.relationship.service.RelationshipService;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class RelationshipBlockPriorityPhase08Test {
|
||||
private UserRelationMapper relationMapper;
|
||||
private RelationshipService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
relationMapper = mock(UserRelationMapper.class);
|
||||
service = new RelationshipService(relationMapper, mock(MatchMapper.class),
|
||||
mock(UserProfileMapper.class), mock(RecommendationRecordMapper.class),
|
||||
mock(AuditLogMapper.class), mock(MessagingService.class), mock(UserMapper.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reverseDirectionBlockMustRejectLike() {
|
||||
arrangeReverseBlock("user-b", "user-a");
|
||||
assertThrows(RuntimeException.class, () -> service.like("user-a", "user-b"));
|
||||
verify(relationMapper, never()).insert(any(UserRelation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reverseDirectionBlockMustRejectPass() {
|
||||
arrangeReverseBlock("user-b", "user-a");
|
||||
assertThrows(RuntimeException.class, () -> service.pass("user-a", "user-b"));
|
||||
verify(relationMapper, never()).insert(any(UserRelation.class));
|
||||
}
|
||||
|
||||
private void arrangeReverseBlock(String blocker, String blocked) {
|
||||
when(relationMapper.findByRelation(blocked, blocker, Constants.RELATION_TYPE_BLOCKED))
|
||||
.thenReturn(Optional.empty());
|
||||
when(relationMapper.findByRelation(blocker, blocked, Constants.RELATION_TYPE_BLOCKED))
|
||||
.thenReturn(Optional.of(new UserRelation()));
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package com.danyang.match.platform.review;
|
||||
|
||||
import com.danyang.match.platform.audit.mapper.AuditLogMapper;
|
||||
import com.danyang.match.platform.audit.mapper.SensitiveAccessLogMapper;
|
||||
import com.danyang.match.platform.local_eligibility.entity.LocalEligibility;
|
||||
import com.danyang.match.platform.local_eligibility.entity.LocalEligibilityEvidence;
|
||||
import com.danyang.match.platform.audit.entity.SensitiveAccessLog;
|
||||
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityEvidenceMapper;
|
||||
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityMapper;
|
||||
import com.danyang.match.platform.notification.service.NotificationService;
|
||||
import com.danyang.match.platform.photo.service.PhotoService;
|
||||
import com.danyang.match.platform.profile.mapper.UserProfileMapper;
|
||||
import com.danyang.match.platform.review.entity.ReviewTask;
|
||||
import com.danyang.match.platform.review.mapper.ReviewItemMapper;
|
||||
import com.danyang.match.platform.review.mapper.ReviewReasonMapper;
|
||||
import com.danyang.match.platform.review.mapper.ReviewTaskMapper;
|
||||
import com.danyang.match.platform.review.service.ReviewService;
|
||||
import com.danyang.match.platform.shared.constant.Constants;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReviewServicePhase05Test {
|
||||
@Mock ReviewTaskMapper taskMapper;
|
||||
@Mock ReviewItemMapper itemMapper;
|
||||
@Mock ReviewReasonMapper reasonMapper;
|
||||
@Mock UserProfileMapper profileMapper;
|
||||
@Mock AuditLogMapper auditMapper;
|
||||
@Mock LocalEligibilityMapper eligibilityMapper;
|
||||
@Mock LocalEligibilityEvidenceMapper evidenceMapper;
|
||||
@Mock SensitiveAccessLogMapper sensitiveAccessLogMapper;
|
||||
@Mock NotificationService notificationService;
|
||||
@Mock PhotoService photoService;
|
||||
ReviewService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ReviewService(taskMapper, itemMapper, reasonMapper, profileMapper,
|
||||
auditMapper, eligibilityMapper, evidenceMapper, sensitiveAccessLogMapper,
|
||||
notificationService, photoService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyOneOperatorCanClaimTask() {
|
||||
when(taskMapper.claimTaskAtomic(eq("task-1"), anyString(), any())).thenReturn(1, 0);
|
||||
ReviewTask claimed = localTask("processing", "operator-a");
|
||||
when(taskMapper.selectById("task-1")).thenReturn(claimed);
|
||||
|
||||
assertTrue(service.claimTask("operator-a", "task-1").isPresent());
|
||||
assertTrue(service.claimTask("operator-b", "task-1").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void localEligibilityDecisionRequiresClaimOwner() {
|
||||
when(taskMapper.selectById("task-1")).thenReturn(localTask("processing", "operator-a"));
|
||||
|
||||
RuntimeException error = assertThrows(RuntimeException.class,
|
||||
() -> service.approveLocalEligibility("task-1", "operator-b", "ok"));
|
||||
|
||||
assertTrue(error.getMessage().contains("领取"));
|
||||
verify(taskMapper, never()).updateById(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalUpdatesEligibilityAndReviewTaskTogether() {
|
||||
ReviewTask task = localTask("processing", "operator-a");
|
||||
LocalEligibility eligibility = new LocalEligibility();
|
||||
eligibility.setId("eligibility-1");
|
||||
eligibility.setUserId("user-1");
|
||||
when(taskMapper.selectById("task-1")).thenReturn(task);
|
||||
when(eligibilityMapper.selectOne(any())).thenReturn(eligibility);
|
||||
|
||||
service.approveLocalEligibility("task-1", "operator-a", "材料有效");
|
||||
|
||||
assertEquals("approved", task.getStatus());
|
||||
assertEquals("verified", eligibility.getVerificationStatus());
|
||||
assertEquals("operator-a", eligibility.getVerifiedBy());
|
||||
verify(taskMapper).updateById(task);
|
||||
verify(eligibilityMapper).updateById(eligibility);
|
||||
}
|
||||
|
||||
@Test
|
||||
void evidenceAccessChecksOwnershipAndWritesSuccessfulAudit() {
|
||||
ReviewTask task = localTask("processing", "operator-a");
|
||||
LocalEligibility eligibility = new LocalEligibility();
|
||||
eligibility.setId("eligibility-1");
|
||||
eligibility.setUserId("user-1");
|
||||
LocalEligibilityEvidence evidence = new LocalEligibilityEvidence();
|
||||
evidence.setId("evidence-1");
|
||||
evidence.setLocalEligibilityId("eligibility-1");
|
||||
evidence.setFileId("photos/user-1/local_eligibility/proof.jpg");
|
||||
when(taskMapper.selectById("task-1")).thenReturn(task);
|
||||
when(eligibilityMapper.selectOne(any())).thenReturn(eligibility);
|
||||
when(evidenceMapper.selectById("evidence-1")).thenReturn(evidence);
|
||||
when(photoService.getSignedUrlForAuthorizedReview(evidence.getFileId()))
|
||||
.thenReturn(java.util.Map.of("url", "signed-url", "expiresIn", 600));
|
||||
|
||||
var result = service.accessLocalEligibilityEvidence(
|
||||
"task-1", "evidence-1", "operator-a", "核验居住证明");
|
||||
|
||||
assertEquals("signed-url", result.get("url"));
|
||||
var captor = org.mockito.ArgumentCaptor.forClass(SensitiveAccessLog.class);
|
||||
verify(sensitiveAccessLogMapper).insert(captor.capture());
|
||||
assertEquals("evidence-1", captor.getValue().getAccessTarget());
|
||||
assertEquals("success", captor.getValue().getAccessResult());
|
||||
assertEquals("operator-a", captor.getValue().getOperatorId());
|
||||
}
|
||||
|
||||
private ReviewTask localTask(String status, String operatorId) {
|
||||
ReviewTask task = new ReviewTask();
|
||||
task.setId("task-1");
|
||||
task.setUserId("user-1");
|
||||
task.setReviewType(Constants.REVIEW_TYPE_LOCAL_ELIGIBILITY);
|
||||
task.setStatus(status);
|
||||
task.setAssignedOperatorId(operatorId);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user