测试门禁:增加授权生命周期行为回归
This commit is contained in:
1 parent
84e1972947
commit
056f3d7cac
4 files changed
+920
-43
No files matched your search
+704
@@ -0,0 +1,704 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.train.domain.TrainAccommodation;
|
||||
import com.ruoyi.train.domain.TrainClassPlan;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.domain.TrainEnrollment;
|
||||
import com.ruoyi.train.domain.TrainExternalOperation;
|
||||
import com.ruoyi.train.domain.TrainExternalResourceMapping;
|
||||
import com.ruoyi.train.domain.TrainStudent;
|
||||
import com.ruoyi.train.integration.dms.config.DmsProperties;
|
||||
import com.ruoyi.train.integration.dms.dto.DmsPasswordCreateResponse;
|
||||
import com.ruoyi.train.integration.dms.dto.DmsPasswordDeleteResponse;
|
||||
import com.ruoyi.train.integration.dms.dto.IssueResultVO;
|
||||
import com.ruoyi.train.integration.dms.gateway.ExternalCredentialGateway;
|
||||
import com.ruoyi.train.integration.dms.orchestration.AccessAuthorizationOrchestrator;
|
||||
import com.ruoyi.train.integration.dms.orchestration.BatchReconciliationResult;
|
||||
import com.ruoyi.train.integration.dms.orchestration.ReconciliationDecision;
|
||||
import com.ruoyi.train.integration.dms.orchestration.ReconciliationResult;
|
||||
import com.ruoyi.train.mapper.TrainAccommodationMapper;
|
||||
import com.ruoyi.train.mapper.TrainClassPlanMapper;
|
||||
import com.ruoyi.train.mapper.TrainDormAuthMapper;
|
||||
import com.ruoyi.train.mapper.TrainEnrollmentMapper;
|
||||
import com.ruoyi.train.mapper.TrainStudentMapper;
|
||||
import com.ruoyi.train.service.IExternalOperationService;
|
||||
import com.ruoyi.train.service.IExternalResourceMappingService;
|
||||
import com.ruoyi.train.service.impl.TrainDormAuthServiceImpl;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* Package A 核心安全与行为回归测试套件 (Requirements 22 A-L)
|
||||
* 真实执行 Java 逻辑与状态机判定,杜绝纯文本断言
|
||||
*/
|
||||
public class DmsAccessSafetyPackageATest {
|
||||
|
||||
private AccessAuthorizationOrchestrator orchestrator;
|
||||
private DmsProperties dmsProperties;
|
||||
private MockCredentialGatewayStub gatewayStub;
|
||||
|
||||
private Map<Long, TrainEnrollment> enrollmentDb;
|
||||
private Map<Long, TrainAccommodation> accommodationDb;
|
||||
private Map<Long, TrainDormAuth> dormAuthDb;
|
||||
private Map<String, TrainStudent> studentDb;
|
||||
private Map<String, TrainClassPlan> planDb;
|
||||
private Map<String, TrainExternalResourceMapping> resourceMappingDb;
|
||||
private Map<Long, TrainExternalOperation> operationDb;
|
||||
|
||||
private AtomicLong authSeq;
|
||||
private AtomicLong opSeq;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
enrollmentDb = new ConcurrentHashMap<>();
|
||||
accommodationDb = new ConcurrentHashMap<>();
|
||||
dormAuthDb = new ConcurrentHashMap<>();
|
||||
studentDb = new ConcurrentHashMap<>();
|
||||
planDb = new ConcurrentHashMap<>();
|
||||
resourceMappingDb = new ConcurrentHashMap<>();
|
||||
operationDb = new ConcurrentHashMap<>();
|
||||
authSeq = new AtomicLong(100);
|
||||
opSeq = new AtomicLong(500);
|
||||
|
||||
dmsProperties = new DmsProperties();
|
||||
dmsProperties.setEnabled(true);
|
||||
gatewayStub = new MockCredentialGatewayStub();
|
||||
|
||||
TrainEnrollmentMapper enrollmentMapper = createMockEnrollmentMapper();
|
||||
TrainAccommodationMapper accommodationMapper = createMockAccommodationMapper();
|
||||
TrainDormAuthMapper dormAuthMapper = createMockDormAuthMapper();
|
||||
TrainStudentMapper studentMapper = createMockStudentMapper();
|
||||
TrainClassPlanMapper classPlanMapper = createMockClassPlanMapper();
|
||||
IExternalResourceMappingService mappingService = createMockMappingService();
|
||||
IExternalOperationService operationService = createMockOperationService();
|
||||
|
||||
TrainDormAuthServiceImpl authService = new TrainDormAuthServiceImpl();
|
||||
setPrivateField(authService, "trainDormAuthMapper", dormAuthMapper);
|
||||
setPrivateField(authService, "trainAccommodationMapper", accommodationMapper);
|
||||
|
||||
orchestrator = new AccessAuthorizationOrchestrator(
|
||||
enrollmentMapper,
|
||||
accommodationMapper,
|
||||
dormAuthMapper,
|
||||
authService,
|
||||
mappingService,
|
||||
operationService,
|
||||
gatewayStub
|
||||
);
|
||||
orchestrator.setStudentMapper(studentMapper);
|
||||
orchestrator.setClassPlanMapper(classPlanMapper);
|
||||
orchestrator.setDmsProperties(dmsProperties);
|
||||
|
||||
// 初始化标准宿舍与主锁映射
|
||||
TrainExternalResourceMapping room = new TrainExternalResourceMapping();
|
||||
room.setDormId("DORM-101");
|
||||
room.setVendorCode("DMS");
|
||||
room.setMappingStatus("ACTIVE");
|
||||
resourceMappingDb.put("ROOM:DORM-101", room);
|
||||
|
||||
TrainExternalResourceMapping lock = new TrainExternalResourceMapping();
|
||||
lock.setDormId("DORM-101");
|
||||
lock.setVendorCode("DMS");
|
||||
lock.setIsPrimary(1);
|
||||
lock.setMappingStatus("ACTIVE");
|
||||
lock.setExternalResourceId("6001");
|
||||
lock.setExternalHardwareId("HW-6001");
|
||||
resourceMappingDb.put("LOCK:DORM-101:6001", lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* A. DMS disabled:住宿创建不会外呼,auth 保持 PENDING_GRANT
|
||||
*/
|
||||
@Test
|
||||
public void testA_DmsDisabled_NoOutboundHttp_AuthStaysPendingGrant() {
|
||||
dmsProperties.setEnabled(false);
|
||||
|
||||
Long eid = 1L;
|
||||
setupEnrollmentAndAccommodation(eid, "STU-001", "13812345678", "PLAN-001", "DORM-101", 10L);
|
||||
|
||||
// 自动对账 -> AWAITING_MANUAL_ISSUE
|
||||
ReconciliationResult result = orchestrator.reconcile(eid);
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, result.getDecision());
|
||||
assertEquals("DMS disabled must NOT invoke external gateway during reconcile", 0, gatewayStub.issueCallCount.get());
|
||||
|
||||
TrainDormAuth auth = dormAuthDb.get(10L);
|
||||
assertNotNull(auth);
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_PENDING_GRANT, auth.getAuthStatus());
|
||||
|
||||
// 管理员显式下发 -> 因 DMS disabled 被拦截并返回提示,状态依然保持 PENDING_GRANT
|
||||
IssueResultVO issueResult = orchestrator.issueAuthorization(auth.getAuthId(), "admin");
|
||||
assertEquals("WAITING_FOR_PREREQUISITE", issueResult.getStatus());
|
||||
assertTrue(issueResult.getMessage().contains("DMS_DISABLED"));
|
||||
assertEquals("External gateway must not be called when disabled", 0, gatewayStub.issueCallCount.get());
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_PENDING_GRANT, auth.getAuthStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* B. Issue:真实手机号进入 gateway stub,假手机号彻底不存在
|
||||
*/
|
||||
@Test
|
||||
public void testB_Issue_RealStudentPhoneEnteredGateway_FakePhoneNonexistent() {
|
||||
Long eid = 2L;
|
||||
String realPhone = "13987654321";
|
||||
setupEnrollmentAndAccommodation(eid, "STU-002", realPhone, "PLAN-001", "DORM-101", 20L);
|
||||
|
||||
ReconciliationResult r = orchestrator.reconcile(eid);
|
||||
IssueResultVO issueResult = orchestrator.issueAuthorization(r.getDoorAuthId(), "admin");
|
||||
assertEquals("ACCEPTED", issueResult.getStatus());
|
||||
assertNotNull(issueResult.getOneTimePlaintextCredential());
|
||||
|
||||
assertEquals(1, gatewayStub.issueCallCount.get());
|
||||
assertEquals("Gateway must receive student real phone number", realPhone, gatewayStub.lastPhonePassed);
|
||||
assertNotEquals("13800000000", gatewayStub.lastPhonePassed);
|
||||
}
|
||||
|
||||
/**
|
||||
* C. missing phone:gateway调用次数 = 0,返回 WAITING_FOR_PREREQUISITE (MISSING_STUDENT_PHONE)
|
||||
*/
|
||||
@Test
|
||||
public void testC_MissingPhone_GatewayCallsZero() {
|
||||
Long eid = 3L;
|
||||
// 学员手机号为 null
|
||||
setupEnrollmentAndAccommodation(eid, "STU-003", null, "PLAN-001", "DORM-101", 30L);
|
||||
|
||||
ReconciliationResult r = orchestrator.reconcile(eid);
|
||||
IssueResultVO issueResult = orchestrator.issueAuthorization(r.getDoorAuthId(), "admin");
|
||||
|
||||
assertEquals("WAITING_FOR_PREREQUISITE", issueResult.getStatus());
|
||||
assertTrue(issueResult.getMessage().contains("MISSING_STUDENT_PHONE"));
|
||||
assertEquals("Gateway must not be called when student phone is missing", 0, gatewayStub.issueCallCount.get());
|
||||
|
||||
// 验证非法格式手机号
|
||||
TrainStudent student = studentDb.get("STU-003");
|
||||
student.setsPhone("12345"); // 非法
|
||||
IssueResultVO invalidResult = orchestrator.issueAuthorization(r.getDoorAuthId(), "admin");
|
||||
assertEquals("WAITING_FOR_PREREQUISITE", invalidResult.getStatus());
|
||||
assertEquals(0, gatewayStub.issueCallCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* D. validUntil:等于 plan.end_time,不是 +30 day
|
||||
*/
|
||||
@Test
|
||||
public void testD_ValidUntil_EqualsPlanEndTime_NotPlus30Days() {
|
||||
Long eid = 4L;
|
||||
Date explicitEndTime = new Date(System.currentTimeMillis() + 86400000L * 14); // 14 天后
|
||||
setupEnrollmentAndAccommodation(eid, "STU-004", "13711223344", "PLAN-004", "DORM-101", 40L);
|
||||
TrainClassPlan plan = planDb.get("PLAN-004");
|
||||
plan.setEndTime(explicitEndTime);
|
||||
|
||||
ReconciliationResult r = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(r.getDoorAuthId(), "admin");
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
String expectedValidUntilStr = sdf.format(explicitEndTime);
|
||||
assertEquals("validUntil must match plan.end_time strictly", expectedValidUntilStr, gatewayStub.lastValidUntilPassed);
|
||||
|
||||
TrainDormAuth auth = dormAuthDb.get(40L);
|
||||
assertEquals(explicitEndTime, auth.getAuthEnd());
|
||||
}
|
||||
|
||||
/**
|
||||
* E. invalid validity:gateway调用次数 = 0,返回 WAITING_FOR_PREREQUISITE
|
||||
*/
|
||||
@Test
|
||||
public void testE_InvalidValidity_GatewayCallsZero() {
|
||||
Long eid = 5L;
|
||||
setupEnrollmentAndAccommodation(eid, "STU-005", "13699887766", "PLAN-005", "DORM-101", 50L);
|
||||
TrainClassPlan plan = planDb.get("PLAN-005");
|
||||
// 结束时间早于现在 (已过期)
|
||||
plan.setEndTime(new Date(System.currentTimeMillis() - 86400000L));
|
||||
|
||||
ReconciliationResult r = orchestrator.reconcile(eid);
|
||||
IssueResultVO issueResult = orchestrator.issueAuthorization(r.getDoorAuthId(), "admin");
|
||||
|
||||
assertEquals("WAITING_FOR_PREREQUISITE", issueResult.getStatus());
|
||||
assertTrue(issueResult.getMessage().contains("INVALID_VALIDITY_WINDOW"));
|
||||
assertEquals("Gateway must not be called when validity window is invalid", 0, gatewayStub.issueCallCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* F. double issue:GRANTING/ACTIVE拒绝再次下发
|
||||
*/
|
||||
@Test
|
||||
public void testF_DoubleIssue_GrantingAndActiveBlockReissue() {
|
||||
Long eid = 6L;
|
||||
setupEnrollmentAndAccommodation(eid, "STU-006", "13511112222", "PLAN-001", "DORM-101", 60L);
|
||||
|
||||
ReconciliationResult r = orchestrator.reconcile(eid);
|
||||
Long authId = r.getDoorAuthId();
|
||||
|
||||
// 首次 issue -> 成功进入 GRANTING
|
||||
IssueResultVO firstIssue = orchestrator.issueAuthorization(authId, "admin");
|
||||
assertEquals("ACCEPTED", firstIssue.getStatus());
|
||||
|
||||
TrainDormAuth auth = dormAuthDb.get(60L);
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_GRANTING, auth.getAuthStatus());
|
||||
|
||||
// 第二次 issue (处于 GRANTING) -> 必须被拒绝
|
||||
try {
|
||||
orchestrator.issueAuthorization(authId, "admin");
|
||||
fail("Expected ServiceException when issuing during GRANTING");
|
||||
} catch (ServiceException ex) {
|
||||
assertTrue(ex.getMessage().contains("禁止重复下发"));
|
||||
}
|
||||
|
||||
// 推进到 ACTIVE 后再次 issue -> 依然被拒绝
|
||||
auth.setAuthStatus(TrainDormAuth.AUTH_STATUS_ACTIVE);
|
||||
try {
|
||||
orchestrator.issueAuthorization(authId, "admin");
|
||||
fail("Expected ServiceException when issuing during ACTIVE");
|
||||
} catch (ServiceException ex) {
|
||||
assertTrue(ex.getMessage().contains("禁止重复下发"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* G. revoke externalAuthId null:绝不调用 revoke(0)
|
||||
*/
|
||||
@Test
|
||||
public void testG_RevokeExternalAuthIdNull_NeverInvokesDeleteZero() {
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAuthId(701L);
|
||||
auth.setAccommodationId(70L);
|
||||
auth.setAuthStatus(TrainDormAuth.AUTH_STATUS_GRANTING);
|
||||
auth.setExternalAuthId(null); // externalAuthId 为 null
|
||||
dormAuthDb.put(70L, auth);
|
||||
|
||||
ReconciliationResult revokeResult = orchestrator.revokeAuthorization(701L, "admin");
|
||||
assertFalse(revokeResult.isSuccess());
|
||||
assertTrue(revokeResult.getReason().contains("UNRESOLVABLE_EXTERNAL_CREDENTIAL"));
|
||||
|
||||
assertEquals("Delete must NOT be invoked with 0 or null", 0, gatewayStub.revokeCallCount.get());
|
||||
assertEquals("Auth must enter ERROR state rather than REVOKED", TrainDormAuth.AUTH_STATUS_ERROR, auth.getAuthStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* H. multi revoke:3条授权 全部被遍历 不能只处理第一条
|
||||
*/
|
||||
@Test
|
||||
public void testH_MultiRevoke_TraversesAllThreeAuthorizations() {
|
||||
Long eid = 8L;
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(eid);
|
||||
enrollment.setStudentId("STU-008");
|
||||
enrollment.setPlanId("PLAN-001");
|
||||
enrollment.setEnrollmentStatus(3); // 退训
|
||||
enrollmentDb.put(eid, enrollment);
|
||||
|
||||
// 创建 3 个住宿历史与 3 个有效授权
|
||||
for (long i = 1; i <= 3; i++) {
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setAccommodationId(800L + i);
|
||||
acc.setEnrollmentId(eid);
|
||||
acc.setDormId("DORM-101");
|
||||
acc.setAccommodationStatus(2); // 历史已迁出
|
||||
accommodationDb.put(acc.getAccommodationId(), acc);
|
||||
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAuthId(8000L + i);
|
||||
auth.setAccommodationId(acc.getAccommodationId());
|
||||
auth.setAuthStatus(TrainDormAuth.AUTH_STATUS_ACTIVE);
|
||||
auth.setExternalAuthId(String.valueOf(88000L + i));
|
||||
dormAuthDb.put(acc.getAccommodationId(), auth);
|
||||
}
|
||||
|
||||
ReconciliationResult result = orchestrator.reconcile(eid);
|
||||
BatchReconciliationResult batch = result.getBatchResult();
|
||||
assertNotNull(batch);
|
||||
assertEquals("Total items evaluated must be 3", 3, batch.getTotal());
|
||||
assertEquals("All 3 items must be dispatched for revoke", 3, batch.getDispatched());
|
||||
assertEquals("All 3 gateway revokes must be executed", 3, gatewayStub.revokeCallCount.get());
|
||||
assertEquals(BatchReconciliationResult.OUTCOME_IN_PROGRESS, batch.getOutcome());
|
||||
}
|
||||
|
||||
/**
|
||||
* I. 部分失败:BatchReconciliationResult = PARTIAL_FAILURE
|
||||
*/
|
||||
@Test
|
||||
public void testI_PartialFailure_ProducesPartialFailureOutcome() {
|
||||
Long eid = 9L;
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(eid);
|
||||
enrollment.setStudentId("STU-009");
|
||||
enrollment.setPlanId("PLAN-001");
|
||||
enrollment.setEnrollmentStatus(3); // 退训
|
||||
enrollmentDb.put(eid, enrollment);
|
||||
|
||||
// 住宿 1: 正常有效
|
||||
TrainAccommodation acc1 = new TrainAccommodation();
|
||||
acc1.setAccommodationId(901L);
|
||||
acc1.setEnrollmentId(eid);
|
||||
acc1.setDormId("DORM-101");
|
||||
acc1.setAccommodationStatus(2);
|
||||
accommodationDb.put(901L, acc1);
|
||||
|
||||
TrainDormAuth auth1 = new TrainDormAuth();
|
||||
auth1.setAuthId(9001L);
|
||||
auth1.setAccommodationId(901L);
|
||||
auth1.setAuthStatus(TrainDormAuth.AUTH_STATUS_ACTIVE);
|
||||
auth1.setExternalAuthId("88001");
|
||||
dormAuthDb.put(901L, auth1);
|
||||
|
||||
// 住宿 2: externalAuthId 缺失导致注销失败
|
||||
TrainAccommodation acc2 = new TrainAccommodation();
|
||||
acc2.setAccommodationId(902L);
|
||||
acc2.setEnrollmentId(eid);
|
||||
acc2.setDormId("DORM-101");
|
||||
acc2.setAccommodationStatus(2);
|
||||
accommodationDb.put(902L, acc2);
|
||||
|
||||
TrainDormAuth auth2 = new TrainDormAuth();
|
||||
auth2.setAuthId(9002L);
|
||||
auth2.setAccommodationId(902L);
|
||||
auth2.setAuthStatus(TrainDormAuth.AUTH_STATUS_ACTIVE);
|
||||
auth2.setExternalAuthId(null); // 无外部凭据号,注销判定失败
|
||||
dormAuthDb.put(902L, auth2);
|
||||
|
||||
ReconciliationResult result = orchestrator.reconcile(eid);
|
||||
BatchReconciliationResult batch = result.getBatchResult();
|
||||
assertNotNull(batch);
|
||||
assertEquals(2, batch.getTotal());
|
||||
assertEquals(1, batch.getDispatched());
|
||||
assertEquals(1, batch.getFailed());
|
||||
assertEquals("Outcome must be PARTIAL_FAILURE", BatchReconciliationResult.OUTCOME_PARTIAL_FAILURE, batch.getOutcome());
|
||||
}
|
||||
|
||||
/**
|
||||
* J. 成功后:last_error 清空
|
||||
*/
|
||||
@Test
|
||||
public void testJ_SuccessStateAdvance_ClearsLastError() throws Exception {
|
||||
TrainDormAuth auth = new TrainDormAuth();
|
||||
auth.setAuthId(1001L);
|
||||
auth.setAccommodationId(1001L);
|
||||
auth.setAuthStatus(TrainDormAuth.AUTH_STATUS_ERROR);
|
||||
auth.setLastError("Old network timeout error");
|
||||
dormAuthDb.put(1001L, auth);
|
||||
|
||||
TrainDormAuthServiceImpl service = new TrainDormAuthServiceImpl();
|
||||
setPrivateField(service, "trainDormAuthMapper", createMockDormAuthMapper());
|
||||
|
||||
// 推进到 ACTIVE 状态
|
||||
service.updateAuthStatus(1001L, TrainDormAuth.AUTH_STATUS_ACTIVE, 501L, "admin");
|
||||
|
||||
TrainDormAuth updated = dormAuthDb.get(1001L);
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_ACTIVE, updated.getAuthStatus());
|
||||
assertNull("last_error must be cleared upon advancing to ACTIVE", updated.getLastError());
|
||||
}
|
||||
|
||||
/**
|
||||
* K. Credential:不进入日志,不进入 list,不进入 export,不进入 operations,不进入普通业务响应
|
||||
*/
|
||||
@Test
|
||||
public void testK_CredentialSecurity_ZeroLeakageInLogsAndVo() throws Exception {
|
||||
// 验证 TrainAccessManagementController.issueCredential 方法配置了 isSaveResponseData = false
|
||||
Method issueMethod = TrainAccessManagementController.class.getMethod("issueCredential", Long.class);
|
||||
Log logAnnotation = issueMethod.getAnnotation(Log.class);
|
||||
assertNotNull("@Log annotation must be present on issue method", logAnnotation);
|
||||
assertFalse("@Log isSaveResponseData MUST be false to prevent plaintext password leakage into sys_oper_log",
|
||||
logAnnotation.isSaveResponseData());
|
||||
|
||||
// 验证 TrainExternalOperation 实体中无 plaintext_password 字段
|
||||
for (Field f : TrainExternalOperation.class.getDeclaredFields()) {
|
||||
assertNotEquals("TrainExternalOperation must not contain password column", "password", f.getName().toLowerCase());
|
||||
assertNotEquals("TrainExternalOperation must not contain plaintext password", "plaintextpassword", f.getName().toLowerCase());
|
||||
}
|
||||
|
||||
// 验证 TrainDormAuth 实体中无 password 字段
|
||||
for (Field f : TrainDormAuth.class.getDeclaredFields()) {
|
||||
assertFalse("TrainDormAuth must not contain plaintext password column", f.getName().toLowerCase().contains("password"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* L. TrainDormAuthController:不存在任何写 Mapping (POST/PUT/DELETE)
|
||||
*/
|
||||
@Test
|
||||
public void testL_TrainDormAuthController_ZeroWriteMappings() {
|
||||
Class<?> clazz = TrainDormAuthController.class;
|
||||
int writeMappingCount = 0;
|
||||
|
||||
for (Method m : clazz.getDeclaredMethods()) {
|
||||
if (m.isAnnotationPresent(PostMapping.class)) {
|
||||
// export 接口属于安全只读查询导出,其余所有 POST 必须被禁止
|
||||
if (!"export".equals(m.getName())) {
|
||||
writeMappingCount++;
|
||||
}
|
||||
}
|
||||
if (m.isAnnotationPresent(PutMapping.class)) {
|
||||
writeMappingCount++;
|
||||
}
|
||||
if (m.isAnnotationPresent(DeleteMapping.class)) {
|
||||
writeMappingCount++;
|
||||
}
|
||||
if (m.isAnnotationPresent(RequestMapping.class)) {
|
||||
RequestMapping rm = m.getAnnotation(RequestMapping.class);
|
||||
for (RequestMethod method : rm.method()) {
|
||||
if (method == RequestMethod.POST && !"export".equals(m.getName())) {
|
||||
writeMappingCount++;
|
||||
}
|
||||
if (method == RequestMethod.PUT || method == RequestMethod.DELETE) {
|
||||
writeMappingCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("TrainDormAuthController must have ZERO write mutation endpoints", 0, writeMappingCount);
|
||||
}
|
||||
|
||||
/* ==================== 辅助测试工具与 Mock 桩 ==================== */
|
||||
|
||||
private void setupEnrollmentAndAccommodation(Long eid, String studentId, String phone, String planId, String dormId, Long accId) {
|
||||
TrainStudent student = new TrainStudent();
|
||||
student.setsId(studentId);
|
||||
student.setsPhone(phone);
|
||||
studentDb.put(studentId, student);
|
||||
|
||||
TrainClassPlan plan = new TrainClassPlan();
|
||||
plan.setPlanId(planId);
|
||||
plan.setEndTime(new Date(System.currentTimeMillis() + 86400000L * 7)); // 默认 7 天后
|
||||
planDb.put(planId, plan);
|
||||
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(eid);
|
||||
enrollment.setStudentId(studentId);
|
||||
enrollment.setPlanId(planId);
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
enrollmentDb.put(eid, enrollment);
|
||||
|
||||
TrainAccommodation acc = new TrainAccommodation();
|
||||
acc.setAccommodationId(accId);
|
||||
acc.setEnrollmentId(eid);
|
||||
acc.setDormId(dormId);
|
||||
acc.setAccommodationStatus(1);
|
||||
acc.setAssignedTime(new Date());
|
||||
accommodationDb.put(accId, acc);
|
||||
}
|
||||
|
||||
private static class MockCredentialGatewayStub implements ExternalCredentialGateway {
|
||||
final AtomicInteger issueCallCount = new AtomicInteger(0);
|
||||
final AtomicInteger revokeCallCount = new AtomicInteger(0);
|
||||
String lastPhonePassed;
|
||||
String lastValidUntilPassed;
|
||||
|
||||
@Override
|
||||
public DmsPasswordCreateResponse issueOnlinePassword(Long lockId, String holderName, String phone,
|
||||
String plaintextPassword, String validStart, String validUntil) {
|
||||
issueCallCount.incrementAndGet();
|
||||
lastPhonePassed = phone;
|
||||
lastValidUntilPassed = validUntil;
|
||||
|
||||
DmsPasswordCreateResponse resp = new DmsPasswordCreateResponse();
|
||||
resp.setId(88000L + issueCallCount.get());
|
||||
resp.setOperationId(99000L + issueCallCount.get());
|
||||
resp.setLockHardwareId("HW-" + lockId);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DmsPasswordDeleteResponse revokeOnlinePassword(Long keyId) {
|
||||
revokeCallCount.incrementAndGet();
|
||||
DmsPasswordDeleteResponse resp = new DmsPasswordDeleteResponse();
|
||||
resp.setOperationId(99000L + revokeCallCount.get());
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
private TrainEnrollmentMapper createMockEnrollmentMapper() {
|
||||
return (TrainEnrollmentMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainEnrollmentMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainEnrollmentById".equals(method.getName())) {
|
||||
return enrollmentDb.get((Long) args[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private TrainAccommodationMapper createMockAccommodationMapper() {
|
||||
return (TrainAccommodationMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainAccommodationMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("selectActiveAccommodationByEnrollmentId".equals(name)) {
|
||||
Long eid = (Long) args[0];
|
||||
return accommodationDb.values().stream()
|
||||
.filter(a -> eid.equals(a.getEnrollmentId()) && a.getAccommodationStatus() != null && (a.getAccommodationStatus() == 0 || a.getAccommodationStatus() == 1))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
if ("selectAccommodationHistoryByEnrollmentId".equals(name)) {
|
||||
Long eid = (Long) args[0];
|
||||
List<TrainAccommodation> list = new ArrayList<>();
|
||||
for (TrainAccommodation a : accommodationDb.values()) {
|
||||
if (eid.equals(a.getEnrollmentId())) {
|
||||
list.add(a);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
if ("selectTrainAccommodationByAccommodationId".equals(name)) {
|
||||
return accommodationDb.get((Long) args[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private TrainDormAuthMapper createMockDormAuthMapper() {
|
||||
return (TrainDormAuthMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainDormAuthMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("selectTrainDormAuthByAccommodationId".equals(name)) {
|
||||
return dormAuthDb.get((Long) args[0]);
|
||||
}
|
||||
if ("selectTrainDormAuthByAuthId".equals(name)) {
|
||||
Long aid = (Long) args[0];
|
||||
return dormAuthDb.values().stream().filter(a -> aid.equals(a.getAuthId())).findFirst().orElse(null);
|
||||
}
|
||||
if ("insertTrainDormAuth".equals(name)) {
|
||||
TrainDormAuth auth = (TrainDormAuth) args[0];
|
||||
auth.setAuthId(authSeq.incrementAndGet());
|
||||
dormAuthDb.put(auth.getAccommodationId(), auth);
|
||||
return 1;
|
||||
}
|
||||
if ("updateTrainDormAuth".equals(name)) {
|
||||
TrainDormAuth auth = (TrainDormAuth) args[0];
|
||||
dormAuthDb.put(auth.getAccommodationId(), auth);
|
||||
return 1;
|
||||
}
|
||||
if ("clearLastError".equals(name)) {
|
||||
Long aid = (Long) args[0];
|
||||
TrainDormAuth auth = dormAuthDb.values().stream().filter(a -> aid.equals(a.getAuthId())).findFirst().orElse(null);
|
||||
if (auth != null) {
|
||||
auth.setLastError(null);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ("updateSyncStatus".equals(name)) {
|
||||
Long aid = (Long) args[0];
|
||||
Integer st = (Integer) args[1];
|
||||
String err = (String) args[2];
|
||||
TrainDormAuth auth = dormAuthDb.values().stream().filter(a -> aid.equals(a.getAuthId())).findFirst().orElse(null);
|
||||
if (auth != null) {
|
||||
auth.setFaceSyncStatus(st);
|
||||
auth.setLastError(err);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (method.getReturnType().equals(int.class) || method.getReturnType().equals(Integer.class)) {
|
||||
return 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private TrainStudentMapper createMockStudentMapper() {
|
||||
return (TrainStudentMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainStudentMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainStudentBySId".equals(method.getName())) {
|
||||
return studentDb.get((String) args[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private TrainClassPlanMapper createMockClassPlanMapper() {
|
||||
return (TrainClassPlanMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainClassPlanMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainClassPlanByPlanId".equals(method.getName())) {
|
||||
return planDb.get((String) args[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private IExternalResourceMappingService createMockMappingService() {
|
||||
return (IExternalResourceMappingService) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{IExternalResourceMappingService.class},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("getActiveRoomMapping".equals(name)) {
|
||||
String dormId = (String) args[1];
|
||||
return resourceMappingDb.get("ROOM:" + dormId);
|
||||
}
|
||||
if ("getActiveLockMappings".equals(name)) {
|
||||
String dormId = (String) args[1];
|
||||
List<TrainExternalResourceMapping> locks = new ArrayList<>();
|
||||
for (Map.Entry<String, TrainExternalResourceMapping> entry : resourceMappingDb.entrySet()) {
|
||||
if (entry.getKey().startsWith("LOCK:" + dormId)) {
|
||||
locks.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
return locks;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private IExternalOperationService createMockOperationService() {
|
||||
return (IExternalOperationService) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{IExternalOperationService.class},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("createOperation".equals(name)) {
|
||||
TrainExternalOperation op = (TrainExternalOperation) args[0];
|
||||
op.setOperationId(opSeq.incrementAndGet());
|
||||
operationDb.put(op.getOperationId(), op);
|
||||
return op;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void setPrivateField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field f = target.getClass().getDeclaredField(fieldName);
|
||||
f.setAccessible(true);
|
||||
f.set(target, value);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user