测试门禁:增加授权生命周期行为回归
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);
|
||||
}
|
||||
}
|
||||
+105
-22
@@ -6,12 +6,15 @@ import com.ruoyi.train.integration.dms.client.DmsHttpClient;
|
||||
import com.ruoyi.train.integration.dms.client.DmsOperationStatusClient;
|
||||
import com.ruoyi.train.integration.dms.client.DmsPasswordClient;
|
||||
import com.ruoyi.train.integration.dms.config.DmsProperties;
|
||||
import com.ruoyi.train.integration.dms.dto.IssueResultVO;
|
||||
import com.ruoyi.train.integration.dms.gateway.DmsCredentialGatewayImpl;
|
||||
import com.ruoyi.train.integration.dms.mock.MockDmsServer;
|
||||
import com.ruoyi.train.integration.dms.poller.DmsOperationPoller;
|
||||
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.ITrainDormAuthService;
|
||||
@@ -99,6 +102,9 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
operationService,
|
||||
credentialGateway
|
||||
);
|
||||
orchestrator.setStudentMapper(createMockStudentMapper());
|
||||
orchestrator.setClassPlanMapper(createMockClassPlanMapper());
|
||||
orchestrator.setDmsProperties(properties);
|
||||
|
||||
// 默认初始化宿舍 201 映射:1 把主门锁 (lockId=5001)
|
||||
TrainExternalResourceMapping room = new TrainExternalResourceMapping();
|
||||
@@ -166,10 +172,15 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
// 3. 学员报到确认 (status -> 1)
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
|
||||
// 触发 reconcile -> 成功发起 GRANT
|
||||
// 触发 reconcile -> C12 方案 B: 自动事件返回 AWAITING_MANUAL_ISSUE,绝不外发密码
|
||||
ReconciliationResult r2 = orchestrator.reconcile(enrollmentId);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, r2.getDecision());
|
||||
assertTrue(r2.isSuccess());
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, r2.getDecision());
|
||||
assertEquals("Auto reconcile must not call DMS password create", 0, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
// 管理员显式交互下发 (Package A / Req 6)
|
||||
IssueResultVO issueResult = orchestrator.issueAuthorization(r2.getDoorAuthId(), "admin");
|
||||
assertEquals("ACCEPTED", issueResult.getStatus());
|
||||
assertNotNull(issueResult.getOneTimePlaintextCredential());
|
||||
assertEquals(1, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
TrainDormAuth auth = getAuthById(r2.getDoorAuthId());
|
||||
@@ -217,10 +228,14 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
acc.setAssignedTime(new Date());
|
||||
accommodationDb.put(20L, acc);
|
||||
|
||||
// 对账 -> 满足所有条件,成功发起 GRANT
|
||||
// 对账 -> 满足所有条件,返回 AWAITING_MANUAL_ISSUE
|
||||
ReconciliationResult r2 = orchestrator.reconcile(enrollmentId);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, r2.getDecision());
|
||||
assertTrue(r2.isSuccess());
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, r2.getDecision());
|
||||
assertEquals(0, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
// 管理员显式下发
|
||||
IssueResultVO issueResult = orchestrator.issueAuthorization(r2.getDoorAuthId(), "admin");
|
||||
assertEquals("ACCEPTED", issueResult.getStatus());
|
||||
assertEquals(1, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
// 模拟对账对齐
|
||||
@@ -241,6 +256,7 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(enrollmentId);
|
||||
enrollment.setStudentId("STU-003");
|
||||
enrollment.setPlanId("PLAN-101");
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
enrollmentDb.put(enrollmentId, enrollment);
|
||||
|
||||
@@ -252,8 +268,10 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
acc.setAssignedTime(new Date());
|
||||
accommodationDb.put(30L, acc);
|
||||
|
||||
// 首次对账
|
||||
orchestrator.reconcile(enrollmentId);
|
||||
// 首次对账 -> AWAITING_MANUAL_ISSUE
|
||||
ReconciliationResult p1 = orchestrator.reconcile(enrollmentId);
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, p1.getDecision());
|
||||
orchestrator.issueAuthorization(p1.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -280,6 +298,7 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(enrollmentId);
|
||||
enrollment.setStudentId("STU-004");
|
||||
enrollment.setPlanId("PLAN-101");
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
enrollmentDb.put(enrollmentId, enrollment);
|
||||
|
||||
@@ -291,7 +310,9 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
accommodationDb.put(40L, acc);
|
||||
|
||||
ReconciliationResult r1 = orchestrator.reconcile(enrollmentId);
|
||||
Long firstOpId = r1.getOperationId();
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, r1.getDecision());
|
||||
IssueResultVO issue1 = orchestrator.issueAuthorization(r1.getDoorAuthId(), "admin");
|
||||
Long firstOpId = operationDb.keySet().iterator().next();
|
||||
|
||||
// 模拟外部门锁执行失败
|
||||
mockServer.setOperationStatus(99001L, "failed", "Motor jammed");
|
||||
@@ -302,12 +323,14 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_ERROR, auth.getAuthStatus());
|
||||
assertNotEquals(TrainDormAuth.AUTH_STATUS_ACTIVE, auth.getAuthStatus());
|
||||
|
||||
// 再次触发对账 -> 重试生成新 Operation
|
||||
ReconciliationResult r2 = orchestrator.reconcile(enrollmentId);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, r2.getDecision());
|
||||
Long secondOpId = r2.getOperationId();
|
||||
// 重试前清除不确定 externalAuthId
|
||||
auth.setExternalAuthId(null);
|
||||
mockServer.setOperationStatus(99002L, "undone", "Queued");
|
||||
|
||||
// 再次下发 -> 重试生成新 Operation
|
||||
IssueResultVO issue2 = orchestrator.issueAuthorization(r1.getDoorAuthId(), "admin");
|
||||
assertEquals("ACCEPTED", issue2.getStatus());
|
||||
|
||||
assertNotEquals("New operation ID must be generated", firstOpId, secondOpId);
|
||||
assertEquals("Previous failed operation must remain in DB",
|
||||
TrainExternalOperation.STATUS_FAILED, operationDb.get(firstOpId).getOperationStatus());
|
||||
}
|
||||
@@ -321,6 +344,7 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(enrollmentId);
|
||||
enrollment.setStudentId("STU-005");
|
||||
enrollment.setPlanId("PLAN-101");
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
enrollmentDb.put(enrollmentId, enrollment);
|
||||
|
||||
@@ -332,7 +356,8 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
accommodationDb.put(50L, acc);
|
||||
|
||||
// 先 Grant 成功并生效
|
||||
orchestrator.reconcile(enrollmentId);
|
||||
ReconciliationResult p1 = orchestrator.reconcile(enrollmentId);
|
||||
orchestrator.issueAuthorization(p1.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -365,6 +390,7 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
TrainEnrollment enrollment = new TrainEnrollment();
|
||||
enrollment.setEnrollmentId(enrollmentId);
|
||||
enrollment.setStudentId("STU-006");
|
||||
enrollment.setPlanId("PLAN-101");
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
enrollmentDb.put(enrollmentId, enrollment);
|
||||
|
||||
@@ -376,7 +402,8 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
oldAcc.setAccommodationStatus(1);
|
||||
accommodationDb.put(60L, oldAcc);
|
||||
|
||||
orchestrator.reconcile(enrollmentId);
|
||||
ReconciliationResult p1 = orchestrator.reconcile(enrollmentId);
|
||||
orchestrator.issueAuthorization(p1.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -414,17 +441,19 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
newAcc.setAccommodationStatus(1);
|
||||
accommodationDb.put(61L, newAcc);
|
||||
|
||||
// 调房后触发对账:必须先 Grant 新房 202!旧房 201 绝不撤销!
|
||||
// 调房后触发对账:新房 202 进入 AWAITING_MANUAL_ISSUE!旧房 201 绝不撤销!
|
||||
ReconciliationResult transferR1 = orchestrator.reconcile(enrollmentId);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, transferR1.getDecision());
|
||||
assertEquals("Old room authorization must remain ACTIVE while new room is granting",
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, transferR1.getDecision());
|
||||
assertEquals("Old room authorization must remain ACTIVE while new room is awaiting issue",
|
||||
TrainDormAuth.AUTH_STATUS_ACTIVE, oldAuth.getAuthStatus());
|
||||
assertEquals("Delete request must be 0 at this point", 0, mockServer.getPasswordDeletePostCounter());
|
||||
|
||||
// 模拟新房 202 经 Poller 确认为 ACTIVE
|
||||
TrainDormAuth newAuth = dormAuthDb.get(61L);
|
||||
mockServer.setOperationStatus(99001L, "success", "New room key accepted");
|
||||
// 管理员下发新房凭据并生效
|
||||
orchestrator.issueAuthorization(transferR1.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99002L, "success", "New room key accepted");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
TrainDormAuth newAuth = dormAuthDb.get(61L);
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_ACTIVE, newAuth.getAuthStatus());
|
||||
|
||||
// 新房确认 ACTIVE 后,再次对账 -> 触发旧房 REVOKE
|
||||
@@ -659,6 +688,60 @@ public class AccessAuthorizationOrchestrationTest {
|
||||
dormAuthDb.put(auth.getAccommodationId(), auth);
|
||||
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 ("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 (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())) {
|
||||
TrainStudent s = new TrainStudent();
|
||||
s.setsId((String) args[0]);
|
||||
s.setsPhone("13912345678");
|
||||
return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private TrainClassPlanMapper createMockClassPlanMapper() {
|
||||
return (TrainClassPlanMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainClassPlanMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainClassPlanByPlanId".equals(method.getName())) {
|
||||
TrainClassPlan p = new TrainClassPlan();
|
||||
p.setPlanId((String) args[0]);
|
||||
p.setEndTime(new Date(System.currentTimeMillis() + 86400000L * 7));
|
||||
return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
+93
-16
@@ -9,9 +9,12 @@ import com.ruoyi.train.integration.dms.config.DmsProperties;
|
||||
import com.ruoyi.train.integration.dms.gateway.DmsCredentialGatewayImpl;
|
||||
import com.ruoyi.train.integration.dms.mock.MockDmsServer;
|
||||
import com.ruoyi.train.integration.dms.poller.DmsOperationPoller;
|
||||
import com.ruoyi.train.integration.dms.dto.IssueResultVO;
|
||||
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 org.junit.After;
|
||||
@@ -104,6 +107,9 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
operationService,
|
||||
credentialGateway
|
||||
);
|
||||
orchestrator.setStudentMapper(createMockStudentMapper());
|
||||
orchestrator.setClassPlanMapper(createMockClassPlanMapper());
|
||||
orchestrator.setDmsProperties(properties);
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -172,7 +178,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
enrollment.setEnrollmentStatus(1);
|
||||
acc.setAccommodationStatus(1);
|
||||
ReconciliationResult r2 = orchestrator.reconcile(eid);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, r2.getDecision());
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, r2.getDecision());
|
||||
orchestrator.issueAuthorization(r2.getDoorAuthId(), "admin");
|
||||
assertEquals(1, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
TrainDormAuth auth = dormAuthDb.get(1001L);
|
||||
@@ -213,7 +220,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
accommodationDb.put(1002L, acc);
|
||||
|
||||
ReconciliationResult r2 = orchestrator.reconcile(eid);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, r2.getDecision());
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, r2.getDecision());
|
||||
orchestrator.issueAuthorization(r2.getDoorAuthId(), "admin");
|
||||
assertEquals(1, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
@@ -243,7 +251,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1003L, acc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -282,7 +291,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1004L, acc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
TrainDormAuth auth = dormAuthDb.get(1004L);
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_GRANTING, auth.getAuthStatus());
|
||||
|
||||
@@ -295,8 +305,9 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
assertNotEquals(TrainDormAuth.AUTH_STATUS_ACTIVE, auth.getAuthStatus());
|
||||
|
||||
// 再次触发对账 -> 重试生成新 Operation
|
||||
ReconciliationResult retryResult = orchestrator.reconcile(eid);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, retryResult.getDecision());
|
||||
auth.setExternalAuthId(null);
|
||||
IssueResultVO retryResult = orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
assertEquals("ACCEPTED", retryResult.getStatus());
|
||||
assertEquals(2, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
mockServer.setOperationStatus(99001L, "success", "Recovered");
|
||||
@@ -325,7 +336,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1005L, acc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
TrainDormAuth auth = dormAuthDb.get(1005L);
|
||||
|
||||
// 轮询返回尚未完成状态 (undone)
|
||||
@@ -361,7 +373,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1006L, acc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -401,7 +414,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1007L, acc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -440,7 +454,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1008L, acc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -476,7 +491,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
oldAcc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1091L, oldAcc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -492,12 +508,13 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
newAcc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1092L, newAcc);
|
||||
|
||||
// 调房对账 1:必须先 Grant 新房!旧房保持 ACTIVE!
|
||||
// 调房对账 1:新房进入 AWAITING_MANUAL_ISSUE!旧房保持 ACTIVE!
|
||||
ReconciliationResult r1 = orchestrator.reconcile(eid);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, r1.getDecision());
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, r1.getDecision());
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_ACTIVE, oldAuth.getAuthStatus());
|
||||
assertEquals(0, mockServer.getPasswordDeletePostCounter());
|
||||
|
||||
orchestrator.issueAuthorization(r1.getDoorAuthId(), "admin");
|
||||
TrainDormAuth newAuth = dormAuthDb.get(1092L);
|
||||
assertNotNull(newAuth);
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_GRANTING, newAuth.getAuthStatus());
|
||||
@@ -675,7 +692,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
accommodationDb.put(1113L, acc);
|
||||
|
||||
ReconciliationResult r = orchestrator.reconcile(eid);
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, r.getDecision());
|
||||
assertEquals(ReconciliationDecision.AWAITING_MANUAL_ISSUE, r.getDecision());
|
||||
orchestrator.issueAuthorization(r.getDoorAuthId(), "admin");
|
||||
assertEquals(1, mockServer.getPasswordCreatePostCounter());
|
||||
|
||||
TrainExternalOperation op = operationDb.values().iterator().next();
|
||||
@@ -736,7 +754,8 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
acc.setAccommodationStatus(1);
|
||||
accommodationDb.put(1115L, acc);
|
||||
|
||||
orchestrator.reconcile(eid);
|
||||
ReconciliationResult rInit = orchestrator.reconcile(eid);
|
||||
orchestrator.issueAuthorization(rInit.getDoorAuthId(), "admin");
|
||||
mockServer.setOperationStatus(99001L, "success", "Success");
|
||||
poller.pollPendingOperations();
|
||||
|
||||
@@ -765,7 +784,43 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("selectTrainEnrollmentById".equals(name) || "selectTrainEnrollmentByEnrollmentId".equals(name)) {
|
||||
return enrollmentDb.get((Long) args[0]);
|
||||
TrainEnrollment e = enrollmentDb.get((Long) args[0]);
|
||||
if (e != null && e.getPlanId() == null) {
|
||||
e.setPlanId("PLAN-MOCK");
|
||||
}
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private TrainStudentMapper createMockStudentMapper() {
|
||||
return (TrainStudentMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainStudentMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainStudentBySId".equals(method.getName())) {
|
||||
TrainStudent s = new TrainStudent();
|
||||
s.setsId((String) args[0]);
|
||||
s.setsPhone("13912345678");
|
||||
return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private TrainClassPlanMapper createMockClassPlanMapper() {
|
||||
return (TrainClassPlanMapper) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{TrainClassPlanMapper.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("selectTrainClassPlanByPlanId".equals(method.getName())) {
|
||||
TrainClassPlan p = new TrainClassPlan();
|
||||
p.setPlanId((String) args[0]);
|
||||
p.setEndTime(new Date(System.currentTimeMillis() + 86400000L * 7));
|
||||
return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -826,6 +881,28 @@ public class DmsMockE2EScenariosSuiteTest {
|
||||
dormAuthDb.put(auth.getAccommodationId(), auth);
|
||||
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 ("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 (method.getReturnType().equals(int.class) || method.getReturnType().equals(Integer.class)) {
|
||||
return 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
+18
-5
@@ -224,10 +224,11 @@ public class DormAuthAlignmentTest
|
||||
TrainDormAuth step1 = dormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
assertEquals(TrainDormAuth.STATUS_FAILED, step1.getFaceSyncStatus());
|
||||
|
||||
// 注销权限 -> STATUS_REVOKED
|
||||
dormAuthService.revokeAuth(authId, "admin");
|
||||
// 注销权限 -> STATUS_REVOKED (使用权威状态推进 updateAuthStatus)
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_REVOKED, null, "admin");
|
||||
TrainDormAuth step2 = dormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
assertEquals(TrainDormAuth.STATUS_REVOKED, step2.getFaceSyncStatus());
|
||||
assertEquals(TrainDormAuth.AUTH_STATUS_REVOKED, step2.getAuthStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +284,7 @@ public class DormAuthAlignmentTest
|
||||
Long newAuthId = newAuth.getAuthId();
|
||||
|
||||
// 3. 旧授权注销 (退宿或换房引起)
|
||||
dormAuthService.revokeAuth(oldAuthId, "admin");
|
||||
dormAuthService.updateAuthStatus(oldAuthId, TrainDormAuth.AUTH_STATUS_REVOKED, null, "admin");
|
||||
|
||||
// 4. 验证两条授权在历史中共存,状态相互独立
|
||||
TrainDormAuth queriedOld = dormAuthService.selectTrainDormAuthByAuthId(oldAuthId);
|
||||
@@ -343,8 +344,8 @@ public class DormAuthAlignmentTest
|
||||
dormAuthService.insertTrainDormAuth(auth);
|
||||
Long authId = auth.getAuthId();
|
||||
|
||||
// 办理注销
|
||||
dormAuthService.revokeAuth(authId, "admin");
|
||||
// 办理注销 (通过 updateAuthStatus 将状态推进为 AUTH_STATUS_REVOKED)
|
||||
dormAuthService.updateAuthStatus(authId, TrainDormAuth.AUTH_STATUS_REVOKED, null, "admin");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -461,6 +462,18 @@ public class DormAuthAlignmentTest
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int clearLastError(Long authId)
|
||||
{
|
||||
TrainDormAuth a = store.get(authId);
|
||||
if (a != null)
|
||||
{
|
||||
a.setLastError(null);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteTrainDormAuthByAuthId(Long authId)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user