- 修复透析集成测试 TestApp.dialysis_state() 返回类型不匹配(39个错误) - 修复 erp-core test_helpers SeaORM Database::connect API 变更 - 修复 health_alert/article/data 集成测试函数签名不匹配 - 修复 DailyMonitoringTab 缺失 Input import - 修复 DeviceReadingsTab 未使用接口声明 - 修复 DialysisManageList keyword → search 参数名
439 lines
14 KiB
Rust
439 lines
14 KiB
Rust
//! 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(), "乐观锁冲突应返回错误");
|
||
}
|