test(health): 告警系统集成测试 — 8 个测试覆盖规则 CRUD/引擎评估/状态流转/cooldown/租户隔离
This commit is contained in:
@@ -18,3 +18,5 @@ mod health_pii_encryption_tests;
|
||||
mod health_points_tests;
|
||||
#[path = "integration/health_dialysis_tests.rs"]
|
||||
mod health_dialysis_tests;
|
||||
#[path = "integration/health_alert_tests.rs"]
|
||||
mod health_alert_tests;
|
||||
|
||||
267
crates/erp-server/tests/integration/health_alert_tests.rs
Normal file
267
crates/erp-server/tests/integration/health_alert_tests.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! 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::ActiveValue::Set;
|
||||
use sea_orm::ActiveModelTrait;
|
||||
|
||||
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()),
|
||||
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, 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, 1, 20,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(total_other, 0, "不同租户不应看到告警");
|
||||
}
|
||||
Reference in New Issue
Block a user