chore: 干净 ERP 基座 — 删除 health/ai/wechat 业务代码
删除内容: - 前端: health/(67文件), ai/(2文件), Copilot, MediaPicker, 相关API/Store/Hook - 后端: wechat_handler, wechat_service, wechat_user entity, analytics handler, ai_workflow_seed - 配置: WechatConfig, AppConfig.wechat, AuthState wechat 字段 - 启动: 微信凭据检查块, ensure_ai_workflows() 调用 - 迁移: 新增 m20260613_000170_drop_wechat_users.rs - 脚本: api_test_health_alert.py, api_test_mp.py, mpsync.sh/ps1 - E2E: health-data page, flows/ 目录 保留: erp-core/auth/workflow/message/config/plugin + 基座前端 + 通用组件
This commit is contained in:
8
crates/erp-server/tests/integration.rs
Normal file
8
crates/erp-server/tests/integration.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#[path = "integration/auth_tests.rs"]
|
||||
mod auth_tests;
|
||||
#[path = "integration/plugin_tests.rs"]
|
||||
mod plugin_tests;
|
||||
#[path = "integration/test_db.rs"]
|
||||
mod test_db;
|
||||
#[path = "integration/workflow_tests.rs"]
|
||||
mod workflow_tests;
|
||||
129
crates/erp-server/tests/integration/auth_tests.rs
Normal file
129
crates/erp-server/tests/integration/auth_tests.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use erp_auth::dto::CreateUserReq;
|
||||
use erp_auth::service::user_service::UserService;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::types::Pagination;
|
||||
|
||||
use super::test_db::TestDb;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_crud() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
let operator_id = uuid::Uuid::new_v4();
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
// 创建用户
|
||||
let user = UserService::create(
|
||||
tenant_id,
|
||||
operator_id,
|
||||
&CreateUserReq {
|
||||
username: "testuser".to_string(),
|
||||
password: "TestPass123".to_string(),
|
||||
email: Some("test@example.com".to_string()),
|
||||
phone: None,
|
||||
display_name: Some("测试用户".to_string()),
|
||||
},
|
||||
db,
|
||||
&event_bus,
|
||||
)
|
||||
.await
|
||||
.expect("创建用户失败");
|
||||
|
||||
assert_eq!(user.username, "testuser");
|
||||
assert_eq!(user.status, "active");
|
||||
|
||||
// 按 ID 查询
|
||||
let found = UserService::get_by_id(user.id, tenant_id, db)
|
||||
.await
|
||||
.expect("查询用户失败");
|
||||
assert_eq!(found.username, "testuser");
|
||||
assert_eq!(found.email, Some("test@example.com".to_string()));
|
||||
|
||||
// 列表查询
|
||||
let (users, total) = UserService::list(
|
||||
tenant_id,
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
.expect("用户列表查询失败");
|
||||
assert_eq!(total, 1);
|
||||
assert_eq!(users[0].username, "testuser");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tenant_isolation() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
let tenant_a = uuid::Uuid::new_v4();
|
||||
let tenant_b = uuid::Uuid::new_v4();
|
||||
let operator_id = uuid::Uuid::new_v4();
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
// 租户 A 创建用户
|
||||
let user_a = UserService::create(
|
||||
tenant_a,
|
||||
operator_id,
|
||||
&CreateUserReq {
|
||||
username: "user_a".to_string(),
|
||||
password: "Pass123456".to_string(),
|
||||
email: None,
|
||||
phone: None,
|
||||
display_name: None,
|
||||
},
|
||||
db,
|
||||
&event_bus,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 租户 B 列表查询不应看到租户 A 的用户
|
||||
let (users_b, total_b) = UserService::list(
|
||||
tenant_b,
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(total_b, 0);
|
||||
assert!(users_b.is_empty());
|
||||
|
||||
// 租户 B 通过 ID 查询租户 A 的用户应返回错误
|
||||
let result = UserService::get_by_id(user_a.id, tenant_b, db).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_username_uniqueness_within_tenant() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
let operator_id = uuid::Uuid::new_v4();
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
let req = CreateUserReq {
|
||||
username: "duplicate".to_string(),
|
||||
password: "Pass123456".to_string(),
|
||||
email: None,
|
||||
phone: None,
|
||||
display_name: None,
|
||||
};
|
||||
|
||||
// 第一次创建成功
|
||||
UserService::create(tenant_id, operator_id, &req, db, &event_bus)
|
||||
.await
|
||||
.expect("创建用户应成功");
|
||||
|
||||
// 同租户重复用户名应失败
|
||||
let result = UserService::create(tenant_id, operator_id, &req, db, &event_bus).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
213
crates/erp-server/tests/integration/plugin_tests.rs
Normal file
213
crates/erp-server/tests/integration/plugin_tests.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use erp_plugin::dynamic_table::DynamicTableManager;
|
||||
use erp_plugin::manifest::{
|
||||
PluginEntity, PluginField, PluginFieldType, PluginManifest, PluginMetadata, PluginSchema,
|
||||
};
|
||||
use sea_orm::{ConnectionTrait, FromQueryResult};
|
||||
|
||||
use super::test_db::TestDb;
|
||||
|
||||
/// 构造一个最小默认值的 PluginField(外部 crate 无法使用 #[cfg(test)] 的 default_for_field)
|
||||
fn make_field(name: &str, field_type: PluginFieldType) -> PluginField {
|
||||
PluginField {
|
||||
name: name.to_string(),
|
||||
field_type,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: None,
|
||||
display_name: None,
|
||||
ui_widget: None,
|
||||
options: None,
|
||||
searchable: None,
|
||||
filterable: None,
|
||||
sortable: None,
|
||||
visible_when: None,
|
||||
ref_entity: None,
|
||||
ref_label_field: None,
|
||||
ref_search_fields: None,
|
||||
cascade_from: None,
|
||||
cascade_filter: None,
|
||||
validation: None,
|
||||
no_cycle: None,
|
||||
scope_role: None,
|
||||
ref_plugin: None,
|
||||
ref_fallback_label: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建测试用 manifest
|
||||
fn make_test_manifest() -> PluginManifest {
|
||||
PluginManifest {
|
||||
metadata: PluginMetadata {
|
||||
id: "erp-test".to_string(),
|
||||
name: "测试插件".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
description: "集成测试用".to_string(),
|
||||
author: "test".to_string(),
|
||||
min_platform_version: None,
|
||||
dependencies: vec![],
|
||||
},
|
||||
schema: Some(PluginSchema {
|
||||
entities: vec![PluginEntity {
|
||||
name: "item".to_string(),
|
||||
display_name: "测试项".to_string(),
|
||||
is_public: None,
|
||||
fields: vec![
|
||||
PluginField {
|
||||
name: "code".to_string(),
|
||||
field_type: PluginFieldType::String,
|
||||
required: true,
|
||||
unique: true,
|
||||
display_name: Some("编码".to_string()),
|
||||
searchable: Some(true),
|
||||
..make_field("code", PluginFieldType::String)
|
||||
},
|
||||
PluginField {
|
||||
name: "name".to_string(),
|
||||
field_type: PluginFieldType::String,
|
||||
required: true,
|
||||
display_name: Some("名称".to_string()),
|
||||
searchable: Some(true),
|
||||
..make_field("name", PluginFieldType::String)
|
||||
},
|
||||
PluginField {
|
||||
name: "status".to_string(),
|
||||
field_type: PluginFieldType::String,
|
||||
filterable: Some(true),
|
||||
display_name: Some("状态".to_string()),
|
||||
..make_field("status", PluginFieldType::String)
|
||||
},
|
||||
PluginField {
|
||||
name: "sort_order".to_string(),
|
||||
field_type: PluginFieldType::Integer,
|
||||
sortable: Some(true),
|
||||
display_name: Some("排序".to_string()),
|
||||
..make_field("sort_order", PluginFieldType::Integer)
|
||||
},
|
||||
],
|
||||
indexes: vec![],
|
||||
relations: vec![],
|
||||
data_scope: None,
|
||||
importable: None,
|
||||
exportable: None,
|
||||
}],
|
||||
}),
|
||||
events: None,
|
||||
ui: None,
|
||||
permissions: None,
|
||||
settings: None,
|
||||
numbering: None,
|
||||
templates: None,
|
||||
trigger_events: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dynamic_table_create_and_query() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
|
||||
let manifest = make_test_manifest();
|
||||
let entity = &manifest.schema.as_ref().unwrap().entities[0];
|
||||
|
||||
// 创建动态表
|
||||
DynamicTableManager::create_table(db, "erp_test", entity)
|
||||
.await
|
||||
.expect("创建动态表失败");
|
||||
|
||||
let table_name = DynamicTableManager::table_name("erp_test", &entity.name);
|
||||
|
||||
// 验证表存在
|
||||
let exists = DynamicTableManager::table_exists(db, &table_name)
|
||||
.await
|
||||
.expect("检查表存在失败");
|
||||
assert!(exists, "动态表应存在");
|
||||
|
||||
// 插入数据
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
let data = serde_json::json!({
|
||||
"code": "ITEM001",
|
||||
"name": "测试项目",
|
||||
"status": "active",
|
||||
"sort_order": 1
|
||||
});
|
||||
|
||||
let (sql, values) =
|
||||
DynamicTableManager::build_insert_sql(&table_name, tenant_id, user_id, &data);
|
||||
db.execute(sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
values,
|
||||
))
|
||||
.await
|
||||
.expect("插入数据失败");
|
||||
|
||||
// 查询数据
|
||||
let (sql, values) = DynamicTableManager::build_query_sql(&table_name, tenant_id, 10, 0);
|
||||
#[derive(FromQueryResult)]
|
||||
struct Row {
|
||||
id: uuid::Uuid,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
let rows = Row::find_by_statement(sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
values,
|
||||
))
|
||||
.all(db)
|
||||
.await
|
||||
.expect("查询数据失败");
|
||||
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].data["code"], "ITEM001");
|
||||
assert_eq!(rows[0].data["name"], "测试项目");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tenant_isolation_in_dynamic_table() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
|
||||
let manifest = make_test_manifest();
|
||||
let entity = &manifest.schema.as_ref().unwrap().entities[0];
|
||||
|
||||
DynamicTableManager::create_table(db, "erp_test_iso", entity)
|
||||
.await
|
||||
.expect("创建动态表失败");
|
||||
|
||||
let table_name = DynamicTableManager::table_name("erp_test_iso", &entity.name);
|
||||
|
||||
let tenant_a = uuid::Uuid::new_v4();
|
||||
let tenant_b = uuid::Uuid::new_v4();
|
||||
let user_id = uuid::Uuid::new_v4();
|
||||
|
||||
// 租户 A 插入数据
|
||||
let data_a = serde_json::json!({"code": "A001", "name": "租户A数据", "status": "active", "sort_order": 1});
|
||||
let (sql, values) =
|
||||
DynamicTableManager::build_insert_sql(&table_name, tenant_a, user_id, &data_a);
|
||||
db.execute(sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
values,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 租户 B 查询不应看到租户 A 的数据
|
||||
let (sql, values) = DynamicTableManager::build_query_sql(&table_name, tenant_b, 10, 0);
|
||||
#[derive(FromQueryResult)]
|
||||
struct Row {
|
||||
id: uuid::Uuid,
|
||||
data: serde_json::Value,
|
||||
}
|
||||
let rows = Row::find_by_statement(sea_orm::Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
sql,
|
||||
values,
|
||||
))
|
||||
.all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(rows.is_empty(), "租户 B 不应看到租户 A 的数据");
|
||||
}
|
||||
114
crates/erp-server/tests/integration/test_db.rs
Normal file
114
crates/erp-server/tests/integration/test_db.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use sea_orm::{ConnectionTrait, Database, DatabaseBackend, Statement};
|
||||
use std::sync::Arc;
|
||||
|
||||
use erp_server_migration::MigratorTrait;
|
||||
|
||||
/// 全局信号量:限制同时创建数据库的测试数量,避免 PostgreSQL 连接耗尽
|
||||
static DB_SEMAPHORE: std::sync::OnceLock<Arc<tokio::sync::Semaphore>> = std::sync::OnceLock::new();
|
||||
|
||||
fn db_semaphore() -> &'static Arc<tokio::sync::Semaphore> {
|
||||
DB_SEMAPHORE.get_or_init(|| Arc::new(tokio::sync::Semaphore::new(4)))
|
||||
}
|
||||
|
||||
/// 测试数据库 — 使用本地 PostgreSQL 创建隔离测试库
|
||||
///
|
||||
/// 连接本地 PostgreSQL(wiki/infrastructure.md 配置),为每个测试创建独立的测试数据库。
|
||||
/// 不依赖 Docker/Testcontainers,与开发环境一致。
|
||||
pub struct TestDb {
|
||||
db: Option<sea_orm::DatabaseConnection>,
|
||||
db_name: String,
|
||||
_permit: Option<tokio::sync::OwnedSemaphorePermit>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub async fn new() -> Self {
|
||||
let permit = db_semaphore()
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.expect("信号量获取失败");
|
||||
|
||||
let db_name = format!("erp_test_{}", uuid::Uuid::now_v7().simple());
|
||||
|
||||
let admin_url = std::env::var("TEST_DB_URL")
|
||||
.unwrap_or_else(|_| "postgres://postgres:123123@localhost:5432/postgres".to_string());
|
||||
|
||||
let admin_db = Database::connect(&admin_url)
|
||||
.await
|
||||
.expect("连接本地 PostgreSQL 失败,请确认服务正在运行");
|
||||
|
||||
admin_db
|
||||
.execute(Statement::from_string(
|
||||
DatabaseBackend::Postgres,
|
||||
format!("CREATE DATABASE \"{}\"", db_name),
|
||||
))
|
||||
.await
|
||||
.expect("创建测试数据库失败");
|
||||
|
||||
drop(admin_db);
|
||||
|
||||
// 从 admin_url 推导测试库 URL(替换路径部分)
|
||||
let test_url = if let Some(pos) = admin_url.rfind('/') {
|
||||
format!("{}/{}", &admin_url[..pos], db_name)
|
||||
} else {
|
||||
format!("postgres://postgres:123123@localhost:5432/{}", db_name)
|
||||
};
|
||||
let db = Database::connect(&test_url)
|
||||
.await
|
||||
.expect("连接测试数据库失败");
|
||||
|
||||
// 运行所有迁移
|
||||
erp_server_migration::Migrator::up(&db, None)
|
||||
.await
|
||||
.expect("执行数据库迁移失败");
|
||||
|
||||
Self {
|
||||
db: Some(db),
|
||||
db_name,
|
||||
_permit: Some(permit),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取数据库连接引用
|
||||
pub fn db(&self) -> &sea_orm::DatabaseConnection {
|
||||
self.db.as_ref().expect("数据库连接已被释放")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDb {
|
||||
fn drop(&mut self) {
|
||||
let db_name = self.db_name.clone();
|
||||
self.db.take();
|
||||
|
||||
// 尝试在独立线程中清理,避免在 tokio runtime 内创建新 runtime
|
||||
let _ = std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build();
|
||||
if let Ok(rt) = rt {
|
||||
rt.block_on(async {
|
||||
let admin_url = std::env::var("TEST_DB_URL")
|
||||
.unwrap_or_else(|_| "postgres://postgres:123123@localhost:5432/postgres".to_string());
|
||||
if let Ok(admin_db) = Database::connect(&admin_url).await {
|
||||
let disconnect_sql = format!(
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}'",
|
||||
db_name
|
||||
);
|
||||
admin_db
|
||||
.execute(Statement::from_string(DatabaseBackend::Postgres, disconnect_sql))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
admin_db
|
||||
.execute(Statement::from_string(
|
||||
DatabaseBackend::Postgres,
|
||||
format!("DROP DATABASE IF EXISTS \"{}\"", db_name),
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
247
crates/erp-server/tests/integration/workflow_tests.rs
Normal file
247
crates/erp-server/tests/integration/workflow_tests.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::types::Pagination;
|
||||
use erp_workflow::dto::{
|
||||
CompleteTaskReq, CreateProcessDefinitionReq, EdgeDef, NodeDef, NodeType, StartInstanceReq,
|
||||
};
|
||||
use erp_workflow::service::definition_service::DefinitionService;
|
||||
use erp_workflow::service::instance_service::InstanceService;
|
||||
use erp_workflow::service::task_service::TaskService;
|
||||
|
||||
use super::test_db::TestDb;
|
||||
|
||||
/// 构建一个最简单的线性流程:开始 → 审批 → 结束
|
||||
/// assignee 指向 operator_id,使 list_pending 能查到任务
|
||||
fn make_simple_definition(
|
||||
name: &str,
|
||||
key: &str,
|
||||
assignee_id: Option<uuid::Uuid>,
|
||||
) -> CreateProcessDefinitionReq {
|
||||
CreateProcessDefinitionReq {
|
||||
name: name.to_string(),
|
||||
key: key.to_string(),
|
||||
category: Some("test".to_string()),
|
||||
description: Some("集成测试流程".to_string()),
|
||||
nodes: vec![
|
||||
NodeDef {
|
||||
id: "start".to_string(),
|
||||
node_type: NodeType::StartEvent,
|
||||
name: "开始".to_string(),
|
||||
assignee_id: None,
|
||||
candidate_groups: None,
|
||||
service_type: None,
|
||||
service_config: None,
|
||||
position: None,
|
||||
},
|
||||
NodeDef {
|
||||
id: "approve".to_string(),
|
||||
node_type: NodeType::UserTask,
|
||||
name: "审批".to_string(),
|
||||
assignee_id,
|
||||
candidate_groups: None,
|
||||
service_type: None,
|
||||
service_config: None,
|
||||
position: None,
|
||||
},
|
||||
NodeDef {
|
||||
id: "end".to_string(),
|
||||
node_type: NodeType::EndEvent,
|
||||
name: "结束".to_string(),
|
||||
assignee_id: None,
|
||||
candidate_groups: None,
|
||||
service_type: None,
|
||||
service_config: None,
|
||||
position: None,
|
||||
},
|
||||
],
|
||||
edges: vec![
|
||||
EdgeDef {
|
||||
id: "e1".to_string(),
|
||||
source: "start".to_string(),
|
||||
target: "approve".to_string(),
|
||||
condition: None,
|
||||
label: None,
|
||||
},
|
||||
EdgeDef {
|
||||
id: "e2".to_string(),
|
||||
source: "approve".to_string(),
|
||||
target: "end".to_string(),
|
||||
condition: None,
|
||||
label: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_definition_crud() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
let operator_id = uuid::Uuid::new_v4();
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
let def = DefinitionService::create(
|
||||
tenant_id,
|
||||
operator_id,
|
||||
&make_simple_definition("测试流程", "test-flow-1", None),
|
||||
db,
|
||||
&event_bus,
|
||||
)
|
||||
.await
|
||||
.expect("创建流程定义失败");
|
||||
|
||||
assert_eq!(def.name, "测试流程");
|
||||
assert_eq!(def.status, "draft");
|
||||
|
||||
let (defs, total) = DefinitionService::list(
|
||||
tenant_id,
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
db,
|
||||
)
|
||||
.await
|
||||
.expect("查询流程定义列表失败");
|
||||
assert_eq!(total, 1);
|
||||
assert_eq!(defs[0].name, "测试流程");
|
||||
|
||||
let found = DefinitionService::get_by_id(def.id, tenant_id, db)
|
||||
.await
|
||||
.expect("查询流程定义失败");
|
||||
assert_eq!(found.id, def.id);
|
||||
|
||||
let published = DefinitionService::publish(def.id, tenant_id, operator_id, db, &event_bus)
|
||||
.await
|
||||
.expect("发布流程定义失败");
|
||||
assert_eq!(published.status, "published");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_instance_lifecycle() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
let operator_id = uuid::Uuid::new_v4();
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
let def = DefinitionService::create(
|
||||
tenant_id,
|
||||
operator_id,
|
||||
&make_simple_definition("生命周期测试", "lifecycle-flow", Some(operator_id)),
|
||||
db,
|
||||
&event_bus,
|
||||
)
|
||||
.await
|
||||
.expect("创建流程定义失败");
|
||||
|
||||
let def = DefinitionService::publish(def.id, tenant_id, operator_id, db, &event_bus)
|
||||
.await
|
||||
.expect("发布流程定义失败");
|
||||
|
||||
let instance = InstanceService::start(
|
||||
tenant_id,
|
||||
operator_id,
|
||||
&StartInstanceReq {
|
||||
definition_id: def.id,
|
||||
business_key: Some("测试实例".to_string()),
|
||||
variables: None,
|
||||
},
|
||||
db,
|
||||
&event_bus,
|
||||
)
|
||||
.await
|
||||
.expect("启动流程实例失败");
|
||||
|
||||
assert_eq!(instance.status, "running");
|
||||
|
||||
let (tasks, task_total) = TaskService::list_pending(
|
||||
tenant_id,
|
||||
operator_id,
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
db,
|
||||
)
|
||||
.await
|
||||
.expect("查询待办任务失败");
|
||||
assert_eq!(task_total, 1);
|
||||
assert_eq!(tasks[0].status, "pending");
|
||||
|
||||
let completed = TaskService::complete(
|
||||
tasks[0].id,
|
||||
tenant_id,
|
||||
operator_id,
|
||||
&CompleteTaskReq {
|
||||
outcome: "approved".to_string(),
|
||||
form_data: Some(serde_json::json!({"comment": "同意"})),
|
||||
},
|
||||
db,
|
||||
&event_bus,
|
||||
)
|
||||
.await
|
||||
.expect("完成任务失败");
|
||||
assert_eq!(completed.status, "completed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_tenant_isolation() {
|
||||
let test_db = TestDb::new().await;
|
||||
let db = test_db.db();
|
||||
let tenant_a = uuid::Uuid::new_v4();
|
||||
let tenant_b = uuid::Uuid::new_v4();
|
||||
let operator_id = uuid::Uuid::new_v4();
|
||||
let event_bus = EventBus::new(100);
|
||||
|
||||
let def_a = DefinitionService::create(
|
||||
tenant_a,
|
||||
operator_id,
|
||||
&make_simple_definition("租户A流程", "tenant-a-flow", None),
|
||||
db,
|
||||
&event_bus,
|
||||
)
|
||||
.await
|
||||
.expect("创建流程定义失败");
|
||||
|
||||
let (defs_b, total_b) = DefinitionService::list(
|
||||
tenant_b,
|
||||
&Pagination {
|
||||
page: Some(1),
|
||||
page_size: Some(10),
|
||||
},
|
||||
db,
|
||||
)
|
||||
.await
|
||||
.expect("查询流程定义列表失败");
|
||||
assert_eq!(total_b, 0);
|
||||
assert!(defs_b.is_empty());
|
||||
|
||||
let result = DefinitionService::get_by_id(def_a.id, tenant_b, db).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_event_bus_pub_sub() {
|
||||
let event_bus = EventBus::new(100);
|
||||
let tenant_id = uuid::Uuid::new_v4();
|
||||
|
||||
let (mut receiver, _handle) = event_bus.subscribe_filtered("user.".to_string());
|
||||
|
||||
let event = erp_core::events::DomainEvent::new(
|
||||
"user.created",
|
||||
tenant_id,
|
||||
serde_json::json!({"username": "test"}),
|
||||
);
|
||||
event_bus.broadcast(event);
|
||||
|
||||
let other_event =
|
||||
erp_core::events::DomainEvent::new("workflow.started", tenant_id, serde_json::json!({}));
|
||||
event_bus.broadcast(other_event);
|
||||
|
||||
let received = receiver.recv().await;
|
||||
assert!(received.is_some());
|
||||
let received = received.unwrap();
|
||||
assert_eq!(received.event_type, "user.created");
|
||||
assert_eq!(received.payload["username"], "test");
|
||||
}
|
||||
Reference in New Issue
Block a user