Files
hms/crates/erp-server/tests/integration/health_patient_tests.rs
iven fdbbc47a60
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): 扩展患者集成测试 +3 — 更新乐观锁/PII加密验证/姓名搜索
2026-04-27 21:58:57 +08:00

320 lines
11 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 患者管理集成测试
//!
//! 验证患者 CRUD、租户隔离、字段校验、软删除等核心行为。
//! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。
use erp_core::events::EventBus;
use erp_health::dto::patient_dto::{CreatePatientReq, UpdatePatientReq};
use erp_health::service::patient_service;
use erp_health::state::HealthState;
use erp_core::crypto::PiiCrypto;
use super::test_db::TestDb;
/// 构建测试用 HealthState
fn make_state(db: &sea_orm::DatabaseConnection) -> HealthState {
HealthState {
db: db.clone(),
event_bus: EventBus::new(100),
crypto: PiiCrypto::dev_default(),
}
}
#[tokio::test]
async fn test_create_patient() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
let operator_id = uuid::Uuid::new_v4();
let req = CreatePatientReq {
name: "张三".to_string(),
gender: Some("male".to_string()),
birth_date: Some(chrono::NaiveDate::from_ymd_opt(1990, 1, 15).unwrap()),
blood_type: Some("A".to_string()),
id_number: Some("110101199001151234".to_string()),
allergy_history: Some("青霉素过敏".to_string()),
medical_history_summary: None,
emergency_contact_name: Some("李四".to_string()),
emergency_contact_phone: Some("13800138000".to_string()),
source: Some("offline".to_string()),
notes: None,
};
let patient = patient_service::create_patient(&state, tenant_id, Some(operator_id), req)
.await
.expect("创建患者应成功");
assert_eq!(patient.name, "张三");
assert_eq!(patient.gender, Some("male".to_string()));
assert_eq!(patient.status, "active");
assert_eq!(patient.verification_status, "pending");
assert_eq!(patient.version, 1);
assert!(patient.id_number.is_none(), "列表视图不应返回身份证号明文");
// 通过 get_patient 验证存储正确
let found = patient_service::get_patient(&state, tenant_id, patient.id)
.await
.expect("查询患者应成功");
assert_eq!(found.name, "张三");
assert_eq!(found.gender, Some("male".to_string()));
}
#[tokio::test]
async fn test_list_patients() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
// 创建 2 个患者
for i in 0..2 {
let req = CreatePatientReq {
name: format!("患者{}", i + 1),
gender: if i == 0 { Some("male".to_string()) } else { Some("female".to_string()) },
birth_date: None,
blood_type: None,
id_number: None,
allergy_history: None,
medical_history_summary: None,
emergency_contact_name: None,
emergency_contact_phone: None,
source: None,
notes: None,
};
patient_service::create_patient(&state, tenant_id, None, req)
.await
.expect("创建患者应成功");
}
let result = patient_service::list_patients(&state, tenant_id, 1, 10, None, None)
.await
.expect("列表查询应成功");
assert_eq!(result.total, 2, "应有 2 条患者记录");
assert_eq!(result.data.len(), 2, "当前页应返回 2 条");
}
#[tokio::test]
async fn test_patient_tenant_isolation() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_a = uuid::Uuid::new_v4();
let tenant_b = uuid::Uuid::new_v4();
// 租户 A 创建患者
let req_a = CreatePatientReq {
name: "租户A患者".to_string(),
gender: Some("male".to_string()),
birth_date: None,
blood_type: None,
id_number: None,
allergy_history: None,
medical_history_summary: None,
emergency_contact_name: None,
emergency_contact_phone: None,
source: None,
notes: None,
};
let patient_a = patient_service::create_patient(&state, tenant_a, None, req_a)
.await
.expect("租户 A 创建患者应成功");
// 租户 B 列表查询应看不到租户 A 的患者
let result_b = patient_service::list_patients(&state, tenant_b, 1, 10, None, None)
.await
.expect("租户 B 列表查询应成功");
assert_eq!(result_b.total, 0, "租户 B 不应看到租户 A 的患者");
assert!(result_b.data.is_empty());
// 租户 B 通过 ID 查询租户 A 的患者应返回 PatientNotFound
let lookup_result = patient_service::get_patient(&state, tenant_b, patient_a.id).await;
assert!(
lookup_result.is_err(),
"跨租户查询应返回错误"
);
}
#[tokio::test]
async fn test_patient_validation_gender() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
let req = CreatePatientReq {
name: "无效性别患者".to_string(),
gender: Some("unknown".to_string()), // 不在白名单 ["male", "female", "other"] 中
birth_date: None,
blood_type: None,
id_number: None,
allergy_history: None,
medical_history_summary: None,
emergency_contact_name: None,
emergency_contact_phone: None,
source: None,
notes: None,
};
let result = patient_service::create_patient(&state, tenant_id, None, req).await;
assert!(result.is_err(), "无效性别应返回校验错误");
// 验证错误消息包含字段名
let err_msg = format!("{:#}", result.unwrap_err());
assert!(
err_msg.contains("gender"),
"错误消息应包含 'gender' 字段名,实际: {}",
err_msg
);
}
#[tokio::test]
async fn test_patient_soft_delete() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
// 创建患者
let req = CreatePatientReq {
name: "待删除患者".to_string(),
gender: Some("other".to_string()),
birth_date: None,
blood_type: None,
id_number: None,
allergy_history: None,
medical_history_summary: None,
emergency_contact_name: None,
emergency_contact_phone: None,
source: None,
notes: None,
};
let patient = patient_service::create_patient(&state, tenant_id, None, req)
.await
.expect("创建患者应成功");
// 软删除
patient_service::delete_patient(&state, tenant_id, patient.id, None, patient.version)
.await
.expect("软删除应成功");
// 列表不应包含已软删除的患者
let result = patient_service::list_patients(&state, tenant_id, 1, 10, None, None)
.await
.expect("列表查询应成功");
assert_eq!(result.total, 0, "软删除后列表应为空");
assert!(result.data.is_empty());
// get_patient 也应返回 PatientNotFound
let lookup = patient_service::get_patient(&state, tenant_id, patient.id).await;
assert!(lookup.is_err(), "软删除后查询应返回错误");
}
#[tokio::test]
async fn test_patient_update_and_optimistic_lock() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
let operator_id = uuid::Uuid::new_v4();
let patient = patient_service::create_patient(&state, tenant_id, Some(operator_id), CreatePatientReq {
name: "更新前".to_string(),
gender: Some("male".to_string()),
birth_date: None, blood_type: None, id_number: None,
allergy_history: None, medical_history_summary: None,
emergency_contact_name: None, emergency_contact_phone: None,
source: None, notes: None,
})
.await
.expect("创建应成功");
// 正确版本更新
let updated = patient_service::update_patient(
&state, tenant_id, patient.id, Some(operator_id),
UpdatePatientReq {
name: Some("更新后".to_string()),
gender: None, birth_date: None, blood_type: None,
id_number: None, allergy_history: None,
medical_history_summary: None, emergency_contact_name: None,
emergency_contact_phone: None, source: None, notes: None,
status: None, verification_status: None,
},
patient.version,
)
.await
.expect("更新应成功");
assert_eq!(updated.name, "更新后");
assert_eq!(updated.version, 2);
// 旧版本更新应失败
let result = patient_service::update_patient(
&state, tenant_id, patient.id, Some(operator_id),
UpdatePatientReq {
name: Some("冲突".to_string()),
gender: None, birth_date: None, blood_type: None,
id_number: None, allergy_history: None,
medical_history_summary: None, emergency_contact_name: None,
emergency_contact_phone: None, source: None, notes: None,
status: None, verification_status: None,
},
patient.version, // 旧版本
)
.await;
assert!(result.is_err(), "旧版本更新应失败");
}
#[tokio::test]
async fn test_patient_pii_encrypted() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
let patient = patient_service::create_patient(&state, tenant_id, None, CreatePatientReq {
name: "加密患者".to_string(),
gender: None,
birth_date: None, blood_type: None,
id_number: Some("330102199001011234".to_string()),
allergy_history: Some("花粉过敏".to_string()),
medical_history_summary: Some("高血压".to_string()),
emergency_contact_name: Some("王五".to_string()),
emergency_contact_phone: Some("13900139000".to_string()),
source: None, notes: None,
})
.await
.expect("创建应成功");
// 列表视图应隐藏 id_number
assert!(patient.id_number.is_none(), "列表视图不应返回身份证号");
// get_patient 详情应包含 PII 解密后的字段(部分脱敏)
let detail = patient_service::get_patient(&state, tenant_id, patient.id)
.await
.expect("查询详情应成功");
// id_number 返回脱敏版本
assert!(detail.id_number.is_some(), "详情应返回 id_number脱敏");
assert_eq!(detail.allergy_history, Some("花粉过敏".to_string()));
assert_eq!(detail.emergency_contact_name, Some("王五".to_string()));
}
#[tokio::test]
async fn test_patient_search_by_name() {
let test_db = TestDb::new().await;
let state = make_state(test_db.db());
let tenant_id = uuid::Uuid::new_v4();
for name in &["赵一", "钱二", "孙三"] {
patient_service::create_patient(&state, tenant_id, None, CreatePatientReq {
name: name.to_string(),
gender: None, birth_date: None, blood_type: None, id_number: None,
allergy_history: None, medical_history_summary: None,
emergency_contact_name: None, emergency_contact_phone: None,
source: None, notes: None,
})
.await
.unwrap();
}
let result = patient_service::list_patients(&state, tenant_id, 1, 10, Some("".to_string()), None)
.await
.expect("搜索应成功");
assert_eq!(result.total, 1);
assert_eq!(result.data[0].name, "钱二");
}