Files
hms/crates/erp-server/tests/integration/health_article_tests.rs
iven d8735eb45c fix(test+web): 修复测试编译错误 + 前端构建问题
- 修复透析集成测试 TestApp.dialysis_state() 返回类型不匹配(39个错误)
- 修复 erp-core test_helpers SeaORM Database::connect API 变更
- 修复 health_alert/article/data 集成测试函数签名不匹配
- 修复 DailyMonitoringTab 缺失 Input import
- 修复 DeviceReadingsTab 未使用接口声明
- 修复 DialysisManageList keyword → search 参数名
2026-04-30 10:21:05 +08:00

439 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 + 状态流、分类 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, 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(), "乐观锁冲突应返回错误");
}