//! erp-health 内容管理(文章 + 分类 + 标签)集成测试 //! //! 验证文章 CRUD + 状态流、分类 CRUD、标签 CRUD、租户隔离、乐观锁。 use erp_health::dto::article_dto::*; use erp_health::service::{article_category_service, article_service, article_tag_service}; use super::test_fixture::TestApp; // --------------------------------------------------------------------------- // 辅助函数 // --------------------------------------------------------------------------- fn default_create_article_req() -> CreateArticleReq { CreateArticleReq { title: "测试文章".to_string(), summary: Some("摘要".to_string()), content: Some("正文内容".to_string()), cover_image: None, category: None, author: Some("张医生".to_string()), published_at: None, slug: None, content_type: None, category_id: None, tag_ids: vec![], } } async fn seed_article(app: &TestApp) -> ArticleResp { article_service::create_article( app.health_state(), app.tenant_id(), Some(app.operator_id()), default_create_article_req(), ) .await .expect("创建文章应成功") } async fn seed_category(app: &TestApp, name: &str) -> CategoryResp { article_category_service::create_category( app.health_state(), app.tenant_id(), Some(app.operator_id()), CreateCategoryReq { name: name.to_string(), slug: None, parent_id: None, description: None, sort_order: None, }, ) .await .expect("创建分类应成功") } async fn seed_tag(app: &TestApp, name: &str) -> TagResp { article_tag_service::create_tag( app.health_state(), app.tenant_id(), Some(app.operator_id()), CreateTagReq { name: name.to_string(), }, ) .await .expect("创建标签应成功") } // --------------------------------------------------------------------------- // 测试 1: 创建文章 // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_create() { let app = TestApp::new().await; let article = seed_article(&app).await; assert_eq!(article.title, "测试文章"); assert_eq!(article.status, "draft"); assert_eq!(article.view_count, 0); assert_eq!(article.version, 1); assert_eq!(article.content_type, "rich_text"); } // --------------------------------------------------------------------------- // 测试 2: 文章状态流 draft → pending_review → published → draft // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_status_flow() { let app = TestApp::new().await; let article = seed_article(&app).await; assert_eq!(article.status, "draft"); // draft → pending_review let submitted = article_service::submit_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), article.version, ) .await .expect("提交审核应成功"); assert_eq!(submitted.status, "pending_review"); // pending_review → published let published = article_service::approve_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), ReviewArticleReq { note: Some("通过".to_string()), version: Some(submitted.version), }, submitted.version, ) .await .expect("审核通过应成功"); assert_eq!(published.status, "published"); // published → draft(取消发布) let unpublished = article_service::unpublish_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), published.version, ) .await .expect("取消发布应成功"); assert_eq!(unpublished.status, "draft"); } // --------------------------------------------------------------------------- // 测试 3: 文章拒绝与重新提交 // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_reject_and_resubmit() { let app = TestApp::new().await; let article = seed_article(&app).await; let submitted = article_service::submit_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), article.version, ) .await .unwrap(); // pending_review → rejected let rejected = article_service::reject_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), ReviewArticleReq { note: Some("内容需修改".to_string()), version: Some(submitted.version), }, submitted.version, ) .await .expect("拒绝应成功"); assert_eq!(rejected.status, "rejected"); // rejected → pending_review let resubmitted = article_service::submit_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), rejected.version, ) .await .expect("重新提交应成功"); assert_eq!(resubmitted.status, "pending_review"); } // --------------------------------------------------------------------------- // 测试 4: 文章更新 // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_update() { let app = TestApp::new().await; let article = seed_article(&app).await; let updated = article_service::update_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), UpdateArticleReq { title: Some("更新标题".to_string()), summary: None, content: None, cover_image: None, category: None, author: None, published_at: None, slug: None, content_type: None, category_id: None, tag_ids: None, sort_order: None, version: article.version, }, ) .await .expect("更新应成功"); assert_eq!(updated.title, "更新标题"); assert_eq!(updated.version, 2); } // --------------------------------------------------------------------------- // 测试 5: 文章列表 + 状态过滤 // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_list_filter() { let app = TestApp::new().await; let a1 = seed_article(&app).await; let _a2 = seed_article(&app).await; // 提交 a1 到 pending_review article_service::submit_article( app.health_state(), app.tenant_id(), a1.id, Some(app.operator_id()), a1.version, ) .await .unwrap(); // 按状态过滤 let pending = article_service::list_articles( app.health_state(), app.tenant_id(), 1, 20, None, Some("pending_review".to_string()), None, None, None, ) .await .unwrap(); assert_eq!(pending.total, 1); let drafts = article_service::list_articles( app.health_state(), app.tenant_id(), 1, 20, None, Some("draft".to_string()), None, None, None, ) .await .unwrap(); assert_eq!(drafts.total, 1); } // --------------------------------------------------------------------------- // 测试 6: 文章软删除 // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_soft_delete() { let app = TestApp::new().await; let article = seed_article(&app).await; article_service::delete_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), article.version, ) .await .expect("删除应成功"); let result = article_service::get_article(app.health_state(), app.tenant_id(), article.id, true).await; assert!(result.is_err(), "软删除后查询应失败"); } // --------------------------------------------------------------------------- // 测试 7: 文章租户隔离 // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_tenant_isolation() { let app = TestApp::new().await; let article = seed_article(&app).await; let other_tenant = uuid::Uuid::new_v4(); let result = article_service::get_article(app.health_state(), other_tenant, article.id, true).await; assert!(result.is_err(), "不同租户不应看到此文章"); } // --------------------------------------------------------------------------- // 测试 8: 分类 CRUD + 租户隔离 // --------------------------------------------------------------------------- #[tokio::test] async fn test_category_crud_and_isolation() { let app = TestApp::new().await; // 创建 let cat = seed_category(&app, "肾病科普").await; assert_eq!(cat.name, "肾病科普"); assert_eq!(cat.version, 1); // 列表 let list = article_category_service::list_categories(app.health_state(), app.tenant_id()) .await .unwrap(); assert_eq!(list.len(), 1); // 更新 let updated = article_category_service::update_category( app.health_state(), app.tenant_id(), cat.id, Some(app.operator_id()), UpdateCategoryReq { name: Some("透析护理".to_string()), slug: None, parent_id: None, description: None, sort_order: None, version: cat.version, }, ) .await .expect("更新分类应成功"); assert_eq!(updated.name, "透析护理"); // 删除 article_category_service::delete_category( app.health_state(), app.tenant_id(), cat.id, Some(app.operator_id()), updated.version, ) .await .expect("删除分类应成功"); let list_after = article_category_service::list_categories(app.health_state(), app.tenant_id()) .await .unwrap(); assert_eq!(list_after.len(), 0, "删除后列表应为空"); // 租户隔离 let cat2 = seed_category(&app, "隔离分类").await; let other_tenant = uuid::Uuid::new_v4(); let other_list = article_category_service::list_categories(app.health_state(), other_tenant) .await .unwrap(); assert_eq!(other_list.len(), 0, "不同租户不应看到分类"); // 防止 unused warning let _ = cat2; } // --------------------------------------------------------------------------- // 测试 9: 标签 CRUD + 文章关联 // --------------------------------------------------------------------------- #[tokio::test] async fn test_tag_crud_and_article_association() { let app = TestApp::new().await; // 创建标签 let tag1 = seed_tag(&app, "高血压").await; let tag2 = seed_tag(&app, "糖尿病").await; assert_eq!(tag1.name, "高血压"); // 创建文章并关联标签 let article = article_service::create_article( app.health_state(), app.tenant_id(), Some(app.operator_id()), CreateArticleReq { title: "带标签的文章".to_string(), tag_ids: vec![tag1.id, tag2.id], ..default_create_article_req() }, ) .await .expect("创建带标签文章应成功"); assert_eq!(article.tags.len(), 2); // 更新标签(替换为只有 tag1) let updated = article_service::update_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), UpdateArticleReq { tag_ids: Some(vec![tag1.id]), version: article.version, title: None, summary: None, content: None, cover_image: None, category: None, author: None, published_at: None, slug: None, content_type: None, category_id: None, sort_order: None, }, ) .await .expect("更新标签应成功"); assert_eq!(updated.tags.len(), 1); // 标签列表 let tags = article_tag_service::list_tags(app.health_state(), app.tenant_id()) .await .unwrap(); assert_eq!(tags.len(), 2); // 更新标签名称 let renamed = article_tag_service::update_tag( app.health_state(), app.tenant_id(), tag1.id, Some(app.operator_id()), UpdateTagReq { name: "血压高".to_string(), version: tag1.version, }, ) .await .expect("更新标签应成功"); assert_eq!(renamed.name, "血压高"); // 删除标签 article_tag_service::delete_tag( app.health_state(), app.tenant_id(), tag2.id, Some(app.operator_id()), tag2.version, ) .await .expect("删除标签应成功"); let tags_after = article_tag_service::list_tags(app.health_state(), app.tenant_id()) .await .unwrap(); assert_eq!(tags_after.len(), 1); } // --------------------------------------------------------------------------- // 测试 10: 乐观锁冲突 // --------------------------------------------------------------------------- #[tokio::test] async fn test_article_version_conflict() { let app = TestApp::new().await; let article = seed_article(&app).await; // 先更新一次,version 变为 2 article_service::update_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), UpdateArticleReq { title: Some("第一次更新".to_string()), version: article.version, summary: None, content: None, cover_image: None, category: None, author: None, published_at: None, slug: None, content_type: None, category_id: None, tag_ids: None, sort_order: None, }, ) .await .unwrap(); // 用旧 version 再次更新应失败 let result = article_service::update_article( app.health_state(), app.tenant_id(), article.id, Some(app.operator_id()), UpdateArticleReq { title: Some("冲突更新".to_string()), version: article.version, // 旧版本号 summary: None, content: None, cover_image: None, category: None, author: None, published_at: None, slug: None, content_type: None, category_id: None, tag_ids: None, sort_order: None, }, ) .await; assert!(result.is_err(), "乐观锁冲突应返回错误"); }