✅ 补个人认证「提交→审核→生效」闭环测试

司机认证最关键的一条业务闭环,单看提交侧或审核侧都验证不到:
提交之后司机侧查到的必须仍是旧生效值(否则等于提交即自动认证),
审核通过之后才变成新值(否则认证永远不生效)。

用内存 Map 承接记录读写,让提交服务与审核服务真实共享同一份状态,
避免两边各自 mock 成「恰好一致」——那种测试对集成缺陷是盲的。

5 个用例覆盖:
- 提交后仍是旧值、审核通过后才变新值;
- 驳回后生效值不变(已认证司机不会被驳回下线);
- 从未认证时审核前查不到生效值;
- 连续提交两次只剩一条待审,且被作废的那条不能再被审核;
- 已通过的记录不可再审(防止把生效快照改坏)。

全模块 63 个测试通过:mvn -o test -pl blade-service/blade-transport -am
This commit is contained in:
2026-10-02 16:36:41 +08:00
parent 2edbacf838
commit 6831ba6fca
@@ -0,0 +1,217 @@
package org.springblade.transport.service.impl;
import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springblade.transport.pojo.dto.PersonCertSubmitRequest;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.entity.TransportCertRecord;
import org.springblade.transport.service.IDriverAppService;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.ITransportCertRecordService;
import org.springblade.transport.service.ITransportVehicleService;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 个人认证的「提交 → 生效值不变 → 审核通过 → 生效值更新」闭环测试。
* <p>
* 这是司机认证最关键的一条业务闭环,也是单看提交侧或审核侧都验证不到的:
* 提交之后司机侧查到的必须仍是**旧生效值**(否则等于提交即自动认证),
* 审核通过之后才变成**新值**(否则认证永远不生效)。
* <p>
* 用一个内存版的记录存储把两侧接起来,避免两边各自 mock 成「恰好一致」。
*/
class PersonCertRoundTripTest {
private static final Long DRIVER_ID = 9001L;
private IDriverAppService driverAppService;
private IDriverService driverService;
private ITransportCertRecordService certRecordService;
private DriverCertServiceImpl driverCertService;
private CertAuditServiceImpl certAuditService;
/**
* 内存记录表:Id → 记录
*/
private final Map<Long, TransportCertRecord> recordStore = new HashMap<>();
private Driver driver;
private long idSequence = 100L;
@BeforeEach
void setUp() {
recordStore.clear();
idSequence = 100L;
driverAppService = mock(IDriverAppService.class);
driverService = mock(IDriverService.class);
certRecordService = mock(ITransportCertRecordService.class);
ITransportVehicleService transportVehicleService = mock(ITransportVehicleService.class);
driver = new Driver();
driver.setId(DRIVER_ID);
driver.setDriverName("张三");
when(driverAppService.currentDriverEntity()).thenAnswer(invocation -> driver);
// 司机表只持有指针,指针是被审核链路改的,这里让桩跟着内存状态走
when(driverService.updateById(any(Driver.class))).thenAnswer(invocation -> {
Driver patch = invocation.getArgument(0);
if (patch.getEffectiveCertRecordId() != null) {
driver.setEffectiveCertRecordId(patch.getEffectiveCertRecordId());
}
return true;
});
stubRecordStore();
driverCertService = new DriverCertServiceImpl(driverAppService, driverService,
transportVehicleService, certRecordService);
certAuditService = new CertAuditServiceImpl(certRecordService, transportVehicleService, driverService);
}
@Test
@DisplayName("提交后仍是旧生效值,审核通过后才变新值")
void submitThenApprove() {
// 1) 先走一遍完整流程,让该司机有一个生效值
Long firstRecordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
certAuditService.audit(firstRecordId, true, null);
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
"审核通过后应能查到生效值");
// 2) 提交新内容:生效值必须**不变**
Long secondRecordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234"));
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
"提交只产生待审记录,不得改变生效值——否则等于提交即自动认证");
assertEquals(firstRecordId, driver.getEffectiveCertRecordId(),
"指针应仍指向原来那条已通过记录");
// 3) 审核通过:生效值才变成新值
certAuditService.audit(secondRecordId, true, null);
assertEquals("张三改名", driverCertService.getEffectivePersonCert().getRealName(),
"审核通过后生效值应切到新记录");
assertEquals(secondRecordId, driver.getEffectiveCertRecordId());
}
@Test
@DisplayName("驳回后生效值不变(已认证对象不会被驳回下线)")
void rejectKeepsEffectiveValue() {
Long firstRecordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
certAuditService.audit(firstRecordId, true, null);
Long secondRecordId = driverCertService.submitPersonCert(personCert("张三改名", "110101199001011234"));
certAuditService.audit(secondRecordId, false, "证件照片模糊");
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
"驳回只终结该次提交,既得认证必须保持有效");
assertEquals(firstRecordId, driver.getEffectiveCertRecordId());
}
@Test
@DisplayName("从未认证过时,审核通过前查不到生效值")
void noEffectiveBeforeFirstApproval() {
assertNull(driverCertService.getEffectivePersonCert());
Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
assertNull(driverCertService.getEffectivePersonCert(),
"待审期间不应有生效值");
certAuditService.audit(recordId, true, null);
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName());
}
@Test
@DisplayName("连续提交两次,不存在两条待审,且旧待审被作废")
void consecutiveSubmitsLeaveSinglePending() {
Long first = driverCertService.submitPersonCert(personCert("第一次", "110101199001011234"));
Long second = driverCertService.submitPersonCert(personCert("第二次", "110101199001011234"));
assertEquals(TransportCertRecord.STATUS_SUPERSEDED, recordStore.get(first).getStatus(),
"前一条待审应被作废");
assertEquals(TransportCertRecord.STATUS_PENDING, recordStore.get(second).getStatus());
long pendingCount = recordStore.values().stream()
.filter(record -> TransportCertRecord.STATUS_PENDING.equals(record.getStatus()))
.count();
assertEquals(1L, pendingCount, "同一司机至多一条待审");
// 被作废的那条不能再被审核(否则会把已作废的记录变成生效值)
org.junit.jupiter.api.Assertions.assertThrows(
org.springblade.core.log.exception.ServiceException.class,
() -> certAuditService.audit(first, true, null));
}
@Test
@DisplayName("已通过的记录不可再被审核(防止把生效快照改坏)")
void approvedRecordCannotBeReaudited() {
Long recordId = driverCertService.submitPersonCert(personCert("张三", "110101199001011234"));
certAuditService.audit(recordId, true, null);
org.junit.jupiter.api.Assertions.assertThrows(
org.springblade.core.log.exception.ServiceException.class,
() -> certAuditService.audit(recordId, false, "事后反悔"));
assertEquals("张三", driverCertService.getEffectivePersonCert().getRealName(),
"重复审核被拒后,生效值不受影响");
}
// ------------------------------------------------------------ 内存记录存储
/**
* 用内存 Map 承接记录读写,让两个服务真实地共享同一份状态
*/
private void stubRecordStore() {
when(certRecordService.save(any(TransportCertRecord.class))).thenAnswer(invocation -> {
TransportCertRecord record = invocation.getArgument(0);
record.setId(++idSequence);
recordStore.put(record.getId(), copyOf(record));
return true;
});
when(certRecordService.updateById(any(TransportCertRecord.class))).thenAnswer(invocation -> {
TransportCertRecord patch = invocation.getArgument(0);
TransportCertRecord stored = recordStore.get(patch.getId());
if (stored != null) {
if (patch.getStatus() != null) {
stored.setStatus(patch.getStatus());
}
stored.setRejectReason(patch.getRejectReason());
stored.setAuditTime(patch.getAuditTime());
stored.setAuditUser(patch.getAuditUser());
}
return true;
});
when(certRecordService.getById(anyLong()))
.thenAnswer(invocation -> recordStore.get(invocation.getArgument(0, Long.class)));
when(certRecordService.selectPendingRecord(any(), anyLong())).thenAnswer(invocation -> {
String bizType = invocation.getArgument(0);
Long bizId = invocation.getArgument(1);
return recordStore.values().stream()
.filter(record -> bizType.equals(record.getBizType())
&& bizId.equals(record.getBizId())
&& TransportCertRecord.STATUS_PENDING.equals(record.getStatus()))
.findFirst()
.orElse(null);
});
}
private TransportCertRecord copyOf(TransportCertRecord source) {
return JSON.parseObject(JSON.toJSONString(source), TransportCertRecord.class);
}
private PersonCertSubmitRequest personCert(String realName, String idCardNo) {
PersonCertSubmitRequest request = new PersonCertSubmitRequest();
request.setRealName(realName);
request.setIdCardNo(idCardNo);
request.setIdCardFrontUrl("http://example.com/front.jpg");
request.setIdCardBackUrl("http://example.com/back.jpg");
return request;
}
}