test(health): 设备读数集成测试 — 8 个测试覆盖批量摄入/设备绑定/聚合/查询/校验/租户隔离
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:54:50 +08:00
parent 55a7d7a03e
commit dc09cc4e2a
2 changed files with 299 additions and 0 deletions

View File

@@ -20,3 +20,5 @@ mod health_points_tests;
mod health_dialysis_tests;
#[path = "integration/health_alert_tests.rs"]
mod health_alert_tests;
#[path = "integration/health_device_reading_tests.rs"]
mod health_device_reading_tests;

View File

@@ -0,0 +1,297 @@
//! erp-health 设备读数集成测试
//!
//! 验证批量摄入、设备绑定自动创建、hourly 聚合、查询过滤、参数校验、租户隔离。
use erp_health::service::device_reading_service::{
BatchReadingRequest, ReadingInput,
};
use erp_health::service::device_reading_service;
use chrono::Datelike;
use sea_orm::ConnectionTrait;
use super::test_fixture::TestApp;
/// 确保当前月份有 device_readings 分区(测试用的 measured_at 是过去时间)
async fn ensure_current_month_partition(app: &TestApp) {
let now = chrono::Utc::now();
let year = now.format("%Y").to_string();
let month = now.format("%m").to_string();
let suffix = format!("{}_{}", year, month);
let start = format!("{}-{}-01", year, month);
// 计算下个月第一天
let next_month = if now.month() == 12 {
format!("{}-01-01", year.parse::<i32>().unwrap() + 1)
} else {
format!("{}-{:02}-01", year, now.month() as u32 + 1)
};
let sql = format!(
"CREATE TABLE IF NOT EXISTS device_readings_{suffix} PARTITION OF device_readings FOR VALUES FROM ('{start}') TO ('{next_month}');"
);
app.db().execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
sql,
)).await.expect("创建分区应成功");
}
/// 构建一条心率读数measured_at 用几分钟前的时间)
fn heart_rate_reading(bpm: i64, minutes_ago: i64) -> ReadingInput {
let measured_at = (chrono::Utc::now() - chrono::Duration::minutes(minutes_ago)).to_rfc3339();
ReadingInput {
device_type: "heart_rate".to_string(),
values: serde_json::json!({"heart_rate": bpm}),
measured_at,
}
}
// ---------------------------------------------------------------------------
// 测试 1: 批量摄入 — 单条读数成功
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_batch_single() {
let app = TestApp::new().await;
ensure_current_month_partition(&app).await;
let patient_id = app.create_patient("读数患者").await;
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "watch-001".to_string(),
device_model: Some("Apple Watch".to_string()),
readings: vec![heart_rate_reading(75, 5)],
},
)
.await
.expect("摄入应成功");
assert_eq!(result.accepted, 1);
assert_eq!(result.duplicates, 0);
assert!(result.earliest.is_some());
assert!(result.latest.is_some());
}
// ---------------------------------------------------------------------------
// 测试 2: 批量摄入 — 多条读数
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_batch_multiple() {
let app = TestApp::new().await;
ensure_current_month_partition(&app).await;
let patient_id = app.create_patient("批量患者").await;
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "watch-002".to_string(),
device_model: None,
readings: vec![
heart_rate_reading(70, 30),
heart_rate_reading(72, 20),
heart_rate_reading(68, 10),
],
},
)
.await
.expect("批量摄入应成功");
assert_eq!(result.accepted, 3);
}
// ---------------------------------------------------------------------------
// 测试 3: 设备绑定自动创建
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_creates_device_binding() {
let app = TestApp::new().await;
ensure_current_month_partition(&app).await;
let patient_id = app.create_patient("绑定患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "band-001".to_string(),
device_model: Some("Mi Band".to_string()),
readings: vec![heart_rate_reading(80, 5)],
},
)
.await
.unwrap();
// 再次使用同一设备,应更新 last_sync_at 而非重复创建
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "band-001".to_string(),
device_model: Some("Mi Band".to_string()),
readings: vec![heart_rate_reading(82, 2)],
},
)
.await
.expect("重复绑定应成功");
assert_eq!(result.accepted, 1);
}
// ---------------------------------------------------------------------------
// 测试 4: hourly 聚合生成
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_hourly_aggregation() {
let app = TestApp::new().await;
ensure_current_month_partition(&app).await;
let patient_id = app.create_patient("聚合患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "watch-003".to_string(),
device_model: None,
readings: vec![
heart_rate_reading(60, 25),
heart_rate_reading(70, 20),
heart_rate_reading(80, 15),
],
},
)
.await
.expect("摄入应成功");
// 查询 hourly 聚合
let hourly = device_reading_service::query_hourly_readings(
app.health_state(), app.tenant_id(), patient_id, "heart_rate", 1, 1, 20,
)
.await
.expect("查询 hourly 应成功");
assert!(hourly.total > 0, "应有聚合记录");
let rec = &hourly.data[0];
assert_eq!(rec.device_type, "heart_rate");
assert!(rec.avg_val > 0.0);
assert!(rec.sample_count > 0);
}
// ---------------------------------------------------------------------------
// 测试 5: 查询读数 — 按类型过滤
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_query_filter() {
let app = TestApp::new().await;
ensure_current_month_partition(&app).await;
let patient_id = app.create_patient("查询患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "watch-004".to_string(),
device_model: None,
readings: vec![
heart_rate_reading(72, 5),
heart_rate_reading(74, 3),
],
},
)
.await
.unwrap();
let readings = device_reading_service::query_device_readings(
app.health_state(), app.tenant_id(), patient_id, Some("heart_rate"), None, 1, 20,
)
.await
.expect("查询应成功");
assert_eq!(readings.total, 2);
assert_eq!(readings.data[0].device_type, "heart_rate");
}
// ---------------------------------------------------------------------------
// 测试 6: 无效 device_type 返回错误
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_invalid_device_type() {
let app = TestApp::new().await;
let patient_id = app.create_patient("校验患者").await;
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "bad-001".to_string(),
device_model: None,
readings: vec![ReadingInput {
device_type: "invalid_type".to_string(),
values: serde_json::json!(42),
measured_at: chrono::Utc::now().to_rfc3339(),
}],
},
)
.await;
assert!(result.is_err(), "无效 device_type 应返回错误");
}
// ---------------------------------------------------------------------------
// 测试 7: 未来时间拒绝
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_future_time_rejected() {
let app = TestApp::new().await;
let patient_id = app.create_patient("未来患者").await;
let future_time = (chrono::Utc::now() + chrono::Duration::hours(1)).to_rfc3339();
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "watch-005".to_string(),
device_model: None,
readings: vec![ReadingInput {
device_type: "heart_rate".to_string(),
values: serde_json::json!({"heart_rate": 80}),
measured_at: future_time,
}],
},
)
.await;
assert!(result.is_err(), "未来时间应被拒绝");
}
// ---------------------------------------------------------------------------
// 测试 8: 无效患者返回错误 + 租户隔离
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_device_reading_invalid_patient_and_isolation() {
let app = TestApp::new().await;
ensure_current_month_partition(&app).await;
// 无效患者
let fake_patient = uuid::Uuid::new_v4();
let result = device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), fake_patient,
BatchReadingRequest {
device_id: "watch-006".to_string(),
device_model: None,
readings: vec![heart_rate_reading(75, 5)],
},
)
.await;
assert!(result.is_err(), "无效患者应返回错误");
// 租户隔离:创建患者并摄入数据,用不同租户查询
let patient_id = app.create_patient("隔离患者").await;
device_reading_service::batch_create_readings(
app.health_state(), app.tenant_id(), patient_id,
BatchReadingRequest {
device_id: "watch-007".to_string(),
device_model: None,
readings: vec![heart_rate_reading(75, 5)],
},
)
.await
.unwrap();
let other_tenant = uuid::Uuid::new_v4();
let readings = device_reading_service::query_device_readings(
app.health_state(), other_tenant, patient_id, None, None, 1, 20,
)
.await
.expect("查询应成功");
assert_eq!(readings.total, 0, "不同租户不应看到读数");
}