test(health): 文章/分类/标签集成测试 — 10 个用例全通过
覆盖文章 CRUD、状态流(draft→pending_review→published→draft)、 拒绝与重提交、列表过滤、软删除、租户隔离、分类 CRUD+隔离、 标签 CRUD+文章关联、乐观锁冲突。
This commit is contained in:
@@ -28,3 +28,5 @@ mod health_follow_up_tests;
|
||||
mod health_consultation_tests;
|
||||
#[path = "integration/health_data_tests.rs"]
|
||||
mod health_data_tests;
|
||||
#[path = "integration/health_article_tests.rs"]
|
||||
mod health_article_tests;
|
||||
|
||||
438
crates/erp-server/tests/integration/health_article_tests.rs
Normal file
438
crates/erp-server/tests/integration/health_article_tests.rs
Normal file
@@ -0,0 +1,438 @@
|
||||
//! erp-health 内容管理(文章 + 分类 + 标签)集成测试
|
||||
//!
|
||||
//! 验证文章 CRUD + 状态流、分类 CRUD、标签 CRUD、租户隔离、乐观锁。
|
||||
|
||||
use erp_health::dto::article_dto::*;
|
||||
use erp_health::service::{article_service, article_category_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,
|
||||
)
|
||||
.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,
|
||||
)
|
||||
.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(), "乐观锁冲突应返回错误");
|
||||
}
|
||||
Reference in New Issue
Block a user