feat: add V034-V035 flyway migrations and phase 04-07 test classes

This commit is contained in:
Codex
2026-09-14 10:01:50 +08:00
parent 0518cda95c
commit 10887f40ed
6 changed files with 406 additions and 0 deletions
@@ -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;
@@ -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;
}
}
@@ -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;
}
}
@@ -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()));
}
}
@@ -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;
}
}