测试门禁:覆盖失败提示与空响应重发保护
This commit is contained in:
1 parent
63914e2499
commit
481c998e82
4 files changed
+401
No files matched your search
+145
@@ -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());
|
||||
}
|
||||
|
||||
+35
@@ -4,6 +4,7 @@ import com.ruoyi.train.integration.dms.auth.DmsTokenManager;
|
||||
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.exception.DmsTransportException;
|
||||
import com.ruoyi.train.integration.dms.gateway.DmsCredentialGatewayImpl;
|
||||
import com.ruoyi.train.integration.dms.gateway.ExternalCredentialGateway;
|
||||
import com.ruoyi.train.integration.dms.mock.MockDmsServer;
|
||||
@@ -114,4 +115,38 @@ public class DmsPasswordAdapterTest {
|
||||
assertNotNull(resp);
|
||||
assertEquals(Long.valueOf(99002L), resp.getOperationId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为测试:密码下发 (Mutation) 返回 HTTP 200 + body="",
|
||||
* 必须抛出 DmsTransportException (EMPTY_RESPONSE),视作外部结果未决,绝对不可当做普通业务失败
|
||||
*/
|
||||
@Test
|
||||
public void testCreateOnlinePassword_EmptyResponseBody_ThrowsDmsTransportException() {
|
||||
mockServer.setSimulateEmptyResponse(true);
|
||||
|
||||
try {
|
||||
credentialGateway.issueOnlinePassword(
|
||||
5001L, "张三", "13900001111", "654321", "2026-09-16 10:00:00", "2026-09-17 18:00:00"
|
||||
);
|
||||
fail("Should throw DmsTransportException on empty response body");
|
||||
} catch (DmsTransportException e) {
|
||||
assertTrue("Exception message should contain EMPTY_RESPONSE", e.getMessage().contains("EMPTY_RESPONSE"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为测试:密码删除 (Mutation) 返回 HTTP 200 + body="",
|
||||
* 必须抛出 DmsTransportException (EMPTY_RESPONSE),视作外部结果未决
|
||||
*/
|
||||
@Test
|
||||
public void testDeleteOnlinePassword_EmptyResponseBody_ThrowsDmsTransportException() {
|
||||
mockServer.setSimulateEmptyResponse(true);
|
||||
|
||||
try {
|
||||
credentialGateway.revokeOnlinePassword(88001L);
|
||||
fail("Should throw DmsTransportException on empty response body");
|
||||
} catch (DmsTransportException e) {
|
||||
assertTrue("Exception message should contain EMPTY_RESPONSE", e.getMessage().contains("EMPTY_RESPONSE"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -40,6 +40,7 @@ public class MockDmsServer {
|
||||
private final Set<String> invalidatedTokens = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private volatile boolean simulateReadTimeout = false;
|
||||
private volatile boolean simulateEmptyResponse = false;
|
||||
private volatile int tokenTtlSeconds = 7200;
|
||||
|
||||
private final Map<Long, OperationStatusEntry> operationStatusMap = new ConcurrentHashMap<>();
|
||||
@@ -83,6 +84,10 @@ public class MockDmsServer {
|
||||
this.simulateReadTimeout = simulate;
|
||||
}
|
||||
|
||||
public void setSimulateEmptyResponse(boolean simulate) {
|
||||
this.simulateEmptyResponse = simulate;
|
||||
}
|
||||
|
||||
public void setTokenTtlSeconds(int ttl) {
|
||||
this.tokenTtlSeconds = ttl;
|
||||
}
|
||||
@@ -245,6 +250,11 @@ public class MockDmsServer {
|
||||
|
||||
passwordCreatePostCounter.incrementAndGet();
|
||||
|
||||
if (simulateEmptyResponse) {
|
||||
sendResponse(exchange, 200, "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (simulateReadTimeout) {
|
||||
// 模拟网络读超时 (sleep 超过客户端 10000ms 或测试配置的短超时)
|
||||
try {
|
||||
@@ -278,6 +288,11 @@ public class MockDmsServer {
|
||||
|
||||
passwordDeletePostCounter.incrementAndGet();
|
||||
|
||||
if (simulateEmptyResponse) {
|
||||
sendResponse(exchange, 200, "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (simulateReadTimeout) {
|
||||
try {
|
||||
Thread.sleep(1200);
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import assert from 'node:assert';
|
||||
import { resolveRevokeFeedback, resolveIssueFeedback, resolveReconcileFeedback } from '../../src/views/train/auth/authFeedback.js';
|
||||
|
||||
console.log('--- Testing resolveRevokeFeedback ---');
|
||||
|
||||
// 1. REVOKE: FAILED (decision == FAILED || success == false)
|
||||
{
|
||||
const res = resolveRevokeFeedback({ decision: 'FAILED', success: false, reason: '设备离线超时' });
|
||||
assert.strictEqual(res.type, 'error');
|
||||
assert.strictEqual(res.message, '设备离线超时');
|
||||
assert.strictEqual(res.message.includes('成功'), false);
|
||||
}
|
||||
{
|
||||
const res = resolveRevokeFeedback({ decision: 'REVOKE_REQUIRED', success: false, reason: 'DMS rejected delete' });
|
||||
assert.strictEqual(res.type, 'error');
|
||||
assert.strictEqual(res.message, 'DMS rejected delete');
|
||||
assert.strictEqual(res.message.includes('成功'), false);
|
||||
}
|
||||
|
||||
// 2. REVOKE: WAITING/queued (decision == REVOKE_REQUIRED && success=true && !externalInvoked)
|
||||
{
|
||||
const res = resolveRevokeFeedback({
|
||||
decision: 'REVOKE_REQUIRED',
|
||||
success: true,
|
||||
externalInvoked: false,
|
||||
reason: 'DMS disabled; authorization has externalAuthId 123, revoke queued as CREATED'
|
||||
});
|
||||
assert.strictEqual(res.type, 'warning');
|
||||
assert.strictEqual(res.message.includes('当前未执行外部注销') || res.message.includes('queued'), true);
|
||||
assert.strictEqual(res.message.includes('注销成功'), false);
|
||||
}
|
||||
|
||||
// 3. REVOKE: IN_PROGRESS / Dispatched (decision == REVOKE_REQUIRED && success=true && externalInvoked=true)
|
||||
{
|
||||
const res = resolveRevokeFeedback({
|
||||
decision: 'REVOKE_REQUIRED',
|
||||
success: true,
|
||||
externalInvoked: true,
|
||||
reason: 'Password delete accepted by DMS; awaiting poller'
|
||||
});
|
||||
assert.strictEqual(res.type, 'info');
|
||||
assert.strictEqual(res.message, '注销请求已提交,等待设备确认。');
|
||||
}
|
||||
|
||||
// 4. REVOKE: NO_ACTION (decision == NO_ACTION && success=true)
|
||||
{
|
||||
const res = resolveRevokeFeedback({
|
||||
decision: 'NO_ACTION',
|
||||
success: true,
|
||||
reason: 'Draft auth never issued to external system; marked REVOKED locally'
|
||||
});
|
||||
assert.strictEqual(res.type, 'info');
|
||||
assert.strictEqual(res.message, 'Draft auth never issued to external system; marked REVOKED locally');
|
||||
}
|
||||
|
||||
console.log('--- Testing resolveIssueFeedback ---');
|
||||
|
||||
// 1. ISSUE: ACCEPTED with credential
|
||||
{
|
||||
const res = resolveIssueFeedback({
|
||||
status: 'ACCEPTED',
|
||||
message: '凭据已提交下发,设备确认后生效。',
|
||||
oneTimePlaintextCredential: '829471'
|
||||
});
|
||||
assert.strictEqual(res.type, 'success');
|
||||
assert.strictEqual(res.openCredentialDialog, true);
|
||||
assert.strictEqual(res.message, '凭据已提交下发,设备确认后生效。');
|
||||
}
|
||||
|
||||
// 2. ISSUE: ACCEPTED without credential
|
||||
{
|
||||
const res = resolveIssueFeedback({
|
||||
status: 'ACCEPTED',
|
||||
message: '凭据已提交下发,设备确认后生效。',
|
||||
oneTimePlaintextCredential: null
|
||||
});
|
||||
assert.strictEqual(res.type, 'success');
|
||||
assert.strictEqual(res.openCredentialDialog, false);
|
||||
}
|
||||
|
||||
// 3. ISSUE: WAITING_FOR_PREREQUISITE (MISSING_STUDENT_PHONE)
|
||||
{
|
||||
const res = resolveIssueFeedback({
|
||||
status: 'WAITING_FOR_PREREQUISITE',
|
||||
message: 'MISSING_STUDENT_PHONE: 学员主档缺失有效手机号,暂无法下发门禁'
|
||||
});
|
||||
assert.strictEqual(res.type, 'warning');
|
||||
assert.strictEqual(res.openCredentialDialog, false);
|
||||
assert.strictEqual(res.message.includes('MISSING_STUDENT_PHONE'), true);
|
||||
}
|
||||
|
||||
// 4. ISSUE: WAITING_FOR_PREREQUISITE (INVALID_VALIDITY_WINDOW)
|
||||
{
|
||||
const res = resolveIssueFeedback({
|
||||
status: 'WAITING_FOR_PREREQUISITE',
|
||||
message: 'INVALID_VALIDITY_WINDOW: 培训计划结束时间缺失或早于生效开始时间,无法确定凭据有效期窗口'
|
||||
});
|
||||
assert.strictEqual(res.type, 'warning');
|
||||
assert.strictEqual(res.openCredentialDialog, false);
|
||||
assert.strictEqual(res.message.includes('INVALID_VALIDITY_WINDOW'), true);
|
||||
}
|
||||
|
||||
// 5. ISSUE: WAITING_FOR_PREREQUISITE (DMS_DISABLED)
|
||||
{
|
||||
const res = resolveIssueFeedback({
|
||||
status: 'WAITING_FOR_PREREQUISITE',
|
||||
message: 'DMS_DISABLED: 共享运行环境门禁对接未启用 (dms.enabled=false),无法向外部厂商下发门禁凭据'
|
||||
});
|
||||
assert.strictEqual(res.type, 'warning');
|
||||
assert.strictEqual(res.openCredentialDialog, false);
|
||||
assert.strictEqual(res.message.includes('DMS_DISABLED'), true);
|
||||
}
|
||||
|
||||
// 6. ISSUE: AMBIGUOUS (AMBIGUOUS_EXTERNAL_MUTATION)
|
||||
{
|
||||
const res = resolveIssueFeedback({
|
||||
status: 'AMBIGUOUS_EXTERNAL_MUTATION',
|
||||
message: 'AMBIGUOUS_EXTERNAL_MUTATION: 上一次下发结果无法确认,禁止重复下发,请先核对厂商侧凭据。(MANUAL_VENDOR_RECONCILIATION_REQUIRED)'
|
||||
});
|
||||
assert.strictEqual(res.type, 'error');
|
||||
assert.strictEqual(res.openCredentialDialog, false);
|
||||
assert.strictEqual(res.message.includes('禁止重复下发'), true);
|
||||
}
|
||||
|
||||
// 7. ISSUE: FAILED
|
||||
{
|
||||
const res = resolveIssueFeedback({
|
||||
status: 'FAILED',
|
||||
message: '门禁凭据下发失败 (厂商拒绝): 门锁离线'
|
||||
});
|
||||
assert.strictEqual(res.type, 'error');
|
||||
assert.strictEqual(res.openCredentialDialog, false);
|
||||
assert.strictEqual(res.message.includes('门禁凭据下发失败'), true);
|
||||
}
|
||||
|
||||
console.log('--- Testing resolveReconcileFeedback ---');
|
||||
|
||||
// 1. RECONCILE: ALL_SETTLED
|
||||
{
|
||||
const res = resolveReconcileFeedback({
|
||||
batchResult: { outcome: 'ALL_SETTLED' },
|
||||
success: true,
|
||||
reason: 'All settled'
|
||||
});
|
||||
assert.strictEqual(res.type, 'success');
|
||||
assert.strictEqual(res.message, 'All settled');
|
||||
}
|
||||
|
||||
// 2. RECONCILE: IN_PROGRESS
|
||||
{
|
||||
const res = resolveReconcileFeedback({
|
||||
batchResult: { outcome: 'IN_PROGRESS' },
|
||||
success: true
|
||||
});
|
||||
assert.strictEqual(res.type, 'info');
|
||||
assert.strictEqual(res.message.includes('指令已派发至外部设备'), true);
|
||||
}
|
||||
|
||||
// 3. RECONCILE: WAITING_FOR_DMS
|
||||
{
|
||||
const res = resolveReconcileFeedback({
|
||||
batchResult: { outcome: 'WAITING_FOR_DMS' },
|
||||
success: true
|
||||
});
|
||||
assert.strictEqual(res.type, 'warning');
|
||||
assert.strictEqual(res.message.includes('等待DMS恢复后处理'), true);
|
||||
assert.strictEqual(res.message.includes('成功'), false);
|
||||
}
|
||||
|
||||
// 4. RECONCILE: PARTIAL_FAILURE (必须为 error,严禁 success,严禁“对账执行成功”)
|
||||
{
|
||||
const res = resolveReconcileFeedback({
|
||||
batchResult: { outcome: 'PARTIAL_FAILURE' },
|
||||
success: false,
|
||||
reason: '2 dispatched, 1 failed'
|
||||
});
|
||||
assert.strictEqual(res.type, 'error');
|
||||
assert.strictEqual(res.message.includes('部分处理失败'), true);
|
||||
assert.strictEqual(res.message.includes('对账执行成功'), false);
|
||||
}
|
||||
|
||||
// 5. RECONCILE: ALL_FAILED (必须为 error,严禁 success)
|
||||
{
|
||||
const res = resolveReconcileFeedback({
|
||||
batchResult: { outcome: 'ALL_FAILED' },
|
||||
success: false,
|
||||
reason: 'All failed'
|
||||
});
|
||||
assert.strictEqual(res.type, 'error');
|
||||
assert.strictEqual(res.message.includes('对账处理失败') || res.message.includes('执行失败'), true);
|
||||
assert.strictEqual(res.message.includes('对账执行成功'), false);
|
||||
}
|
||||
|
||||
// 6. RECONCILE: REQUIRES_MANUAL_SETTLEMENT (必须为 error,明确需要人工核对厂商侧状态)
|
||||
{
|
||||
const res = resolveReconcileFeedback({
|
||||
batchResult: { outcome: 'REQUIRES_MANUAL_SETTLEMENT' },
|
||||
success: false,
|
||||
reason: 'Manual check required'
|
||||
});
|
||||
assert.strictEqual(res.type, 'error');
|
||||
assert.strictEqual(res.message.includes('需要人工核对厂商侧状态'), true);
|
||||
assert.strictEqual(res.message.includes('对账执行成功'), false);
|
||||
}
|
||||
|
||||
console.log('ALL FRONTEND FEEDBACK TESTS PASSED!');
|
||||
Reference in new issue
Block a user