P2 平台通用服务: - manifest 扩展: settings/numbering/templates/trigger_events/importable/exportable 声明 - 插件配置 UI: PluginSettingsForm 自动表单 + 后端校验 + 详情抽屉 Settings 标签页 - 编号规则: Host API numbering-generate + PostgreSQL 序列 + manifest 绑定 - 触发事件: data_service create/update/delete 自动发布 DomainEvent - WIT 接口: 新增 numbering-generate/setting-get Host API P3 质量保障: - plugin_validator.rs: 安全扫描(WASM大小/实体数量/字段校验) + 复杂度评分 - 运行时监控指标: RuntimeMetrics (错误率/响应时间/Fuel/内存) - 性能基准: BenchmarkResult 阈值定义 - 上传时自动安全扫描 + /validate API 端点 P4 插件市场: - 数据库迁移: plugin_market_entries + plugin_market_reviews 表 - 前端 PluginMarket 页面: 分类浏览/搜索/详情/评分 - 路由注册: /plugins/market 测试: 269 全通过 (71 erp-plugin + 41 auth + 57 config + 34 core + 50 message + 16 workflow)
200 lines
6.7 KiB
Rust
200 lines
6.7 KiB
Rust
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 的数据");
|
||
}
|