Files
hms/crates/erp-server/tests/integration/health_alert_tests.rs
iven 55a7d7a03e
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
test(health): 告警系统集成测试 — 8 个测试覆盖规则 CRUD/引擎评估/状态流转/cooldown/租户隔离
2026-04-27 21:48:12 +08:00

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