//! 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(), "软删除后查询应返回错误"); }