diff --git a/crates/erp-server/tests/integration.rs b/crates/erp-server/tests/integration.rs index 173360f..e01bb6b 100644 --- a/crates/erp-server/tests/integration.rs +++ b/crates/erp-server/tests/integration.rs @@ -30,3 +30,17 @@ mod health_consultation_tests; mod health_data_tests; #[path = "integration/health_article_tests.rs"] mod health_article_tests; +#[path = "integration/health_doctor_tests.rs"] +mod health_doctor_tests; +#[path = "integration/health_diagnosis_tests.rs"] +mod health_diagnosis_tests; +#[path = "integration/health_consent_tests.rs"] +mod health_consent_tests; +#[path = "integration/health_medication_tests.rs"] +mod health_medication_tests; +#[path = "integration/health_dialysis_prescription_tests.rs"] +mod health_dialysis_prescription_tests; +#[path = "integration/health_follow_up_template_tests.rs"] +mod health_follow_up_template_tests; +#[path = "integration/health_daily_monitoring_tests.rs"] +mod health_daily_monitoring_tests; diff --git a/crates/erp-server/tests/integration/health_consent_tests.rs b/crates/erp-server/tests/integration/health_consent_tests.rs new file mode 100644 index 0000000..dce2c56 --- /dev/null +++ b/crates/erp-server/tests/integration/health_consent_tests.rs @@ -0,0 +1,162 @@ +//! erp-health 知情同意集成测试 +//! +//! 验证同意授权、撤销、列表按患者过滤、租户隔离。 + +use erp_health::dto::consent_dto::*; +use erp_health::service::consent_service; + +use super::test_fixture::TestApp; + +fn default_create_consent_req(patient_id: uuid::Uuid) -> CreateConsentReq { + CreateConsentReq { + patient_id, + consent_type: "data_processing".to_string(), + consent_scope: "健康数据采集与处理".to_string(), + expiry_date: None, + consent_method: Some("电子签名".to_string()), + witness_name: None, + notes: None, + } +} + +async fn seed_consent(app: &TestApp, patient_id: uuid::Uuid) -> ConsentResp { + consent_service::grant_consent( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_consent_req(patient_id), + ) + .await + .expect("授权应成功") +} + +// --------------------------------------------------------------------------- +// 测试 1: 授权同意 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consent_grant() { + let app = TestApp::new().await; + let patient_id = app.create_patient("同意患者").await; + + let consent = seed_consent(&app, patient_id).await; + + assert_eq!(consent.patient_id, patient_id); + assert_eq!(consent.consent_type, "data_processing"); + assert_eq!(consent.status, "granted"); + assert!(consent.granted_at.is_some()); + assert_eq!(consent.version, 1); +} + +// --------------------------------------------------------------------------- +// 测试 2: 撤销同意 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consent_revoke() { + let app = TestApp::new().await; + let patient_id = app.create_patient("撤销患者").await; + let consent = seed_consent(&app, patient_id).await; + assert_eq!(consent.status, "granted"); + + let revoked = consent_service::revoke_consent( + app.health_state(), app.tenant_id(), consent.id, + Some(app.operator_id()), + RevokeConsentReq { + notes: Some("患者要求撤销".to_string()), + version: consent.version, + }, + ) + .await + .expect("撤销应成功"); + + assert_eq!(revoked.status, "revoked"); + assert!(revoked.revoked_at.is_some()); + assert_eq!(revoked.version, 2); +} + +// --------------------------------------------------------------------------- +// 测试 3: 列表按患者过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consent_list_by_patient() { + let app = TestApp::new().await; + let patient_a = app.create_patient("同意列表A").await; + let patient_b = app.create_patient("同意列表B").await; + + seed_consent(&app, patient_a).await; + seed_consent(&app, patient_a).await; + seed_consent(&app, patient_b).await; + + let list_a = consent_service::list_consents( + app.health_state(), app.tenant_id(), patient_a, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_a.total, 2); + + let list_b = consent_service::list_consents( + app.health_state(), app.tenant_id(), patient_b, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_b.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 4: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consent_tenant_isolation() { + let app = TestApp::new().await; + let patient_id = app.create_patient("同意隔离患者").await; + seed_consent(&app, patient_id).await; + + let other_tenant = uuid::Uuid::new_v4(); + let list = consent_service::list_consents( + app.health_state(), other_tenant, patient_id, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list.total, 0, "不同租户不应看到同意记录"); +} + +// --------------------------------------------------------------------------- +// 测试 5: 无效患者授权返回错误 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consent_invalid_patient() { + let app = TestApp::new().await; + let fake_patient = uuid::Uuid::new_v4(); + + let result = consent_service::grant_consent( + app.health_state(), app.tenant_id(), None, + default_create_consent_req(fake_patient), + ) + .await; + assert!(result.is_err(), "无效患者应返回错误"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 撤销版本冲突 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_consent_revoke_version_conflict() { + let app = TestApp::new().await; + let patient_id = app.create_patient("同意锁患者").await; + let consent = seed_consent(&app, patient_id).await; + + // 先撤销一次 + consent_service::revoke_consent( + app.health_state(), app.tenant_id(), consent.id, + Some(app.operator_id()), + RevokeConsentReq { notes: None, version: consent.version }, + ) + .await + .unwrap(); + + // 用旧 version 再撤销应失败 + let result = consent_service::revoke_consent( + app.health_state(), app.tenant_id(), consent.id, + Some(app.operator_id()), + RevokeConsentReq { notes: None, version: consent.version }, + ) + .await; + assert!(result.is_err(), "乐观锁冲突应返回错误"); +} diff --git a/crates/erp-server/tests/integration/health_daily_monitoring_tests.rs b/crates/erp-server/tests/integration/health_daily_monitoring_tests.rs new file mode 100644 index 0000000..752b12b --- /dev/null +++ b/crates/erp-server/tests/integration/health_daily_monitoring_tests.rs @@ -0,0 +1,185 @@ +//! erp-health 日常监测集成测试 +//! +//! 验证日常监测 CRUD(委托 vital_signs)、列表按患者过滤、租户隔离。 + +use erp_health::dto::daily_monitoring_dto::*; +use erp_health::service::daily_monitoring_service; + +use super::test_fixture::TestApp; + +fn default_create_req(patient_id: uuid::Uuid) -> CreateDailyMonitoringReq { + CreateDailyMonitoringReq { + patient_id, + record_date: chrono::NaiveDate::from_ymd_opt(2026, 5, 1).unwrap(), + morning_bp_systolic: Some(125), + morning_bp_diastolic: Some(82), + evening_bp_systolic: None, + evening_bp_diastolic: None, + weight: Some(68.5), + blood_sugar: Some(5.2), + fluid_intake: Some(2000), + urine_output: Some(1500), + notes: Some("状态良好".to_string()), + } +} + +async fn seed_monitoring(app: &TestApp, patient_id: uuid::Uuid) -> DailyMonitoringResp { + daily_monitoring_service::create_daily_monitoring( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_req(patient_id), + ) + .await + .expect("创建日常监测应成功") +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建日常监测 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_daily_monitoring_create() { + let app = TestApp::new().await; + let patient_id = app.create_patient("监测患者").await; + + let dm = seed_monitoring(&app, patient_id).await; + + assert_eq!(dm.patient_id, patient_id); + assert_eq!(dm.morning_bp_systolic, Some(125)); + assert_eq!(dm.weight, Some(68.5)); + assert_eq!(dm.version, 1); +} + +// --------------------------------------------------------------------------- +// 测试 2: 查询日常监测 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_daily_monitoring_get() { + let app = TestApp::new().await; + let patient_id = app.create_patient("监测查询患者").await; + let dm = seed_monitoring(&app, patient_id).await; + + let fetched = daily_monitoring_service::get_daily_monitoring( + app.health_state(), app.tenant_id(), dm.id, + ) + .await + .expect("查询应成功"); + assert_eq!(fetched.id, dm.id); + assert_eq!(fetched.blood_sugar, Some(5.2)); +} + +// --------------------------------------------------------------------------- +// 测试 3: 更新日常监测 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_daily_monitoring_update() { + let app = TestApp::new().await; + let patient_id = app.create_patient("监测更新患者").await; + let dm = seed_monitoring(&app, patient_id).await; + + let updated = daily_monitoring_service::update_daily_monitoring( + app.health_state(), app.tenant_id(), dm.id, + Some(app.operator_id()), + UpdateDailyMonitoringReq { + weight: Some(67.0), + blood_sugar: None, + morning_bp_systolic: None, + morning_bp_diastolic: None, + evening_bp_systolic: Some(118), + evening_bp_diastolic: Some(76), + fluid_intake: None, + urine_output: None, + notes: None, + record_date: None, + }, + dm.version, + ) + .await + .expect("更新应成功"); + + assert_eq!(updated.weight, Some(67.0)); + assert_eq!(updated.evening_bp_systolic, Some(118)); + assert_eq!(updated.version, 2); +} + +// --------------------------------------------------------------------------- +// 测试 4: 列表按患者过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_daily_monitoring_list_by_patient() { + let app = TestApp::new().await; + let patient_a = app.create_patient("监测列表A").await; + let patient_b = app.create_patient("监测列表B").await; + + seed_monitoring(&app, patient_a).await; + seed_monitoring(&app, patient_b).await; + + let list_a = daily_monitoring_service::list_daily_monitoring( + app.health_state(), app.tenant_id(), patient_a, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_a.total, 1); + + let list_b = daily_monitoring_service::list_daily_monitoring( + app.health_state(), app.tenant_id(), patient_b, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_b.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 5: 软删除 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_daily_monitoring_soft_delete() { + let app = TestApp::new().await; + let patient_id = app.create_patient("监测删除患者").await; + let dm = seed_monitoring(&app, patient_id).await; + + daily_monitoring_service::delete_daily_monitoring( + app.health_state(), app.tenant_id(), dm.id, + Some(app.operator_id()), dm.version, + ) + .await + .expect("删除应成功"); + + let result = daily_monitoring_service::get_daily_monitoring( + app.health_state(), app.tenant_id(), dm.id, + ) + .await; + assert!(result.is_err(), "软删除后查询应失败"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_daily_monitoring_tenant_isolation() { + let app = TestApp::new().await; + let patient_id = app.create_patient("监测隔离患者").await; + seed_monitoring(&app, patient_id).await; + + let other_tenant = uuid::Uuid::new_v4(); + let list = daily_monitoring_service::list_daily_monitoring( + app.health_state(), other_tenant, patient_id, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list.total, 0, "不同租户不应看到日常监测"); +} + +// --------------------------------------------------------------------------- +// 测试 7: 无效患者创建返回错误 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_daily_monitoring_invalid_patient() { + let app = TestApp::new().await; + let fake_patient = uuid::Uuid::new_v4(); + + let result = daily_monitoring_service::create_daily_monitoring( + app.health_state(), app.tenant_id(), None, + default_create_req(fake_patient), + ) + .await; + assert!(result.is_err(), "无效患者应返回错误"); +} diff --git a/crates/erp-server/tests/integration/health_diagnosis_tests.rs b/crates/erp-server/tests/integration/health_diagnosis_tests.rs new file mode 100644 index 0000000..0caa720 --- /dev/null +++ b/crates/erp-server/tests/integration/health_diagnosis_tests.rs @@ -0,0 +1,208 @@ +//! erp-health 诊断记录集成测试 +//! +//! 验证诊断 CRUD、列表按患者过滤、租户隔离、乐观锁。 + +use erp_health::dto::diagnosis_dto::*; +use erp_health::service::diagnosis_service; + +use super::test_fixture::TestApp; + +fn default_create_diagnosis_req() -> CreateDiagnosisReq { + CreateDiagnosisReq { + icd_code: "N18.9".to_string(), + diagnosis_name: "慢性肾脏病".to_string(), + diagnosis_type: "primary".to_string(), + diagnosed_date: chrono::NaiveDate::from_ymd_opt(2026, 4, 15).unwrap(), + status: "active".to_string(), + health_record_id: None, + diagnosed_by: None, + notes: None, + } +} + +async fn seed_diagnosis(app: &TestApp, patient_id: uuid::Uuid, icd_code: &str, name: &str) -> DiagnosisResp { + diagnosis_service::create_diagnosis( + app.health_state(), app.tenant_id(), patient_id, Some(app.operator_id()), + CreateDiagnosisReq { + icd_code: icd_code.to_string(), + diagnosis_name: name.to_string(), + ..default_create_diagnosis_req() + }, + ) + .await + .expect("创建诊断应成功") +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建诊断 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_diagnosis_create() { + let app = TestApp::new().await; + let patient_id = app.create_patient("诊断患者").await; + + let diag = seed_diagnosis(&app, patient_id, "N18.9", "慢性肾脏病").await; + + assert_eq!(diag.patient_id, patient_id); + assert_eq!(diag.icd_code, "N18.9"); + assert_eq!(diag.diagnosis_type, "primary"); + assert_eq!(diag.status, "active"); + assert_eq!(diag.version, 1); +} + +// --------------------------------------------------------------------------- +// 测试 2: 更新诊断 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_diagnosis_update() { + let app = TestApp::new().await; + let patient_id = app.create_patient("诊断更新患者").await; + let diag = seed_diagnosis(&app, patient_id, "N18.8", "CKD更新").await; + + let updated = diagnosis_service::update_diagnosis( + app.health_state(), app.tenant_id(), diag.id, + Some(app.operator_id()), + UpdateDiagnosisReq { + status: Some("chronic".to_string()), + notes: Some("长期随访".to_string()), + icd_code: None, + diagnosis_name: None, + diagnosis_type: None, + diagnosed_date: None, + health_record_id: None, + diagnosed_by: None, + }, + diag.version, + ) + .await + .expect("更新应成功"); + + assert_eq!(updated.status, "chronic"); + assert_eq!(updated.version, 2); +} + +// --------------------------------------------------------------------------- +// 测试 3: 列表按患者过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_diagnosis_list_by_patient() { + let app = TestApp::new().await; + let patient_a = app.create_patient("诊断列表A").await; + let patient_b = app.create_patient("诊断列表B").await; + + seed_diagnosis(&app, patient_a, "N18.1", "CKD 1期").await; + seed_diagnosis(&app, patient_a, "N18.2", "CKD 2期").await; + seed_diagnosis(&app, patient_b, "E11.9", "2型糖尿病").await; + + let list_a = diagnosis_service::list_diagnoses( + app.health_state(), app.tenant_id(), patient_a, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_a.total, 2); + + let list_b = diagnosis_service::list_diagnoses( + app.health_state(), app.tenant_id(), patient_b, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_b.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 4: 软删除 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_diagnosis_soft_delete() { + let app = TestApp::new().await; + let patient_id = app.create_patient("诊断删除患者").await; + let diag = seed_diagnosis(&app, patient_id, "N18.3", "CKD删除").await; + + diagnosis_service::delete_diagnosis( + app.health_state(), app.tenant_id(), diag.id, + Some(app.operator_id()), diag.version, + ) + .await + .expect("删除应成功"); + + let list = diagnosis_service::list_diagnoses( + app.health_state(), app.tenant_id(), patient_id, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list.total, 0); +} + +// --------------------------------------------------------------------------- +// 测试 5: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_diagnosis_tenant_isolation() { + let app = TestApp::new().await; + let patient_id = app.create_patient("诊断隔离患者").await; + seed_diagnosis(&app, patient_id, "N18.4", "CKD隔离").await; + + let other_tenant = uuid::Uuid::new_v4(); + let list = diagnosis_service::list_diagnoses( + app.health_state(), other_tenant, patient_id, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list.total, 0, "不同租户不应看到诊断记录"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 无效患者创建诊断返回错误 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_diagnosis_invalid_patient() { + let app = TestApp::new().await; + let fake_patient = uuid::Uuid::new_v4(); + + let result = diagnosis_service::create_diagnosis( + app.health_state(), app.tenant_id(), fake_patient, None, + default_create_diagnosis_req(), + ) + .await; + assert!(result.is_err(), "无效患者应返回错误"); +} + +// --------------------------------------------------------------------------- +// 测试 7: 乐观锁冲突 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_diagnosis_version_conflict() { + let app = TestApp::new().await; + let patient_id = app.create_patient("诊断锁患者").await; + let diag = seed_diagnosis(&app, patient_id, "N18.5", "CKD锁").await; + + // 先更新一次 + diagnosis_service::update_diagnosis( + app.health_state(), app.tenant_id(), diag.id, + Some(app.operator_id()), + UpdateDiagnosisReq { + status: Some("resolved".to_string()), + icd_code: None, diagnosis_name: None, diagnosis_type: None, + diagnosed_date: None, health_record_id: None, + diagnosed_by: None, notes: None, + }, + diag.version, + ) + .await + .unwrap(); + + // 用旧 version 再更新应失败 + let result = diagnosis_service::update_diagnosis( + app.health_state(), app.tenant_id(), diag.id, + Some(app.operator_id()), + UpdateDiagnosisReq { + status: Some("chronic".to_string()), + icd_code: None, diagnosis_name: None, diagnosis_type: None, + diagnosed_date: None, health_record_id: None, + diagnosed_by: None, notes: None, + }, + diag.version, + ) + .await; + assert!(result.is_err(), "乐观锁冲突应返回错误"); +} diff --git a/crates/erp-server/tests/integration/health_dialysis_prescription_tests.rs b/crates/erp-server/tests/integration/health_dialysis_prescription_tests.rs new file mode 100644 index 0000000..828f25e --- /dev/null +++ b/crates/erp-server/tests/integration/health_dialysis_prescription_tests.rs @@ -0,0 +1,228 @@ +//! erp-health 透析方案管理集成测试 +//! +//! 验证透析方案 CRUD、列表按患者过滤、租户隔离、乐观锁。 + +use erp_health::dto::dialysis_prescription_dto::*; +use erp_health::service::dialysis_prescription_service; + +use super::test_fixture::TestApp; + +fn default_create_req(patient_id: uuid::Uuid) -> CreateDialysisPrescriptionReq { + CreateDialysisPrescriptionReq { + patient_id, + dialyzer_model: Some("FX80".to_string()), + membrane_area: Some(1.8), + dialysate_potassium: Some(2.0), + dialysate_calcium: Some(1.5), + dialysate_bicarbonate: Some(35.0), + anticoagulation_type: Some("heparin".to_string()), + anticoagulation_dose: Some("2000IU".to_string()), + target_ultrafiltration_ml: Some(2000), + target_dry_weight: Some(65.0), + blood_flow_rate: Some(300), + dialysate_flow_rate: Some(500), + frequency_per_week: Some(3), + duration_minutes: Some(240), + vascular_access_type: Some("avf".to_string()), + vascular_access_location: Some("左前臂".to_string()), + effective_from: chrono::NaiveDate::from_ymd_opt(2026, 5, 1), + effective_to: None, + notes: None, + } +} + +async fn seed_prescription(app: &TestApp, patient_id: uuid::Uuid) -> DialysisPrescriptionResp { + dialysis_prescription_service::create_prescription( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_req(patient_id), + ) + .await + .expect("创建透析方案应成功") +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建透析方案 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_dialysis_prescription_create() { + let app = TestApp::new().await; + let patient_id = app.create_patient("透析方案患者").await; + + let rx = seed_prescription(&app, patient_id).await; + + assert_eq!(rx.patient_id, patient_id); + assert_eq!(rx.status, "active"); + assert_eq!(rx.dialyzer_model, Some("FX80".to_string())); + assert_eq!(rx.frequency_per_week, Some(3)); + assert_eq!(rx.version, 1); +} + +// --------------------------------------------------------------------------- +// 测试 2: 查询透析方案 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_dialysis_prescription_get() { + let app = TestApp::new().await; + let patient_id = app.create_patient("方案查询患者").await; + let rx = seed_prescription(&app, patient_id).await; + + let fetched = dialysis_prescription_service::get_prescription( + app.health_state(), app.tenant_id(), rx.id, + ) + .await + .expect("查询应成功"); + assert_eq!(fetched.id, rx.id); +} + +// --------------------------------------------------------------------------- +// 测试 3: 更新透析方案 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_dialysis_prescription_update() { + let app = TestApp::new().await; + let patient_id = app.create_patient("方案更新患者").await; + let rx = seed_prescription(&app, patient_id).await; + + let updated = dialysis_prescription_service::update_prescription( + app.health_state(), app.tenant_id(), rx.id, + Some(app.operator_id()), + UpdateDialysisPrescriptionReq { + blood_flow_rate: Some(350), + frequency_per_week: Some(4), + status: None, + dialyzer_model: None, membrane_area: None, + dialysate_potassium: None, dialysate_calcium: None, + dialysate_bicarbonate: None, anticoagulation_type: None, + anticoagulation_dose: None, target_ultrafiltration_ml: None, + target_dry_weight: None, dialysate_flow_rate: None, + duration_minutes: None, vascular_access_type: None, + vascular_access_location: None, effective_from: None, + effective_to: None, notes: None, + }, + rx.version, + ) + .await + .expect("更新应成功"); + + assert_eq!(updated.blood_flow_rate, Some(350)); + assert_eq!(updated.frequency_per_week, Some(4)); + assert_eq!(updated.version, 2); +} + +// --------------------------------------------------------------------------- +// 测试 4: 列表按患者过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_dialysis_prescription_list_by_patient() { + let app = TestApp::new().await; + let patient_a = app.create_patient("方案列表A").await; + let patient_b = app.create_patient("方案列表B").await; + + seed_prescription(&app, patient_a).await; + seed_prescription(&app, patient_b).await; + + let list_a = dialysis_prescription_service::list_prescriptions( + app.health_state(), app.tenant_id(), 1, 20, Some(patient_a), None, + ) + .await + .unwrap(); + assert_eq!(list_a.total, 1); + + let list_b = dialysis_prescription_service::list_prescriptions( + app.health_state(), app.tenant_id(), 1, 20, Some(patient_b), None, + ) + .await + .unwrap(); + assert_eq!(list_b.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 5: 软删除 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_dialysis_prescription_soft_delete() { + let app = TestApp::new().await; + let patient_id = app.create_patient("方案删除患者").await; + let rx = seed_prescription(&app, patient_id).await; + + dialysis_prescription_service::delete_prescription( + app.health_state(), app.tenant_id(), rx.id, + Some(app.operator_id()), rx.version, + ) + .await + .expect("删除应成功"); + + let result = dialysis_prescription_service::get_prescription( + app.health_state(), app.tenant_id(), rx.id, + ) + .await; + assert!(result.is_err(), "软删除后查询应失败"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_dialysis_prescription_tenant_isolation() { + let app = TestApp::new().await; + let patient_id = app.create_patient("方案隔离患者").await; + seed_prescription(&app, patient_id).await; + + let other_tenant = uuid::Uuid::new_v4(); + let list = dialysis_prescription_service::list_prescriptions( + app.health_state(), other_tenant, 1, 20, None, None, + ) + .await + .unwrap(); + assert_eq!(list.total, 0, "不同租户不应看到方案"); +} + +// --------------------------------------------------------------------------- +// 测试 7: 乐观锁冲突 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_dialysis_prescription_version_conflict() { + let app = TestApp::new().await; + let patient_id = app.create_patient("方案锁患者").await; + let rx = seed_prescription(&app, patient_id).await; + + // 先更新一次 + dialysis_prescription_service::update_prescription( + app.health_state(), app.tenant_id(), rx.id, + Some(app.operator_id()), + UpdateDialysisPrescriptionReq { + blood_flow_rate: Some(350), + status: None, dialyzer_model: None, membrane_area: None, + dialysate_potassium: None, dialysate_calcium: None, + dialysate_bicarbonate: None, anticoagulation_type: None, + anticoagulation_dose: None, target_ultrafiltration_ml: None, + target_dry_weight: None, frequency_per_week: None, + dialysate_flow_rate: None, duration_minutes: None, + vascular_access_type: None, vascular_access_location: None, + effective_from: None, effective_to: None, notes: None, + }, + rx.version, + ) + .await + .unwrap(); + + // 用旧 version 再更新应失败 + let result = dialysis_prescription_service::update_prescription( + app.health_state(), app.tenant_id(), rx.id, + Some(app.operator_id()), + UpdateDialysisPrescriptionReq { + blood_flow_rate: Some(400), + status: None, dialyzer_model: None, membrane_area: None, + dialysate_potassium: None, dialysate_calcium: None, + dialysate_bicarbonate: None, anticoagulation_type: None, + anticoagulation_dose: None, target_ultrafiltration_ml: None, + target_dry_weight: None, frequency_per_week: None, + dialysate_flow_rate: None, duration_minutes: None, + vascular_access_type: None, vascular_access_location: None, + effective_from: None, effective_to: None, notes: None, + }, + rx.version, + ) + .await; + assert!(result.is_err(), "乐观锁冲突应返回错误"); +} diff --git a/crates/erp-server/tests/integration/health_doctor_tests.rs b/crates/erp-server/tests/integration/health_doctor_tests.rs new file mode 100644 index 0000000..8e992c1 --- /dev/null +++ b/crates/erp-server/tests/integration/health_doctor_tests.rs @@ -0,0 +1,241 @@ +//! erp-health 医生管理集成测试 +//! +//! 验证医生 CRUD、列表搜索、租户隔离、乐观锁。 + +use erp_health::dto::doctor_dto::*; +use erp_health::service::doctor_service; + +use super::test_fixture::TestApp; + +fn default_create_doctor_req() -> CreateDoctorReq { + CreateDoctorReq { + user_id: None, + name: "张三".to_string(), + department: Some("肾内科".to_string()), + title: Some("主任医师".to_string()), + specialty: Some("血液透析".to_string()), + license_number: None, + bio: None, + } +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建医生 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_doctor_create() { + let app = TestApp::new().await; + let doctor = doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_doctor_req(), + ) + .await + .expect("创建医生应成功"); + + assert_eq!(doctor.name, "张三"); + assert_eq!(doctor.department, Some("肾内科".to_string())); + assert_eq!(doctor.online_status, "offline"); + assert_eq!(doctor.version, 1); +} + +// --------------------------------------------------------------------------- +// 测试 2: 查询医生 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_doctor_get() { + let app = TestApp::new().await; + let doctor = doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_doctor_req(), + ) + .await + .unwrap(); + + let fetched = doctor_service::get_doctor( + app.health_state(), app.tenant_id(), doctor.id, + ) + .await + .expect("查询应成功"); + assert_eq!(fetched.id, doctor.id); + assert_eq!(fetched.name, "张三"); +} + +// --------------------------------------------------------------------------- +// 测试 3: 更新医生 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_doctor_update() { + let app = TestApp::new().await; + let doctor = doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_doctor_req(), + ) + .await + .unwrap(); + + let updated = doctor_service::update_doctor( + app.health_state(), app.tenant_id(), doctor.id, + Some(app.operator_id()), + UpdateDoctorReq { + name: Some("李四".to_string()), + department: None, + title: Some("副主任医师".to_string()), + specialty: None, + license_number: None, + bio: Some("擅长透析治疗".to_string()), + online_status: Some("online".to_string()), + }, + doctor.version, + ) + .await + .expect("更新应成功"); + + assert_eq!(updated.name, "李四"); + assert_eq!(updated.title, Some("副主任医师".to_string())); + assert_eq!(updated.online_status, "online"); + assert_eq!(updated.version, 2); +} + +// --------------------------------------------------------------------------- +// 测试 4: 医生列表 + 搜索 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_doctor_list_and_search() { + let app = TestApp::new().await; + + doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateDoctorReq { + name: "王医生".to_string(), + department: Some("心内科".to_string()), + ..default_create_doctor_req() + }, + ) + .await + .unwrap(); + + doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateDoctorReq { + name: "赵医生".to_string(), + department: Some("肾内科".to_string()), + ..default_create_doctor_req() + }, + ) + .await + .unwrap(); + + // 全量列表 + let all = doctor_service::list_doctors( + app.health_state(), app.tenant_id(), 1, 20, None, None, None, + ) + .await + .unwrap(); + assert_eq!(all.total, 2); + + // 按科室过滤 + let renal = doctor_service::list_doctors( + app.health_state(), app.tenant_id(), 1, 20, + None, Some("肾内科".to_string()), None, + ) + .await + .unwrap(); + assert_eq!(renal.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 5: 医生软删除 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_doctor_soft_delete() { + let app = TestApp::new().await; + let doctor = doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_doctor_req(), + ) + .await + .unwrap(); + + doctor_service::delete_doctor( + app.health_state(), app.tenant_id(), doctor.id, + Some(app.operator_id()), doctor.version, + ) + .await + .expect("删除应成功"); + + let result = doctor_service::get_doctor( + app.health_state(), app.tenant_id(), doctor.id, + ) + .await; + assert!(result.is_err(), "软删除后查询应失败"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_doctor_tenant_isolation() { + let app = TestApp::new().await; + let doctor = doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_doctor_req(), + ) + .await + .unwrap(); + + let other_tenant = uuid::Uuid::new_v4(); + let result = doctor_service::get_doctor( + app.health_state(), other_tenant, doctor.id, + ) + .await; + assert!(result.is_err(), "不同租户不应看到此医生"); + + let other_list = doctor_service::list_doctors( + app.health_state(), other_tenant, 1, 20, None, None, None, + ) + .await + .unwrap(); + assert_eq!(other_list.total, 0); +} + +// --------------------------------------------------------------------------- +// 测试 7: 乐观锁冲突 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_doctor_version_conflict() { + let app = TestApp::new().await; + let doctor = doctor_service::create_doctor( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_doctor_req(), + ) + .await + .unwrap(); + + // 先更新一次 + doctor_service::update_doctor( + app.health_state(), app.tenant_id(), doctor.id, + Some(app.operator_id()), + UpdateDoctorReq { + name: Some("第一次".to_string()), + department: None, title: None, specialty: None, + license_number: None, bio: None, online_status: None, + }, + doctor.version, + ) + .await + .unwrap(); + + // 用旧 version 再更新应失败 + let result = doctor_service::update_doctor( + app.health_state(), app.tenant_id(), doctor.id, + Some(app.operator_id()), + UpdateDoctorReq { + name: Some("冲突".to_string()), + department: None, title: None, specialty: None, + license_number: None, bio: None, online_status: None, + }, + doctor.version, + ) + .await; + assert!(result.is_err(), "乐观锁冲突应返回错误"); +} diff --git a/crates/erp-server/tests/integration/health_follow_up_template_tests.rs b/crates/erp-server/tests/integration/health_follow_up_template_tests.rs new file mode 100644 index 0000000..d69573a --- /dev/null +++ b/crates/erp-server/tests/integration/health_follow_up_template_tests.rs @@ -0,0 +1,244 @@ +//! erp-health 随访模板集成测试 +//! +//! 验证模板 CRUD + 字段管理、列表过滤、租户隔离、乐观锁。 + +use erp_health::dto::follow_up_template_dto::*; +use erp_health::service::follow_up_template_service; + +use super::test_fixture::TestApp; + +fn default_field_req() -> TemplateFieldReq { + TemplateFieldReq { + label: "血压".to_string(), + field_key: "blood_pressure".to_string(), + field_type: "text".to_string(), + required: true, + options: None, + placeholder: Some("请输入血压值".to_string()), + validation: None, + sort_order: 0, + } +} + +fn default_create_req() -> CreateFollowUpTemplateReq { + CreateFollowUpTemplateReq { + name: "电话随访模板".to_string(), + description: Some("标准电话随访".to_string()), + follow_up_type: "phone".to_string(), + applicable_scope: None, + fields: vec![default_field_req()], + } +} + +async fn seed_template(app: &TestApp) -> FollowUpTemplateResp { + follow_up_template_service::create_template( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_req(), + ) + .await + .expect("创建模板应成功") +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建模板(含字段) +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_template_create_with_fields() { + let app = TestApp::new().await; + let tmpl = seed_template(&app).await; + + assert_eq!(tmpl.name, "电话随访模板"); + assert_eq!(tmpl.follow_up_type, "phone"); + assert_eq!(tmpl.status, "active"); + assert_eq!(tmpl.fields.len(), 1); + assert_eq!(tmpl.fields[0].field_key, "blood_pressure"); + assert_eq!(tmpl.version, 1); +} + +// --------------------------------------------------------------------------- +// 测试 2: 查询模板(含字段) +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_template_get_with_fields() { + let app = TestApp::new().await; + let tmpl = seed_template(&app).await; + + let fetched = follow_up_template_service::get_template( + app.health_state(), app.tenant_id(), tmpl.id, + ) + .await + .expect("查询应成功"); + assert_eq!(fetched.id, tmpl.id); + assert_eq!(fetched.fields.len(), 1); +} + +// --------------------------------------------------------------------------- +// 测试 3: 更新模板(替换字段) +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_template_update_replace_fields() { + let app = TestApp::new().await; + let tmpl = seed_template(&app).await; + + let updated = follow_up_template_service::update_template( + app.health_state(), app.tenant_id(), tmpl.id, + Some(app.operator_id()), + UpdateFollowUpTemplateReq { + name: Some("更新后的模板".to_string()), + fields: Some(vec![ + TemplateFieldReq { + label: "体重".to_string(), + field_key: "weight".to_string(), + field_type: "number".to_string(), + required: false, + options: None, + placeholder: None, + validation: None, + sort_order: 0, + }, + TemplateFieldReq { + label: "症状".to_string(), + field_key: "symptoms".to_string(), + field_type: "textarea".to_string(), + required: true, + options: None, + placeholder: None, + validation: None, + sort_order: 1, + }, + ]), + description: None, + follow_up_type: None, + applicable_scope: None, + status: None, + }, + tmpl.version, + ) + .await + .expect("更新应成功"); + + assert_eq!(updated.name, "更新后的模板"); + assert_eq!(updated.fields.len(), 2); + assert_eq!(updated.version, 2); +} + +// --------------------------------------------------------------------------- +// 测试 4: 列表过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_template_list_filter() { + let app = TestApp::new().await; + + follow_up_template_service::create_template( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + CreateFollowUpTemplateReq { + name: "门诊随访".to_string(), + follow_up_type: "outpatient".to_string(), + fields: vec![], + description: None, + applicable_scope: None, + }, + ) + .await + .unwrap(); + + follow_up_template_service::create_template( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_req(), + ) + .await + .unwrap(); + + // 全量 + let all = follow_up_template_service::list_templates( + app.health_state(), app.tenant_id(), 1, 20, None, None, + ) + .await + .unwrap(); + assert_eq!(all.total, 2); + + // 按类型过滤 + let phone = follow_up_template_service::list_templates( + app.health_state(), app.tenant_id(), 1, 20, + Some("phone".to_string()), None, + ) + .await + .unwrap(); + assert_eq!(phone.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 5: 软删除(含字段) +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_template_soft_delete() { + let app = TestApp::new().await; + let tmpl = seed_template(&app).await; + + follow_up_template_service::delete_template( + app.health_state(), app.tenant_id(), tmpl.id, + Some(app.operator_id()), tmpl.version, + ) + .await + .expect("删除应成功"); + + let result = follow_up_template_service::get_template( + app.health_state(), app.tenant_id(), tmpl.id, + ) + .await; + assert!(result.is_err(), "软删除后查询应失败"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_template_tenant_isolation() { + let app = TestApp::new().await; + seed_template(&app).await; + + let other_tenant = uuid::Uuid::new_v4(); + let list = follow_up_template_service::list_templates( + app.health_state(), other_tenant, 1, 20, None, None, + ) + .await + .unwrap(); + assert_eq!(list.total, 0, "不同租户不应看到模板"); +} + +// --------------------------------------------------------------------------- +// 测试 7: 乐观锁冲突 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_template_version_conflict() { + let app = TestApp::new().await; + let tmpl = seed_template(&app).await; + + // 先更新一次 + follow_up_template_service::update_template( + app.health_state(), app.tenant_id(), tmpl.id, + Some(app.operator_id()), + UpdateFollowUpTemplateReq { + name: Some("第一次".to_string()), + description: None, follow_up_type: None, + applicable_scope: None, status: None, fields: None, + }, + tmpl.version, + ) + .await + .unwrap(); + + // 用旧 version 再更新应失败 + let result = follow_up_template_service::update_template( + app.health_state(), app.tenant_id(), tmpl.id, + Some(app.operator_id()), + UpdateFollowUpTemplateReq { + name: Some("冲突".to_string()), + description: None, follow_up_type: None, + applicable_scope: None, status: None, fields: None, + }, + tmpl.version, + ) + .await; + assert!(result.is_err(), "乐观锁冲突应返回错误"); +} diff --git a/crates/erp-server/tests/integration/health_medication_tests.rs b/crates/erp-server/tests/integration/health_medication_tests.rs new file mode 100644 index 0000000..f32f5e7 --- /dev/null +++ b/crates/erp-server/tests/integration/health_medication_tests.rs @@ -0,0 +1,222 @@ +//! erp-health 用药记录集成测试 +//! +//! 验证用药记录 CRUD、列表按患者过滤、租户隔离、乐观锁。 + +use erp_health::dto::medication_record_dto::*; +use erp_health::service::medication_record_service; + +use super::test_fixture::TestApp; + +fn default_create_medication_req(patient_id: uuid::Uuid) -> CreateMedicationRecordReq { + CreateMedicationRecordReq { + patient_id, + medication_name: "缬沙坦".to_string(), + generic_name: Some("Valsartan".to_string()), + dosage: Some("80mg".to_string()), + unit: Some("mg".to_string()), + frequency: Some("daily".to_string()), + route: Some("oral".to_string()), + start_date: chrono::NaiveDate::from_ymd_opt(2026, 1, 1), + end_date: None, + is_current: Some(true), + prescribed_by: None, + notes: None, + } +} + +async fn seed_medication(app: &TestApp, patient_id: uuid::Uuid) -> MedicationRecordResp { + medication_record_service::create_medication( + app.health_state(), app.tenant_id(), Some(app.operator_id()), + default_create_medication_req(patient_id), + ) + .await + .expect("创建用药记录应成功") +} + +// --------------------------------------------------------------------------- +// 测试 1: 创建用药记录 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_create() { + let app = TestApp::new().await; + let patient_id = app.create_patient("用药患者").await; + + let med = seed_medication(&app, patient_id).await; + + assert_eq!(med.patient_id, patient_id); + assert_eq!(med.medication_name, "缬沙坦"); + assert_eq!(med.frequency, Some("daily".to_string())); + assert!(med.is_current); + assert_eq!(med.version, 1); +} + +// --------------------------------------------------------------------------- +// 测试 2: 查询用药记录 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_get() { + let app = TestApp::new().await; + let patient_id = app.create_patient("用药查询患者").await; + let med = seed_medication(&app, patient_id).await; + + let fetched = medication_record_service::get_medication( + app.health_state(), app.tenant_id(), med.id, + ) + .await + .expect("查询应成功"); + assert_eq!(fetched.id, med.id); + assert_eq!(fetched.medication_name, "缬沙坦"); +} + +// --------------------------------------------------------------------------- +// 测试 3: 更新用药记录 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_update() { + let app = TestApp::new().await; + let patient_id = app.create_patient("用药更新患者").await; + let med = seed_medication(&app, patient_id).await; + + let updated = medication_record_service::update_medication( + app.health_state(), app.tenant_id(), med.id, + Some(app.operator_id()), + UpdateMedicationRecordReq { + dosage: Some("160mg".to_string()), + is_current: Some(false), + medication_name: None, generic_name: None, unit: None, + frequency: None, route: None, start_date: None, + end_date: None, prescribed_by: None, notes: None, + }, + med.version, + ) + .await + .expect("更新应成功"); + + assert_eq!(updated.dosage, Some("160mg".to_string())); + assert!(!updated.is_current); + assert_eq!(updated.version, 2); +} + +// --------------------------------------------------------------------------- +// 测试 4: 列表按患者过滤 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_list_by_patient() { + let app = TestApp::new().await; + let patient_a = app.create_patient("用药列表A").await; + let patient_b = app.create_patient("用药列表B").await; + + seed_medication(&app, patient_a).await; + seed_medication(&app, patient_b).await; + + let list_a = medication_record_service::list_medications( + app.health_state(), app.tenant_id(), patient_a, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_a.total, 1); + + let list_b = medication_record_service::list_medications( + app.health_state(), app.tenant_id(), patient_b, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list_b.total, 1); +} + +// --------------------------------------------------------------------------- +// 测试 5: 软删除 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_soft_delete() { + let app = TestApp::new().await; + let patient_id = app.create_patient("用药删除患者").await; + let med = seed_medication(&app, patient_id).await; + + medication_record_service::delete_medication( + app.health_state(), app.tenant_id(), med.id, + Some(app.operator_id()), med.version, + ) + .await + .expect("删除应成功"); + + let result = medication_record_service::get_medication( + app.health_state(), app.tenant_id(), med.id, + ) + .await; + assert!(result.is_err(), "软删除后查询应失败"); +} + +// --------------------------------------------------------------------------- +// 测试 6: 租户隔离 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_tenant_isolation() { + let app = TestApp::new().await; + let patient_id = app.create_patient("用药隔离患者").await; + seed_medication(&app, patient_id).await; + + let other_tenant = uuid::Uuid::new_v4(); + let list = medication_record_service::list_medications( + app.health_state(), other_tenant, patient_id, 1, 20, + ) + .await + .unwrap(); + assert_eq!(list.total, 0, "不同租户不应看到用药记录"); +} + +// --------------------------------------------------------------------------- +// 测试 7: 乐观锁冲突 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_version_conflict() { + let app = TestApp::new().await; + let patient_id = app.create_patient("用药锁患者").await; + let med = seed_medication(&app, patient_id).await; + + // 先更新一次 + medication_record_service::update_medication( + app.health_state(), app.tenant_id(), med.id, + Some(app.operator_id()), + UpdateMedicationRecordReq { + dosage: Some("160mg".to_string()), + medication_name: None, generic_name: None, unit: None, + frequency: None, route: None, start_date: None, + end_date: None, is_current: None, prescribed_by: None, notes: None, + }, + med.version, + ) + .await + .unwrap(); + + // 用旧 version 再更新应失败 + let result = medication_record_service::update_medication( + app.health_state(), app.tenant_id(), med.id, + Some(app.operator_id()), + UpdateMedicationRecordReq { + dosage: Some("320mg".to_string()), + medication_name: None, generic_name: None, unit: None, + frequency: None, route: None, start_date: None, + end_date: None, is_current: None, prescribed_by: None, notes: None, + }, + med.version, + ) + .await; + assert!(result.is_err(), "乐观锁冲突应返回错误"); +} + +// --------------------------------------------------------------------------- +// 测试 8: 无效患者创建用药记录返回错误 +// --------------------------------------------------------------------------- +#[tokio::test] +async fn test_medication_invalid_patient() { + let app = TestApp::new().await; + let fake_patient = uuid::Uuid::new_v4(); + + let result = medication_record_service::create_medication( + app.health_state(), app.tenant_id(), None, + default_create_medication_req(fake_patient), + ) + .await; + assert!(result.is_err(), "无效患者应返回错误"); +}