功能修复: 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 统一格式化
369 lines
11 KiB
Rust
369 lines
11 KiB
Rust
//! erp-dialysis 透析记录集成测试
|
||
//!
|
||
//! 验证透析 CRUD、状态流转、PII 加密、乐观锁、租户隔离等核心行为。
|
||
|
||
use chrono::{NaiveDate, NaiveTime};
|
||
use erp_dialysis::dto::dialysis_dto::*;
|
||
use erp_dialysis::service::dialysis_service;
|
||
|
||
use super::test_fixture::TestApp;
|
||
|
||
fn default_create_req(patient_id: uuid::Uuid) -> CreateDialysisRecordReq {
|
||
CreateDialysisRecordReq {
|
||
patient_id,
|
||
dialysis_date: NaiveDate::from_ymd_opt(2026, 5, 1).unwrap(),
|
||
start_time: Some(NaiveTime::from_hms_opt(8, 0, 0).unwrap()),
|
||
end_time: Some(NaiveTime::from_hms_opt(12, 0, 0).unwrap()),
|
||
dry_weight: Some(65.0),
|
||
pre_weight: Some(68.0),
|
||
post_weight: Some(65.5),
|
||
pre_bp_systolic: Some(140),
|
||
pre_bp_diastolic: Some(90),
|
||
post_bp_systolic: Some(130),
|
||
post_bp_diastolic: Some(85),
|
||
pre_heart_rate: Some(80),
|
||
post_heart_rate: Some(75),
|
||
ultrafiltration_volume: Some(2500),
|
||
dialysis_duration: Some(240),
|
||
blood_flow_rate: Some(300),
|
||
dialysis_type: "HD".to_string(),
|
||
symptoms: None,
|
||
complication_notes: Some("无并发症".to_string()),
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 1: 创建透析记录基本 CRUD
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_create_basic() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("透析患者").await;
|
||
|
||
let req = default_create_req(patient_id);
|
||
let record = dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
req,
|
||
)
|
||
.await
|
||
.expect("创建透析记录应成功");
|
||
|
||
assert_eq!(record.patient_id, patient_id);
|
||
assert_eq!(record.dialysis_type, "HD");
|
||
assert_eq!(record.status, "draft");
|
||
assert_eq!(record.ultrafiltration_volume, Some(2500));
|
||
|
||
// 读取
|
||
let fetched =
|
||
dialysis_service::get_dialysis_record(app.dialysis_state(), app.tenant_id(), record.id)
|
||
.await
|
||
.expect("查询应成功");
|
||
assert_eq!(fetched.id, record.id);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 2: PII 字段加密存储
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_create_pii_encrypted() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("PII患者").await;
|
||
|
||
let mut req = default_create_req(patient_id);
|
||
req.symptoms = Some(serde_json::json!({"头晕": "轻度", "恶心": "无"}));
|
||
req.complication_notes = Some("低血压发作".to_string());
|
||
|
||
let record = dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
req,
|
||
)
|
||
.await
|
||
.expect("创建应成功");
|
||
|
||
// 通过 service 读取应自动解密
|
||
assert!(record.symptoms.is_some());
|
||
assert_eq!(record.complication_notes, Some("低血压发作".to_string()));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 3: 状态流转 pending → reviewed
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_update_status_flow() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("状态流转患者").await;
|
||
|
||
let record = dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(patient_id),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(record.status, "draft");
|
||
|
||
// 先将状态推进到 completed(draft → completed → reviewed)
|
||
use erp_dialysis::entity::dialysis_record;
|
||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||
let _ = dialysis_record::Entity::update_many()
|
||
.col_expr(
|
||
dialysis_record::Column::Status,
|
||
sea_orm::sea_query::Expr::val("completed").into(),
|
||
)
|
||
.col_expr(
|
||
dialysis_record::Column::Version,
|
||
sea_orm::sea_query::Expr::val(record.version + 1).into(),
|
||
)
|
||
.filter(dialysis_record::Column::Id.eq(record.id))
|
||
.exec(app.db())
|
||
.await
|
||
.expect("状态推进应成功");
|
||
|
||
// 审核: completed → reviewed
|
||
let reviewed = dialysis_service::review_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
record.id,
|
||
app.operator_id(),
|
||
record.version + 1,
|
||
)
|
||
.await
|
||
.expect("审核应成功");
|
||
assert_eq!(reviewed.status, "reviewed");
|
||
assert!(reviewed.reviewed_by.is_some());
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 4: 按患者 ID 过滤列表
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_list_by_patient() {
|
||
let app = TestApp::new().await;
|
||
let patient_a = app.create_patient("列表患者A").await;
|
||
let patient_b = app.create_patient("列表患者B").await;
|
||
|
||
dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(patient_a),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
|
||
dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(patient_a),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
|
||
dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(patient_b),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
|
||
let list_a = dialysis_service::list_dialysis_records(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
patient_a,
|
||
1,
|
||
20,
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(list_a.total, 2);
|
||
|
||
let list_b = dialysis_service::list_dialysis_records(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
patient_b,
|
||
1,
|
||
20,
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(list_b.total, 1);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 5: 租户隔离
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_tenant_isolation() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("隔离患者").await;
|
||
|
||
let record = dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(patient_id),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
|
||
// 用不同 tenant_id 查询应失败
|
||
let other_tenant = uuid::Uuid::new_v4();
|
||
let result =
|
||
dialysis_service::get_dialysis_record(app.dialysis_state(), other_tenant, record.id).await;
|
||
assert!(result.is_err(), "不同租户不应看到此记录");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 6: 乐观锁 — 旧版本更新拒绝
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_version_conflict() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("乐观锁患者").await;
|
||
|
||
let record = dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(patient_id),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
|
||
// 用正确版本更新
|
||
let updated = dialysis_service::update_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
record.id,
|
||
Some(app.operator_id()),
|
||
UpdateDialysisRecordReq {
|
||
dialysis_date: None,
|
||
start_time: None,
|
||
end_time: None,
|
||
dry_weight: None,
|
||
pre_weight: None,
|
||
post_weight: None,
|
||
pre_bp_systolic: None,
|
||
pre_bp_diastolic: None,
|
||
post_bp_systolic: None,
|
||
post_bp_diastolic: None,
|
||
pre_heart_rate: None,
|
||
post_heart_rate: None,
|
||
ultrafiltration_volume: None,
|
||
dialysis_duration: None,
|
||
blood_flow_rate: None,
|
||
dialysis_type: Some("HDF".to_string()),
|
||
symptoms: None,
|
||
complication_notes: None,
|
||
},
|
||
record.version,
|
||
)
|
||
.await
|
||
.expect("更新应成功");
|
||
assert_eq!(updated.dialysis_type, "HDF");
|
||
|
||
// 用旧版本更新应失败
|
||
let result = dialysis_service::update_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
record.id,
|
||
Some(app.operator_id()),
|
||
UpdateDialysisRecordReq {
|
||
dialysis_date: None,
|
||
start_time: None,
|
||
end_time: None,
|
||
dry_weight: None,
|
||
pre_weight: None,
|
||
post_weight: None,
|
||
pre_bp_systolic: None,
|
||
pre_bp_diastolic: None,
|
||
post_bp_systolic: None,
|
||
post_bp_diastolic: None,
|
||
pre_heart_rate: None,
|
||
post_heart_rate: None,
|
||
ultrafiltration_volume: None,
|
||
dialysis_duration: None,
|
||
blood_flow_rate: None,
|
||
dialysis_type: Some("HD".to_string()),
|
||
symptoms: None,
|
||
complication_notes: None,
|
||
},
|
||
record.version, // 旧版本
|
||
)
|
||
.await;
|
||
assert!(result.is_err(), "旧版本更新应失败");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 7: 软删除后不可见
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_soft_delete() {
|
||
let app = TestApp::new().await;
|
||
let patient_id = app.create_patient("软删除患者").await;
|
||
|
||
let record = dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(patient_id),
|
||
)
|
||
.await
|
||
.unwrap();
|
||
|
||
// 删除
|
||
dialysis_service::delete_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
record.id,
|
||
Some(app.operator_id()),
|
||
record.version,
|
||
)
|
||
.await
|
||
.expect("删除应成功");
|
||
|
||
// 查询应失败
|
||
let result =
|
||
dialysis_service::get_dialysis_record(app.dialysis_state(), app.tenant_id(), record.id)
|
||
.await;
|
||
assert!(result.is_err(), "软删除后应不可见");
|
||
|
||
// 列表中不应出现
|
||
let list = dialysis_service::list_dialysis_records(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
patient_id,
|
||
1,
|
||
20,
|
||
)
|
||
.await
|
||
.unwrap();
|
||
assert_eq!(list.total, 0);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 8: 无效患者 ID 返回错误
|
||
// ---------------------------------------------------------------------------
|
||
#[tokio::test]
|
||
async fn test_dialysis_create_without_patient_returns_error() {
|
||
let app = TestApp::new().await;
|
||
let fake_patient = uuid::Uuid::new_v4();
|
||
|
||
let result = dialysis_service::create_dialysis_record(
|
||
app.dialysis_state(),
|
||
app.tenant_id(),
|
||
Some(app.operator_id()),
|
||
default_create_req(fake_patient),
|
||
)
|
||
.await;
|
||
assert!(result.is_err(), "无效患者应返回错误");
|
||
}
|