Files
hms/crates/erp-server/tests/integration/health_device_reading_tests.rs
iven 6d5a711d2c
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
fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
2026-05-07 23:43:14 +08:00

334 lines
11 KiB
Rust
Raw Permalink 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 chrono::Datelike;
use erp_health::service::device_reading_service;
use erp_health::service::device_reading_service::{BatchReadingRequest, ReadingInput};
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, "不同租户不应看到读数");
}