Files
hms/crates/erp-server/tests/integration/health_appointment_tests.rs
iven 6d5a711d2c
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
fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
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 统一格式化
2026-05-07 23:43:14 +08:00

395 lines
14 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::crypto::PiiCrypto;
use erp_core::events::EventBus;
use erp_health::dto::appointment_dto::{CreateAppointmentReq, UpdateAppointmentStatusReq};
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 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(), "跨租户查询预约应返回错误");
}
// ---------------------------------------------------------------------------
// 测试 4: 预约状态流转 — pending → confirmed → completed
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_appointment_status_flow() {
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, 15).unwrap();
seed_schedule(&state, tenant_id, doctor_id, date).await;
let appt = appointment_service::create_appointment(
&state,
tenant_id,
Some(operator_id),
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,
},
)
.await
.expect("创建预约应成功");
assert_eq!(appt.status, "pending");
// pending → confirmed
let confirmed = appointment_service::update_appointment_status(
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "confirmed".to_string(),
cancel_reason: None,
},
appt.version,
)
.await
.expect("确认应成功");
assert_eq!(confirmed.status, "confirmed");
// confirmed → completed
let completed = appointment_service::update_appointment_status(
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "completed".to_string(),
cancel_reason: None,
},
confirmed.version,
)
.await
.expect("完成应成功");
assert_eq!(completed.status, "completed");
}
// ---------------------------------------------------------------------------
// 测试 5: 预约取消 — pending → cancelled
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_appointment_cancel() {
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, 16).unwrap();
seed_schedule(&state, tenant_id, doctor_id, date).await;
let appt = appointment_service::create_appointment(
&state,
tenant_id,
None,
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,
},
)
.await
.expect("创建应成功");
let cancelled = appointment_service::update_appointment_status(
&state,
tenant_id,
appt.id,
None,
UpdateAppointmentStatusReq {
status: "cancelled".to_string(),
cancel_reason: Some("患者临时有事".to_string()),
},
appt.version,
)
.await
.expect("取消应成功");
assert_eq!(cancelled.status, "cancelled");
}
// ---------------------------------------------------------------------------
// 测试 6: 预约乐观锁 — 旧版本更新拒绝
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_appointment_version_conflict() {
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, 17).unwrap();
seed_schedule(&state, tenant_id, doctor_id, date).await;
let appt = appointment_service::create_appointment(
&state,
tenant_id,
Some(operator_id),
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,
},
)
.await
.expect("创建应成功");
// 正确版本确认
let _confirmed = appointment_service::update_appointment_status(
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "confirmed".to_string(),
cancel_reason: None,
},
appt.version,
)
.await
.expect("确认应成功");
// 用旧版本再更新应失败
let result = appointment_service::update_appointment_status(
&state,
tenant_id,
appt.id,
Some(operator_id),
UpdateAppointmentStatusReq {
status: "cancelled".to_string(),
cancel_reason: None,
},
appt.version, // 旧版本
)
.await;
assert!(result.is_err(), "旧版本更新应失败");
}