test(health): 设备读数集成测试 — 8 个测试覆盖批量摄入/设备绑定/聚合/查询/校验/租户隔离
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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, "不同租户不应看到读数");
|
||||
}
|
||||
Reference in New Issue
Block a user