- HealthState.crypto: HealthCrypto → PiiCrypto (erp-core) - create_patient: 加密 phone/allergy/medical_history + HMAC 索引 - update_patient: 同上,同步加密 - model_to_resp_decrypted: 解密所有 Tier 1 字段 - model_to_resp (列表): Tier 1 字段返回 None - list_patients 搜索: 新增 phone hash 精确搜索 - article handler: 适配新 list_articles 签名 - article 迁移: 添加 category_id 列 - error.rs: From<String> for HealthError - 集成测试: HealthCrypto → PiiCrypto::dev_default()
209 lines
7.0 KiB
Rust
209 lines
7.0 KiB
Rust
//! erp-health 患者管理集成测试
|
|
//!
|
|
//! 验证患者 CRUD、租户隔离、字段校验、软删除等核心行为。
|
|
//! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。
|
|
|
|
use erp_core::events::EventBus;
|
|
use erp_health::dto::patient_dto::CreatePatientReq;
|
|
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(), "软删除后查询应返回错误");
|
|
}
|