Files
hms/crates/erp-server/tests/integration/health_alert_tests.rs
iven 6d5a711d2c
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
2026-05-07 23:43:14 +08:00

308 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! erp-health 告警系统集成测试
//!
//! 验证告警规则 CRUD、引擎评估阈值/连续/趋势、告警状态流转、cooldown、租户隔离。
use erp_health::dto::alert_dto::*;
use erp_health::entity::{alert_rules, vital_signs_hourly};
use erp_health::service::{alert_engine, alert_rule_service, alert_service};
use sea_orm::ActiveModelTrait;
use sea_orm::ActiveValue::Set;
use super::test_fixture::TestApp;
/// 创建告警规则(单阈值)
async fn seed_threshold_rule(
app: &TestApp,
device_type: &str,
threshold: f64,
) -> alert_rules::Model {
alert_rule_service::create_rule(
app.health_state(),
app.tenant_id(),
app.operator_id(),
CreateAlertRuleRequest {
name: "高收缩压".to_string(),
description: Some("收缩压超过阈值".to_string()),
device_type: device_type.to_string(),
condition_type: "single_threshold".to_string(),
condition_params: serde_json::json!({"direction": "above", "value": threshold}),
severity: Some("warning".to_string()),
apply_tags: None,
notify_roles: None,
cooldown_minutes: Some(60),
},
)
.await
.expect("创建规则应成功")
}
/// 插入一条 hourly 汇总记录
async fn seed_hourly(app: &TestApp, patient_id: uuid::Uuid, device_type: &str, avg_val: f64) {
let model = vital_signs_hourly::ActiveModel {
id: Set(uuid::Uuid::now_v7()),
tenant_id: Set(app.tenant_id()),
patient_id: Set(patient_id),
device_type: Set(device_type.to_string()),
hour_start: Set(chrono::Utc::now()),
min_val: Set(Some(avg_val - 1.0)),
max_val: Set(Some(avg_val + 1.0)),
avg_val: Set(avg_val),
sample_count: Set(1),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
deleted_at: Set(None),
version: Set(1),
};
model.insert(app.db()).await.expect("插入 hourly 应成功");
}
// ---------------------------------------------------------------------------
// 测试 1: 告警规则 CRUD — 创建 + 列表
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_rule_create_and_list() {
let app = TestApp::new().await;
let rule = seed_threshold_rule(&app, "heart_rate", 140.0).await;
assert_eq!(rule.name, "高收缩压");
assert_eq!(rule.condition_type, "single_threshold");
assert!(rule.is_active);
let (rules, total) =
alert_rule_service::list_rules(app.health_state(), app.tenant_id(), None, 1, 20)
.await
.expect("列表应成功");
assert_eq!(total, 1);
assert_eq!(rules[0].id, rule.id);
}
// ---------------------------------------------------------------------------
// 测试 2: 告警规则停用
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_rule_deactivate() {
let app = TestApp::new().await;
let rule = seed_threshold_rule(&app, "heart_rate", 140.0).await;
assert!(rule.is_active);
let deactivated = alert_rule_service::deactivate_rule(
app.health_state(),
app.tenant_id(),
rule.id,
rule.version,
)
.await
.expect("停用应成功");
assert!(!deactivated.is_active);
}
// ---------------------------------------------------------------------------
// 测试 3: 单阈值引擎评估 — 超阈值触发告警
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_engine_single_threshold_trigger() {
let app = TestApp::new().await;
let patient_id = app.create_patient("阈值患者").await;
seed_threshold_rule(&app, "heart_rate", 140.0).await;
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("评估应成功");
assert_eq!(triggered.len(), 1);
assert_eq!(triggered[0].status, "pending");
assert_eq!(triggered[0].severity, "warning");
}
// ---------------------------------------------------------------------------
// 测试 4: 单阈值引擎评估 — 未超阈值不触发
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_engine_single_threshold_no_trigger() {
let app = TestApp::new().await;
let patient_id = app.create_patient("正常患者").await;
seed_threshold_rule(&app, "heart_rate", 140.0).await;
seed_hourly(&app, patient_id, "heart_rate", 120.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("评估应成功");
assert!(triggered.is_empty());
}
// ---------------------------------------------------------------------------
// 测试 5: cooldown — 重复评估不产生重复告警
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_engine_cooldown_suppresses() {
let app = TestApp::new().await;
let patient_id = app.create_patient("冷却患者").await;
seed_threshold_rule(&app, "heart_rate", 140.0).await;
seed_hourly(&app, patient_id, "heart_rate", 160.0).await;
let first = alert_engine::evaluate_rules(
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("首次评估应成功");
assert_eq!(first.len(), 1);
// 再次评估cooldown 内不应重复
let second = alert_engine::evaluate_rules(
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.expect("二次评估应成功");
assert!(second.is_empty(), "cooldown 内不应重复触发");
}
// ---------------------------------------------------------------------------
// 测试 6: 告警状态流转 — pending → acknowledged → resolved
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_status_flow() {
let app = TestApp::new().await;
let patient_id = app.create_patient("流转患者").await;
seed_threshold_rule(&app, "heart_rate", 140.0).await;
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.unwrap();
let alert = &triggered[0];
assert_eq!(alert.status, "pending");
// pending → acknowledged
let acked = alert_service::acknowledge_alert(
app.health_state(),
app.tenant_id(),
alert.id,
app.operator_id(),
alert.version,
)
.await
.expect("确认应成功");
assert_eq!(acked.status, "acknowledged");
assert!(acked.acknowledged_by.is_some());
// acknowledged → resolved
let resolved =
alert_service::resolve_alert(app.health_state(), app.tenant_id(), acked.id, acked.version)
.await
.expect("解决应成功");
assert_eq!(resolved.status, "resolved");
assert!(resolved.resolved_at.is_some());
}
// ---------------------------------------------------------------------------
// 测试 7: 告警状态流转 — pending → dismissed
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_dismiss_from_pending() {
let app = TestApp::new().await;
let patient_id = app.create_patient("忽略患者").await;
seed_threshold_rule(&app, "heart_rate", 140.0).await;
seed_hourly(&app, patient_id, "heart_rate", 155.0).await;
let triggered = alert_engine::evaluate_rules(
app.health_state(),
app.tenant_id(),
patient_id,
"heart_rate",
)
.await
.unwrap();
let alert = &triggered[0];
let dismissed = alert_service::dismiss_alert(
app.health_state(),
app.tenant_id(),
alert.id,
app.operator_id(),
alert.version,
)
.await
.expect("忽略应成功");
assert_eq!(dismissed.status, "dismissed");
}
// ---------------------------------------------------------------------------
// 测试 8: 告警列表按患者过滤 + 租户隔离
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_alert_list_filter_and_tenant_isolation() {
let app = TestApp::new().await;
let patient_a = app.create_patient("过滤患者A").await;
let patient_b = app.create_patient("过滤患者B").await;
seed_threshold_rule(&app, "heart_rate", 140.0).await;
seed_hourly(&app, patient_a, "heart_rate", 155.0).await;
seed_hourly(&app, patient_b, "heart_rate", 160.0).await;
alert_engine::evaluate_rules(app.health_state(), app.tenant_id(), patient_a, "heart_rate")
.await
.unwrap();
alert_engine::evaluate_rules(app.health_state(), app.tenant_id(), patient_b, "heart_rate")
.await
.unwrap();
// 按患者 A 过滤
let (alerts_a, total_a) = alert_service::list_alerts(
app.health_state(),
app.tenant_id(),
Some(patient_a),
None,
None,
1,
20,
)
.await
.unwrap();
assert_eq!(total_a, 1);
assert_eq!(alerts_a[0].patient_id, patient_a);
// 租户隔离
let other_tenant = uuid::Uuid::new_v4();
let (_alerts_other, total_other) = alert_service::list_alerts(
app.health_state(),
other_tenant,
Some(patient_a),
None,
None,
1,
20,
)
.await
.unwrap();
assert_eq!(total_other, 0, "不同租户不应看到告警");
}