新增门禁:实现访问管理界面与Mock端到端测试
This commit is contained in:
1 parent
20c5d494cf
commit
d976fcfb4d
29 files changed
+2423
-281
No files matched your search
+245
@@ -0,0 +1,245 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.domain.TrainExternalOperation;
|
||||
import com.ruoyi.train.domain.TrainExternalResourceMapping;
|
||||
import com.ruoyi.train.integration.dms.orchestration.AccessAuthorizationOrchestrator;
|
||||
import com.ruoyi.train.integration.dms.orchestration.ReconciliationResult;
|
||||
import com.ruoyi.train.service.IExternalOperationService;
|
||||
import com.ruoyi.train.service.IExternalResourceMappingService;
|
||||
import com.ruoyi.train.service.ITrainDormAuthService;
|
||||
|
||||
/**
|
||||
* 宿舍门禁与外部资源映射管理端Controller (Wave 3B)
|
||||
* <p>
|
||||
* 安全红线:
|
||||
* 1. 严格限制管理员权限访问 (train:access:manage / train:access:query);
|
||||
* 2. 严禁提供远程开锁 (Remote Unlock) 入口;
|
||||
* 3. 严禁向前端返回任何明文密码 (password)、Token 或 Secret;
|
||||
* 4. 手动对账严格遵循领域业务前置条件校验,绝不提供绕过前置条件的强开接口。
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-09-16
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/train/access")
|
||||
public class TrainAccessManagementController extends BaseController
|
||||
{
|
||||
private static final String DEFAULT_VENDOR = "DMS";
|
||||
|
||||
@Autowired
|
||||
private IExternalResourceMappingService externalResourceMappingService;
|
||||
|
||||
@Autowired
|
||||
private ITrainDormAuthService trainDormAuthService;
|
||||
|
||||
@Autowired
|
||||
private IExternalOperationService externalOperationService;
|
||||
|
||||
@Autowired
|
||||
private AccessAuthorizationOrchestrator accessAuthorizationOrchestrator;
|
||||
|
||||
/**
|
||||
* 查询指定宿舍的外部资源映射状态(包含房间映射、门锁映射及主锁配置健康度)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:query')")
|
||||
@GetMapping("/mapping/dorm/{dormId}")
|
||||
public AjaxResult getDormMapping(@PathVariable("dormId") String dormId)
|
||||
{
|
||||
TrainExternalResourceMapping roomMapping = externalResourceMappingService.getActiveRoomMapping(DEFAULT_VENDOR, dormId);
|
||||
List<TrainExternalResourceMapping> lockMappings = externalResourceMappingService.getActiveLockMappings(DEFAULT_VENDOR, dormId);
|
||||
if (lockMappings == null)
|
||||
{
|
||||
lockMappings = new ArrayList<>();
|
||||
}
|
||||
|
||||
long primaryCount = lockMappings.stream()
|
||||
.filter(l -> l.getIsPrimary() != null && l.getIsPrimary() == 1)
|
||||
.count();
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("dormId", dormId);
|
||||
data.put("vendorCode", DEFAULT_VENDOR);
|
||||
data.put("roomMapping", roomMapping);
|
||||
data.put("lockMappings", lockMappings);
|
||||
data.put("lockCount", lockMappings.size());
|
||||
data.put("primaryLockCount", primaryCount);
|
||||
data.put("hasPrimaryLock", primaryCount == 1);
|
||||
data.put("isConfigValid", primaryCount == 1 && roomMapping != null);
|
||||
|
||||
if (primaryCount == 0)
|
||||
{
|
||||
data.put("configError", "未配置主门锁 (0 primary lock),门禁凭据无法自动下发");
|
||||
}
|
||||
else if (primaryCount > 1)
|
||||
{
|
||||
data.put("configError", "配置异常:存在多把主门锁 (" + primaryCount + " primary locks),违反单一主锁最少特权原则");
|
||||
}
|
||||
else if (roomMapping == null)
|
||||
{
|
||||
data.put("configError", "未绑定外部房间映射 (No active room mapping)");
|
||||
}
|
||||
else
|
||||
{
|
||||
data.put("configError", null);
|
||||
}
|
||||
|
||||
return success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定或更新宿舍外部房间映射
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:manage')")
|
||||
@PostMapping("/mapping/bind-room")
|
||||
public AjaxResult bindRoomMapping(@RequestBody TrainExternalResourceMapping mapping)
|
||||
{
|
||||
if (mapping.getVendorCode() == null || mapping.getVendorCode().trim().isEmpty())
|
||||
{
|
||||
mapping.setVendorCode(DEFAULT_VENDOR);
|
||||
}
|
||||
mapping.setCreateBy(getUsernameSafe());
|
||||
TrainExternalResourceMapping result = externalResourceMappingService.bindRoomMapping(mapping);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定或新增宿舍外部门锁映射 (支持一房多锁)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:manage')")
|
||||
@PostMapping("/mapping/bind-lock")
|
||||
public AjaxResult bindLockMapping(@RequestBody TrainExternalResourceMapping mapping)
|
||||
{
|
||||
if (mapping.getVendorCode() == null || mapping.getVendorCode().trim().isEmpty())
|
||||
{
|
||||
mapping.setVendorCode(DEFAULT_VENDOR);
|
||||
}
|
||||
mapping.setCreateBy(getUsernameSafe());
|
||||
TrainExternalResourceMapping result = externalResourceMappingService.bindLockMapping(mapping);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除/注销指定的外部资源映射
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:manage')")
|
||||
@DeleteMapping("/mapping/{mappingId}")
|
||||
public AjaxResult unbindMapping(@PathVariable("mappingId") Long mappingId)
|
||||
{
|
||||
boolean ok = externalResourceMappingService.deactivateMapping(mappingId, getUsernameSafe());
|
||||
return toAjax(ok ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询门禁授权列表 (支持按学员、宿舍、状态筛选)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:query')")
|
||||
@GetMapping("/auth/list")
|
||||
public TableDataInfo listAuth(TrainDormAuth trainDormAuth)
|
||||
{
|
||||
startPage();
|
||||
List<TrainDormAuth> list = trainDormAuthService.selectTrainDormAuthList(trainDormAuth);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定门禁授权详细信息
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:query')")
|
||||
@GetMapping("/auth/{authId}")
|
||||
public AjaxResult getAuthDetail(@PathVariable("authId") Long authId)
|
||||
{
|
||||
TrainDormAuth auth = trainDormAuthService.selectTrainDormAuthByAuthId(authId);
|
||||
return success(auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定门禁授权下的外部操作流水历史 (全要素脱敏返回,绝不泄漏密码或凭据)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:query')")
|
||||
@GetMapping("/operations/{doorAuthId}")
|
||||
public AjaxResult getOperationHistory(@PathVariable("doorAuthId") Long doorAuthId)
|
||||
{
|
||||
List<TrainExternalOperation> operations = externalOperationService.listOperationsByDoorAuthId(doorAuthId);
|
||||
List<Map<String, Object>> sanitizedList = new ArrayList<>();
|
||||
if (operations != null)
|
||||
{
|
||||
for (TrainExternalOperation op : operations)
|
||||
{
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("operationId", op.getOperationId());
|
||||
item.put("doorAuthId", op.getDoorAuthId());
|
||||
item.put("operationType", op.getOperationType());
|
||||
item.put("operationStatus", op.getOperationStatus());
|
||||
item.put("vendorCode", op.getVendorCode());
|
||||
item.put("externalOperationId", op.getExternalOperationId());
|
||||
item.put("externalHardwareId", op.getExternalHardwareId());
|
||||
item.put("attemptCount", op.getAttemptCount());
|
||||
item.put("requestedAt", op.getRequestedAt());
|
||||
item.put("completedAt", op.getCompletedAt());
|
||||
item.put("nextPollTime", op.getNextPollTime());
|
||||
item.put("createTime", op.getCreateTime());
|
||||
item.put("updateTime", op.getUpdateTime());
|
||||
item.put("lastErrorCode", op.getLastErrorCode());
|
||||
item.put("lastErrorMessage", sanitizeSecret(op.getLastErrorMessage()));
|
||||
item.put("remark", sanitizeSecret(op.getRemark()));
|
||||
sanitizedList.add(item);
|
||||
}
|
||||
}
|
||||
return success(sanitizedList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员手动触发参训学员门禁权限对账
|
||||
* <p>
|
||||
* 严格遵循相同前置条件校验规则:未报到、未分宿、缺映射或门锁配置异常均正常返回 WAITING_FOR_PREREQUISITE,
|
||||
* 绝不提供绕过安全前置条件的强开权限逻辑。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('train:access:reconcile')")
|
||||
@PostMapping("/reconcile/{enrollmentId}")
|
||||
public AjaxResult manualReconcile(@PathVariable("enrollmentId") Long enrollmentId)
|
||||
{
|
||||
ReconciliationResult result = accessAuthorizationOrchestrator.reconcile(enrollmentId);
|
||||
return success(result);
|
||||
}
|
||||
|
||||
private String sanitizeSecret(String input)
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return input.replaceAll("(?i)(password|token|secret|appsecret)=[^&\\s]+", "$1=[PROTECTED]")
|
||||
.replaceAll("(?i)(\"password\"\\s*:\\s*\")[^\"]+(\")", "$1[PROTECTED]$2");
|
||||
}
|
||||
|
||||
private String getUsernameSafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
return SecurityUtils.getUsername();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return "admin";
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 010 DMS 门禁与外部资源映射管理权限 Migration 契约测试 (Wave 3B Gate M)
|
||||
*/
|
||||
public class DmsAccessManagementPermissionMigrationContractTest
|
||||
{
|
||||
@Test
|
||||
public void migrationClosesAccessManagementPermissionsWithoutRoleWrites() throws Exception
|
||||
{
|
||||
String sql = new String(Files.readAllBytes(findMigration()), StandardCharsets.UTF_8).toLowerCase();
|
||||
|
||||
// 必须包含新增的核心权限标识与幂等子句
|
||||
assertContains(sql,
|
||||
"2191", "train:access:query",
|
||||
"2192", "train:access:manage",
|
||||
"2193", "train:access:reconcile",
|
||||
"menu_type", "'f'", "where not exists");
|
||||
|
||||
// 严禁修改角色权限映射 (角色授权由后续 RBAC 统一分配)
|
||||
assertFalse("migration must not insert role assignments", sql.contains("insert into public.sys_role_menu"));
|
||||
assertFalse("migration must not update role assignments", sql.contains("update public.sys_role_menu"));
|
||||
assertFalse("migration must not delete role assignments", sql.contains("delete from public.sys_role_menu"));
|
||||
|
||||
// 严禁包含高危远程开锁权限或泛化无约束权限
|
||||
assertFalse("must not create remote unlock permission", sql.contains("remoteunlock") || sql.contains("remote-unlock"));
|
||||
assertFalse("must not contain plaintext password column", sql.contains("password") || sql.contains("secret"));
|
||||
}
|
||||
|
||||
private Path findMigration()
|
||||
{
|
||||
for (Path path = Paths.get("").toAbsolutePath(); path != null; path = path.getParent())
|
||||
{
|
||||
Path candidate = path.resolve("sql/system/kingbase/010_dms_access_management_permissions.sql");
|
||||
if (Files.isRegularFile(candidate)) return candidate;
|
||||
}
|
||||
throw new IllegalStateException("010_dms_access_management_permissions.sql migration not found from working directory");
|
||||
}
|
||||
|
||||
private void assertContains(String sql, String... tokens)
|
||||
{
|
||||
for (String token : tokens)
|
||||
{
|
||||
assertTrue("missing migration token: " + token, sql.contains(token));
|
||||
}
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
package com.ruoyi.web.controller.train;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.train.domain.TrainDormAuth;
|
||||
import com.ruoyi.train.domain.TrainExternalOperation;
|
||||
import com.ruoyi.train.domain.TrainExternalResourceMapping;
|
||||
import com.ruoyi.train.integration.dms.orchestration.AccessAuthorizationOrchestrator;
|
||||
import com.ruoyi.train.integration.dms.orchestration.ReconciliationDecision;
|
||||
import com.ruoyi.train.integration.dms.orchestration.ReconciliationResult;
|
||||
import com.ruoyi.train.service.IExternalOperationService;
|
||||
import com.ruoyi.train.service.IExternalResourceMappingService;
|
||||
import com.ruoyi.train.service.ITrainDormAuthService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* 门禁与外部资源映射管理控制器自动化测试 (Wave 3B)
|
||||
*/
|
||||
public class TrainAccessManagementControllerTest
|
||||
{
|
||||
private TrainAccessManagementController controller;
|
||||
|
||||
private List<TrainExternalResourceMapping> mappingDb;
|
||||
private List<TrainDormAuth> dormAuthDb;
|
||||
private List<TrainExternalOperation> operationDb;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
controller = new TrainAccessManagementController();
|
||||
mappingDb = new ArrayList<>();
|
||||
dormAuthDb = new ArrayList<>();
|
||||
operationDb = new ArrayList<>();
|
||||
|
||||
IExternalResourceMappingService mappingService = (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 mappingDb.stream()
|
||||
.filter(m -> dormId.equals(m.getDormId()) && "ROOM".equals(m.getExternalResourceType()) && "ACTIVE".equals(m.getMappingStatus()))
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
if ("getActiveLockMappings".equals(name)) {
|
||||
String dormId = (String) args[1];
|
||||
List<TrainExternalResourceMapping> locks = new ArrayList<>();
|
||||
for (TrainExternalResourceMapping m : mappingDb) {
|
||||
if (dormId.equals(m.getDormId()) && "DOOR_LOCK".equals(m.getExternalResourceType()) && "ACTIVE".equals(m.getMappingStatus())) {
|
||||
locks.add(m);
|
||||
}
|
||||
}
|
||||
return locks;
|
||||
}
|
||||
if ("bindRoomMapping".equals(name)) {
|
||||
TrainExternalResourceMapping m = (TrainExternalResourceMapping) args[0];
|
||||
m.setMappingId(101L);
|
||||
m.setMappingStatus("ACTIVE");
|
||||
mappingDb.add(m);
|
||||
return m;
|
||||
}
|
||||
if ("bindLockMapping".equals(name)) {
|
||||
TrainExternalResourceMapping m = (TrainExternalResourceMapping) args[0];
|
||||
m.setMappingId(201L);
|
||||
m.setMappingStatus("ACTIVE");
|
||||
mappingDb.add(m);
|
||||
return m;
|
||||
}
|
||||
if ("deactivateMapping".equals(name)) {
|
||||
Long mappingId = (Long) args[0];
|
||||
mappingDb.removeIf(m -> mappingId.equals(m.getMappingId()));
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
ITrainDormAuthService authService = (ITrainDormAuthService) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{ITrainDormAuthService.class},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("selectTrainDormAuthList".equals(name)) {
|
||||
return dormAuthDb;
|
||||
}
|
||||
if ("selectTrainDormAuthByAuthId".equals(name)) {
|
||||
Long id = (Long) args[0];
|
||||
return dormAuthDb.stream().filter(a -> id.equals(a.getAuthId())).findFirst().orElse(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
IExternalOperationService operationService = (IExternalOperationService) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{IExternalOperationService.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("listOperationsByDoorAuthId".equals(method.getName())) {
|
||||
Long doorAuthId = (Long) args[0];
|
||||
List<TrainExternalOperation> list = new ArrayList<>();
|
||||
for (TrainExternalOperation op : operationDb) {
|
||||
if (doorAuthId.equals(op.getDoorAuthId())) {
|
||||
list.add(op);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
AccessAuthorizationOrchestrator orchestrator = new AccessAuthorizationOrchestrator() {
|
||||
@Override
|
||||
public ReconciliationResult reconcile(Long enrollmentId) {
|
||||
if (enrollmentId == 999L) {
|
||||
return ReconciliationResult.waiting("Prerequisites missing: enrollment not reported");
|
||||
}
|
||||
return ReconciliationResult.grantRequired(10L, 501L, true, true, "Triggered by admin manual reconcile");
|
||||
}
|
||||
};
|
||||
|
||||
setField(controller, "externalResourceMappingService", mappingService);
|
||||
setField(controller, "trainDormAuthService", authService);
|
||||
setField(controller, "externalOperationService", operationService);
|
||||
setField(controller, "accessAuthorizationOrchestrator", orchestrator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试房间门锁映射查询及主锁配置健康度判断:
|
||||
* 1. 0主锁 -> configError
|
||||
* 2. 1主锁 -> isConfigValid = true
|
||||
* 3. 2主锁 -> configError
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testGetDormMapping_HealthAndPrimaryPolicy()
|
||||
{
|
||||
String dormId = "DORM-301";
|
||||
|
||||
// 1. 无任何映射 -> 0 primary
|
||||
AjaxResult r1 = controller.getDormMapping(dormId);
|
||||
assertEquals(200, r1.get("code"));
|
||||
Map<String, Object> data1 = (Map<String, Object>) r1.get("data");
|
||||
assertEquals(0L, data1.get("primaryLockCount"));
|
||||
assertFalse((Boolean) data1.get("isConfigValid"));
|
||||
assertNotNull(data1.get("configError"));
|
||||
assertTrue(data1.get("configError").toString().contains("0 primary lock"));
|
||||
|
||||
// 2. 绑定 1 个房间与 1 个主门锁 -> 正常有效
|
||||
TrainExternalResourceMapping room = new TrainExternalResourceMapping();
|
||||
room.setDormId(dormId);
|
||||
room.setExternalResourceType("ROOM");
|
||||
room.setMappingStatus("ACTIVE");
|
||||
mappingDb.add(room);
|
||||
|
||||
TrainExternalResourceMapping lock1 = new TrainExternalResourceMapping();
|
||||
lock1.setDormId(dormId);
|
||||
lock1.setExternalResourceType("DOOR_LOCK");
|
||||
lock1.setIsPrimary(1);
|
||||
lock1.setMappingStatus("ACTIVE");
|
||||
mappingDb.add(lock1);
|
||||
|
||||
AjaxResult r2 = controller.getDormMapping(dormId);
|
||||
Map<String, Object> data2 = (Map<String, Object>) r2.get("data");
|
||||
assertEquals(1L, data2.get("primaryLockCount"));
|
||||
assertTrue((Boolean) data2.get("isConfigValid"));
|
||||
assertNull(data2.get("configError"));
|
||||
|
||||
// 3. 误配置了第 2 个主锁 -> 配置异常
|
||||
TrainExternalResourceMapping lock2 = new TrainExternalResourceMapping();
|
||||
lock2.setDormId(dormId);
|
||||
lock2.setExternalResourceType("DOOR_LOCK");
|
||||
lock2.setIsPrimary(1);
|
||||
lock2.setMappingStatus("ACTIVE");
|
||||
mappingDb.add(lock2);
|
||||
|
||||
AjaxResult r3 = controller.getDormMapping(dormId);
|
||||
Map<String, Object> data3 = (Map<String, Object>) r3.get("data");
|
||||
assertEquals(2L, data3.get("primaryLockCount"));
|
||||
assertFalse((Boolean) data3.get("isConfigValid"));
|
||||
assertTrue(data3.get("configError").toString().contains("存在多把主门锁"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试操作流水查询与敏感数据脱敏:
|
||||
* 即使原始错误消息或备注中含有 password 或 token,API 必须将其全局替换为 [PROTECTED]
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testGetOperationHistory_SanitizesSensitiveData()
|
||||
{
|
||||
Long authId = 88L;
|
||||
TrainExternalOperation op = new TrainExternalOperation();
|
||||
op.setOperationId(1001L);
|
||||
op.setDoorAuthId(authId);
|
||||
op.setOperationType("GRANT");
|
||||
op.setOperationStatus("FAILED");
|
||||
op.setLastErrorMessage("DMS responded error: password=PlaintextSecret123&appsecret=TopSecretValue");
|
||||
op.setRemark("Operator tried with token=raw_token_xyz");
|
||||
operationDb.add(op);
|
||||
|
||||
AjaxResult r = controller.getOperationHistory(authId);
|
||||
assertEquals(200, r.get("code"));
|
||||
List<Map<String, Object>> list = (List<Map<String, Object>>) r.get("data");
|
||||
assertEquals(1, list.size());
|
||||
|
||||
Map<String, Object> item = list.get(0);
|
||||
String errMsg = (String) item.get("lastErrorMessage");
|
||||
String remark = (String) item.get("remark");
|
||||
|
||||
assertFalse("Password must not be leaked in error message", errMsg.contains("PlaintextSecret123"));
|
||||
assertFalse("AppSecret must not be leaked in error message", errMsg.contains("TopSecretValue"));
|
||||
assertFalse("Token must not be leaked in remark", remark.contains("raw_token_xyz"));
|
||||
assertTrue("Sensitive text must be replaced with [PROTECTED]", errMsg.contains("[PROTECTED]"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试手动对账接口:继续遵循相同前置条件校验,绝不提供绕过前置条件的强开权限
|
||||
*/
|
||||
@Test
|
||||
public void testManualReconcile_RespectsPrerequisites()
|
||||
{
|
||||
// 1. 未满足条件学员 -> WAITING_FOR_PREREQUISITE
|
||||
AjaxResult r1 = controller.manualReconcile(999L);
|
||||
assertEquals(200, r1.get("code"));
|
||||
ReconciliationResult res1 = (ReconciliationResult) r1.get("data");
|
||||
assertEquals(ReconciliationDecision.WAITING_FOR_PREREQUISITE, res1.getDecision());
|
||||
|
||||
// 2. 满足条件学员 -> GRANT_REQUIRED
|
||||
AjaxResult r2 = controller.manualReconcile(111L);
|
||||
assertEquals(200, r2.get("code"));
|
||||
ReconciliationResult res2 = (ReconciliationResult) r2.get("data");
|
||||
assertEquals(ReconciliationDecision.GRANT_REQUIRED, res2.getDecision());
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全基线红线断言:
|
||||
* 1. 控制器中严禁存在任何名为 remoteUnlock 或类似远程应急开锁方法
|
||||
* 2. 控制器类所有公开方法均无密码明文入参或返回
|
||||
*/
|
||||
@Test
|
||||
public void testSecurityReview_ZeroRemoteUnlockMethod()
|
||||
{
|
||||
for (Method m : TrainAccessManagementController.class.getDeclaredMethods()) {
|
||||
String name = m.getName().toLowerCase();
|
||||
assertFalse("Remote unlock is strictly prohibited in controller: " + name,
|
||||
name.contains("remoteunlock") || name.contains("remote_unlock") || name.contains("forceunlock"));
|
||||
}
|
||||
}
|
||||
|
||||
private void setField(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