Files
hms/crates/erp-server/tests/integration/health_article_tests.rs
iven 6d5a711d2c
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
2026-05-07 23:43:14 +08:00

522 lines
15 KiB
Rust
Raw Permalink 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_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(), "乐观锁冲突应返回错误");
}