测试门禁:覆盖失败提示与空响应重发保护

This commit is contained in:
wangzhibiao committed 2026-09-21 16:56:06 +08:00
1 parent 63914e2499
commit 481c998e82
4 files changed
+401

No files matched your search

@@ -19,6 +19,7 @@ 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.ReconciliationItemVO;
import com.ruoyi.train.integration.dms.orchestration.ReconciliationResult;
import com.ruoyi.train.mapper.TrainAccommodationMapper;
import com.ruoyi.train.mapper.TrainClassPlanMapper;
@@ -474,6 +475,146 @@ public class DmsConsistencyHardeningPackageA1Test {
assertEquals(TrainExternalOperation.STATUS_CREATED, op.getOperationStatus());
}
/**
* MAJ-03 行为测试 1: 2 dispatched + 1 failed -> PARTIAL_FAILURE -> success=false
*/
@Test
public void testMaj03_Behavior1_TwoDispatchedOneFailed_PartialFailure_SuccessFalse() {
BatchReconciliationResult batch = new BatchReconciliationResult(101L);
batch.addItem(new ReconciliationItemVO(1L, 11L, "REVOKE", "DISPATCHED", "ok"));
batch.addItem(new ReconciliationItemVO(2L, 12L, "REVOKE", "DISPATCHED", "ok"));
batch.addItem(new ReconciliationItemVO(3L, 13L, "REVOKE", "FAILED", "vendor error"));
batch.setDispatched(2);
batch.setFailed(1);
batch.computeOutcome();
assertEquals(BatchReconciliationResult.OUTCOME_PARTIAL_FAILURE, batch.getOutcome());
assertFalse(batch.isSuccess());
ReconciliationResult recon = ReconciliationResult.batch(batch);
assertFalse("ReconciliationResult.batch() success 必须为 false", recon.isSuccess());
assertTrue(recon.isExternalInvoked());
}
/**
* MAJ-03 行为测试 2: 全部 failed -> ALL_FAILED -> success=false
*/
@Test
public void testMaj03_Behavior2_AllFailed_SuccessFalse() {
BatchReconciliationResult batch = new BatchReconciliationResult(101L);
batch.addItem(new ReconciliationItemVO(1L, 11L, "REVOKE", "FAILED", "error 1"));
batch.addItem(new ReconciliationItemVO(2L, 12L, "REVOKE", "FAILED", "error 2"));
batch.setFailed(2);
batch.computeOutcome();
assertEquals(BatchReconciliationResult.OUTCOME_ALL_FAILED, batch.getOutcome());
assertFalse(batch.isSuccess());
ReconciliationResult recon = ReconciliationResult.batch(batch);
assertFalse("ReconciliationResult.batch() success 必须为 false", recon.isSuccess());
assertFalse(recon.isExternalInvoked());
}
/**
* MAJ-03 行为测试 3: manualRequired > 0 -> REQUIRES_MANUAL_SETTLEMENT -> success=false
*/
@Test
public void testMaj03_Behavior3_ManualRequired_RequiresManualSettlement_SuccessFalse() {
BatchReconciliationResult batch = new BatchReconciliationResult(101L);
batch.addItem(new ReconciliationItemVO(1L, 11L, "REVOKE", "MANUAL_REQUIRED", "AMBIGUOUS_EXTERNAL_CREDENTIAL"));
batch.setManualRequired(1);
batch.computeOutcome();
assertEquals(BatchReconciliationResult.OUTCOME_REQUIRES_MANUAL_SETTLEMENT, batch.getOutcome());
assertFalse(batch.isSuccess());
ReconciliationResult recon = ReconciliationResult.batch(batch);
assertFalse("ReconciliationResult.batch() success 必须为 false", recon.isSuccess());
assertFalse(recon.isExternalInvoked());
}
/**
* MAJ-03 行为测试 4: queued only -> WAITING_FOR_DMS -> success=true -> externalInvoked=false
*/
@Test
public void testMaj03_Behavior4_QueuedOnly_WaitingForDms_SuccessTrue_ExternalInvokedFalse() {
BatchReconciliationResult batch = new BatchReconciliationResult(101L);
batch.addItem(new ReconciliationItemVO(1L, 11L, "REVOKE", "QUEUED", "DMS disabled"));
batch.setQueued(1);
batch.computeOutcome();
assertEquals(BatchReconciliationResult.OUTCOME_WAITING_FOR_DMS, batch.getOutcome());
assertTrue(batch.isSuccess());
assertFalse(batch.isExternalInvoked());
ReconciliationResult recon = ReconciliationResult.batch(batch);
assertTrue("ReconciliationResult.batch() success 必须为 true", recon.isSuccess());
assertFalse("ReconciliationResult.batch() externalInvoked 必须为 false", recon.isExternalInvoked());
}
/**
* MAJ-03 行为测试 5: dispatched only -> IN_PROGRESS -> success=true
*/
@Test
public void testMaj03_Behavior5_DispatchedOnly_InProgress_SuccessTrue() {
BatchReconciliationResult batch = new BatchReconciliationResult(101L);
batch.addItem(new ReconciliationItemVO(1L, 11L, "REVOKE", "DISPATCHED", "ok"));
batch.addItem(new ReconciliationItemVO(2L, 12L, "REVOKE", "DISPATCHED", "ok"));
batch.setDispatched(2);
batch.computeOutcome();
assertEquals(BatchReconciliationResult.OUTCOME_IN_PROGRESS, batch.getOutcome());
assertTrue(batch.isSuccess());
assertTrue(batch.isExternalInvoked());
ReconciliationResult recon = ReconciliationResult.batch(batch);
assertTrue("ReconciliationResult.batch() success 必须为 true", recon.isSuccess());
assertTrue("ReconciliationResult.batch() externalInvoked 必须为 true", recon.isExternalInvoked());
}
/**
* EMPTY_RESPONSE ambiguity 行为测试:
* 厂商 Mutation 返回 HTTP 200,但 body 为空字符串/null,
* 异常被归类为不确定性异常 (DmsTransportException),
* 必须进入 operation = AMBIGUOUS, auth = ERROR,
* 第二次 Issue 必须被重发守卫阻断,且 gateway total call 仍然 = 1,严禁 FAILED 导致重发!
*/
@Test
public void testEmptyResponse_MutationClassifiedAsAmbiguous_BlocksReissue() {
TrainDormAuth auth = new TrainDormAuth();
auth.setAuthId(authSeq.incrementAndGet());
auth.setAccommodationId(201L);
auth.setAuthStatus(TrainDormAuth.AUTH_STATUS_PENDING_GRANT);
auth.setCredentialType("ONLINE_PASSWORD");
dormAuthDb.put(201L, auth);
gateway.shouldThrowEmptyResponse = true;
// 第一次下发:由于空响应,抛出 DmsTransportException
IssueResultVO result1 = orchestrator.issueAuthorization(auth.getAuthId(), "admin");
assertNotNull(result1);
assertEquals("第一次下发由于空响应必须为 AMBIGUOUS_EXTERNAL_MUTATION",
"AMBIGUOUS_EXTERNAL_MUTATION", result1.getStatus());
TrainExternalOperation op1 = operationService.selectLatestGrantOperationByDoorAuthId(auth.getAuthId());
assertNotNull(op1);
assertEquals("operation_status 必须为 AMBIGUOUS",
TrainExternalOperation.STATUS_AMBIGUOUS, op1.getOperationStatus());
TrainDormAuth updatedAuth = dormAuthService.selectTrainDormAuthByAuthId(auth.getAuthId());
assertEquals("auth_status 必须为 ERROR",
TrainDormAuth.AUTH_STATUS_ERROR, updatedAuth.getAuthStatus());
assertEquals("网关已被调用 1 次", 1, gateway.issueCallCount.get());
// 第二次下发:必须被重发守卫阻断,严禁重新调用外部网关!
IssueResultVO result2 = orchestrator.issueAuthorization(auth.getAuthId(), "admin");
assertNotNull(result2);
assertEquals("第二次下发必须被拦截为 AMBIGUOUS_EXTERNAL_MUTATION",
"AMBIGUOUS_EXTERNAL_MUTATION", result2.getStatus());
assertEquals("网关总调用次数仍然必须为 1,绝对禁止第二次调用",
1, gateway.issueCallCount.get());
}
/**
* T9: 同一宿舍同一有效床位(status in 0,1)重复分配:
* Service 必须预检拦截并抛出明确业务错误 BED_ALREADY_OCCUPIED
@@ -598,6 +739,7 @@ public class DmsConsistencyHardeningPackageA1Test {
final AtomicInteger revokeCallCount = new AtomicInteger(0);
boolean shouldThrowTimeout = false;
boolean shouldRejectBusiness = false;
boolean shouldThrowEmptyResponse = false;
Long failOnKeyId = null;
@Override
@@ -605,6 +747,9 @@ public class DmsConsistencyHardeningPackageA1Test {
String plaintextPassword, String validStart, String validUntil) {
issueCallCount.incrementAndGet();
if (shouldThrowEmptyResponse) {
throw new DmsTransportException("EMPTY_RESPONSE: Received null response from DMS password creation");
}
if (shouldThrowTimeout) {
throw new DmsTransportException("SocketTimeoutException: Read timed out after HTTP write", new SocketTimeoutException());
}