Files
erp/crates/erp-server/tests/integration/plugin_tests.rs
iven e8739e80c7
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
feat: Q4 测试覆盖 + 插件生态 — 集成测试/E2E/进销存插件/热更新
Q4 成熟度路线图全部完成:

1. 集成测试框架 (Testcontainers + PostgreSQL):
   - auth_tests: 用户 CRUD、租户隔离、用户名唯一性
   - plugin_tests: 动态表创建查询、租户数据隔离

2. Playwright E2E 测试:
   - 登录页面渲染和表单验证测试
   - 用户管理、插件管理、多租户隔离占位测试

3. 进销存插件 (erp-plugin-inventory):
   - 6 实体: 产品/仓库/库存/供应商/采购单/销售单
   - 12 权限、6 页面、完整 manifest
   - WASM 编译验证通过

4. 插件热更新:
   - POST /api/v1/admin/plugins/{id}/upgrade
   - manifest 对比 + 增量 DDL + WASM 热加载
   - 失败保持旧版本继续运行

5. 文档更新: CLAUDE.md + wiki/index.md 同步 Q2-Q4 进度
2026-04-17 22:17:47 +08:00

191 lines
6.5 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.

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,
}
}
/// 构建测试用 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(),
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,
}],
}),
events: None,
ui: None,
permissions: 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 的数据");
}