Files
hms/crates/erp-server/tests/integration/health_appointment_tests.rs
iven e6f036eaf4
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
feat(health): patient_service 集成 PiiCrypto — 电话/过敏史/病史加密
- 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()
2026-04-26 10:37:52 +08:00

249 lines
8.3 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、租户隔离、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(),
"跨租户查询预约应返回错误"
);
}