Files
hms/crates/erp-server/tests/integration/health_device_reading_tests.rs
iven dc09cc4e2a
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 个测试覆盖批量摄入/设备绑定/聚合/查询/校验/租户隔离
2026-04-27 21:54:50 +08:00

298 lines
10 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 设备读数集成测试
//!
//! 验证批量摄入、设备绑定自动创建、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, "不同租户不应看到读数");
}