- 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()
249 lines
8.3 KiB
Rust
249 lines
8.3 KiB
Rust
//! erp-health 预约排班集成测试
|
||
//!
|
||
//! 验证预约 CRUD、租户隔离、CAS 排班名额等核心行为。
|
||
//! 使用 TestDb 创建隔离 PostgreSQL 数据库,直接调用 service 层函数。
|
||
//! 预约创建依赖患者 + 医护档案 + 排班三条前置数据。
|
||
|
||
use erp_core::events::EventBus;
|
||
use erp_health::dto::appointment_dto::CreateAppointmentReq;
|
||
use erp_health::dto::doctor_dto::CreateDoctorReq;
|
||
use erp_health::dto::patient_dto::CreatePatientReq;
|
||
use erp_health::service::{
|
||
appointment_service, doctor_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(),
|
||
}
|
||
}
|
||
|
||
/// 创建患者并返回其 ID
|
||
async fn seed_patient(
|
||
state: &HealthState,
|
||
tenant_id: uuid::Uuid,
|
||
name: &str,
|
||
) -> uuid::Uuid {
|
||
let req = CreatePatientReq {
|
||
name: 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,
|
||
};
|
||
let patient = patient_service::create_patient(state, tenant_id, None, req)
|
||
.await
|
||
.expect("创建患者应成功");
|
||
patient.id
|
||
}
|
||
|
||
/// 创建医护档案并返回其 ID
|
||
async fn seed_doctor(
|
||
state: &HealthState,
|
||
tenant_id: uuid::Uuid,
|
||
name: &str,
|
||
) -> uuid::Uuid {
|
||
let req = CreateDoctorReq {
|
||
user_id: None,
|
||
name: name.to_string(),
|
||
department: Some("内科".to_string()),
|
||
title: Some("主治医师".to_string()),
|
||
specialty: Some("心血管内科".to_string()),
|
||
license_number: None,
|
||
bio: None,
|
||
};
|
||
let doctor = doctor_service::create_doctor(state, tenant_id, None, req)
|
||
.await
|
||
.expect("创建医护档案应成功");
|
||
doctor.id
|
||
}
|
||
|
||
/// 创建排班并返回其 ID
|
||
async fn seed_schedule(
|
||
state: &HealthState,
|
||
tenant_id: uuid::Uuid,
|
||
doctor_id: uuid::Uuid,
|
||
date: chrono::NaiveDate,
|
||
) -> uuid::Uuid {
|
||
let req = erp_health::dto::appointment_dto::CreateScheduleReq {
|
||
doctor_id,
|
||
schedule_date: date,
|
||
period_type: Some("am".to_string()),
|
||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||
end_time: chrono::NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
|
||
max_appointments: 10,
|
||
};
|
||
let schedule = appointment_service::create_schedule(state, tenant_id, None, req)
|
||
.await
|
||
.expect("创建排班应成功");
|
||
schedule.id
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 1: 创建预约
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[tokio::test]
|
||
async fn test_create_appointment() {
|
||
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_id = seed_patient(&state, tenant_id, "预约测试患者").await;
|
||
let doctor_id = seed_doctor(&state, tenant_id, "预约测试医生").await;
|
||
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 10).unwrap();
|
||
seed_schedule(&state, tenant_id, doctor_id, date).await;
|
||
|
||
let req = CreateAppointmentReq {
|
||
patient_id,
|
||
doctor_id: Some(doctor_id),
|
||
appointment_type: Some("outpatient".to_string()),
|
||
appointment_date: date,
|
||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||
end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||
notes: Some("首次就诊".to_string()),
|
||
};
|
||
|
||
let appointment =
|
||
appointment_service::create_appointment(&state, tenant_id, Some(operator_id), req)
|
||
.await
|
||
.expect("创建预约应成功");
|
||
|
||
assert_eq!(appointment.patient_id, patient_id);
|
||
assert_eq!(appointment.doctor_id, Some(doctor_id));
|
||
assert_eq!(appointment.appointment_type, "outpatient");
|
||
assert_eq!(appointment.status, "pending");
|
||
assert_eq!(appointment.version, 1);
|
||
assert_eq!(
|
||
appointment.notes,
|
||
Some("首次就诊".to_string())
|
||
);
|
||
|
||
// 通过 get_appointment 验证存储正确
|
||
let found = appointment_service::get_appointment(&state, tenant_id, appointment.id)
|
||
.await
|
||
.expect("查询预约应成功");
|
||
assert_eq!(found.id, appointment.id);
|
||
assert_eq!(found.status, "pending");
|
||
assert_eq!(found.version, 1);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 2: 列表查询 — 创建 2 条预约后验证分页计数
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[tokio::test]
|
||
async fn test_list_appointments() {
|
||
let test_db = TestDb::new().await;
|
||
let state = make_state(test_db.db());
|
||
let tenant_id = uuid::Uuid::new_v4();
|
||
|
||
let patient_id = seed_patient(&state, tenant_id, "列表测试患者").await;
|
||
let doctor_id = seed_doctor(&state, tenant_id, "列表测试医生").await;
|
||
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 12).unwrap();
|
||
seed_schedule(&state, tenant_id, doctor_id, date).await;
|
||
|
||
// 创建 2 条预约(同一个排班时段,CAS 按排班 start_time 匹配)
|
||
for _i in 0..2 {
|
||
let req = CreateAppointmentReq {
|
||
patient_id,
|
||
doctor_id: Some(doctor_id),
|
||
appointment_type: None,
|
||
appointment_date: date,
|
||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||
end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||
notes: None,
|
||
};
|
||
appointment_service::create_appointment(&state, tenant_id, None, req)
|
||
.await
|
||
.expect("创建预约应成功");
|
||
}
|
||
|
||
let result = appointment_service::list_appointments(
|
||
&state,
|
||
tenant_id,
|
||
1,
|
||
10,
|
||
None,
|
||
None,
|
||
None,
|
||
None,
|
||
)
|
||
.await
|
||
.expect("列表查询应成功");
|
||
|
||
assert_eq!(result.total, 2, "应有 2 条预约记录");
|
||
assert_eq!(result.data.len(), 2, "当前页应返回 2 条");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 测试 3: 租户隔离 — 租户 A 的预约对租户 B 不可见
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[tokio::test]
|
||
async fn test_appointment_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 patient_a = seed_patient(&state, tenant_a, "租户A患者").await;
|
||
let doctor_a = seed_doctor(&state, tenant_a, "租户A医生").await;
|
||
let date = chrono::NaiveDate::from_ymd_opt(2026, 6, 1).unwrap();
|
||
seed_schedule(&state, tenant_a, doctor_a, date).await;
|
||
|
||
let req = CreateAppointmentReq {
|
||
patient_id: patient_a,
|
||
doctor_id: Some(doctor_a),
|
||
appointment_type: None,
|
||
appointment_date: date,
|
||
start_time: chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap(),
|
||
end_time: chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||
notes: None,
|
||
};
|
||
let appointment_a =
|
||
appointment_service::create_appointment(&state, tenant_a, None, req)
|
||
.await
|
||
.expect("租户 A 创建预约应成功");
|
||
|
||
// 租户 B 列表查询应看不到租户 A 的预约
|
||
let result_b = appointment_service::list_appointments(
|
||
&state,
|
||
tenant_b,
|
||
1,
|
||
10,
|
||
None,
|
||
None,
|
||
None,
|
||
None,
|
||
)
|
||
.await
|
||
.expect("租户 B 列表查询应成功");
|
||
assert_eq!(result_b.total, 0, "租户 B 不应看到租户 A 的预约");
|
||
assert!(result_b.data.is_empty());
|
||
|
||
// 租户 B 通过 ID 查询租户 A 的预约应返回错误
|
||
let lookup_result =
|
||
appointment_service::get_appointment(&state, tenant_b, appointment_a.id).await;
|
||
assert!(
|
||
lookup_result.is_err(),
|
||
"跨租户查询预约应返回错误"
|
||
);
|
||
}
|