test(health): 告警系统集成测试 — 8 个测试覆盖规则 CRUD/引擎评估/状态流转/cooldown/租户隔离
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

This commit is contained in:
iven
2026-04-27 21:48:12 +08:00
parent 3aaa0a9598
commit 55a7d7a03e
2 changed files with 269 additions and 0 deletions

View File

@@ -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;

View 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, "不同租户不应看到告警");
}