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 统一格式化
This commit is contained in:
@@ -10,7 +10,13 @@ use crate::manifest::{PluginEntity, PluginField, PluginFieldType};
|
||||
pub(crate) fn sanitize_identifier(input: &str) -> String {
|
||||
input
|
||||
.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
|
||||
.map(|c| {
|
||||
if c.is_ascii_alphanumeric() || c == '_' {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -56,7 +62,9 @@ impl DynamicTableManager {
|
||||
|
||||
let col_name = format!("_f_{}", sanitize_identifier(&field.name));
|
||||
let sql_type = field.field_type.generated_sql_type();
|
||||
let expr = field.field_type.generated_expr(&sanitize_identifier(&field.name));
|
||||
let expr = field
|
||||
.field_type
|
||||
.generated_expr(&sanitize_identifier(&field.name));
|
||||
|
||||
gen_cols.push(format!(
|
||||
" \"{}\" {} GENERATED ALWAYS AS ({}) STORED",
|
||||
@@ -80,8 +88,7 @@ impl DynamicTableManager {
|
||||
|
||||
// pg_trgm 索引
|
||||
for field in &entity.fields {
|
||||
if field.searchable == Some(true)
|
||||
&& matches!(field.field_type, PluginFieldType::String)
|
||||
if field.searchable == Some(true) && matches!(field.field_type, PluginFieldType::String)
|
||||
{
|
||||
let sf = sanitize_identifier(&field.name);
|
||||
indexes.push(format!(
|
||||
@@ -128,11 +135,7 @@ impl DynamicTableManager {
|
||||
entity: &PluginEntity,
|
||||
) -> PluginResult<()> {
|
||||
let ddl = Self::build_create_table_sql(plugin_id, entity);
|
||||
for sql in ddl
|
||||
.split(';')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
for sql in ddl.split(';').map(|s| s.trim()).filter(|s| !s.is_empty()) {
|
||||
tracing::info!(sql = %sql, "Executing DDL");
|
||||
db.execute_unprepared(sql).await.map_err(|e| {
|
||||
tracing::error!(sql = %sql, error = %e, "DDL execution failed");
|
||||
@@ -179,21 +182,25 @@ impl DynamicTableManager {
|
||||
continue;
|
||||
}
|
||||
// 新增字段 + 需要 Generated Column 的条件
|
||||
let needs_gen = field.unique
|
||||
|| field.sortable == Some(true)
|
||||
|| field.filterable == Some(true);
|
||||
let needs_gen =
|
||||
field.unique || field.sortable == Some(true) || field.filterable == Some(true);
|
||||
if needs_gen {
|
||||
new_filterable.push(field.clone());
|
||||
if field.sortable == Some(true) {
|
||||
new_sortable.push(field.clone());
|
||||
}
|
||||
}
|
||||
if field.searchable == Some(true) && matches!(field.field_type, PluginFieldType::String) {
|
||||
if field.searchable == Some(true) && matches!(field.field_type, PluginFieldType::String)
|
||||
{
|
||||
new_searchable.push(field.clone());
|
||||
}
|
||||
}
|
||||
|
||||
FieldDiff { new_filterable, new_sortable, new_searchable }
|
||||
FieldDiff {
|
||||
new_filterable,
|
||||
new_sortable,
|
||||
new_searchable,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema 演进:为已有实体新增 Generated Column 和索引
|
||||
@@ -212,7 +219,9 @@ impl DynamicTableManager {
|
||||
}
|
||||
let col_name = format!("_f_{}", sanitize_identifier(&field.name));
|
||||
let sql_type = field.field_type.generated_sql_type();
|
||||
let expr = field.field_type.generated_expr(&sanitize_identifier(&field.name));
|
||||
let expr = field
|
||||
.field_type
|
||||
.generated_expr(&sanitize_identifier(&field.name));
|
||||
let _safe_field = sanitize_identifier(&field.name);
|
||||
|
||||
statements.push(format!(
|
||||
@@ -329,7 +338,11 @@ impl DynamicTableManager {
|
||||
LIMIT $2 OFFSET $3",
|
||||
table_name
|
||||
);
|
||||
let values = vec![tenant_id.into(), (limit as i64).into(), (offset as i64).into()];
|
||||
let values = vec![
|
||||
tenant_id.into(),
|
||||
(limit as i64).into(),
|
||||
(offset as i64).into(),
|
||||
];
|
||||
(sql, values)
|
||||
}
|
||||
|
||||
@@ -398,7 +411,9 @@ impl DynamicTableManager {
|
||||
table_name, set_expr
|
||||
);
|
||||
let values = vec![
|
||||
serde_json::to_string(&partial_data).unwrap_or_default().into(),
|
||||
serde_json::to_string(&partial_data)
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
user_id.into(),
|
||||
id.into(),
|
||||
tenant_id.into(),
|
||||
@@ -408,11 +423,7 @@ impl DynamicTableManager {
|
||||
}
|
||||
|
||||
/// 构建 DELETE SQL(软删除)
|
||||
pub fn build_delete_sql(
|
||||
table_name: &str,
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
) -> (String, Vec<Value>) {
|
||||
pub fn build_delete_sql(table_name: &str, id: Uuid, tenant_id: Uuid) -> (String, Vec<Value>) {
|
||||
let sql = format!(
|
||||
"UPDATE \"{}\" \
|
||||
SET deleted_at = NOW(), updated_at = NOW() \
|
||||
@@ -469,19 +480,19 @@ impl DynamicTableManager {
|
||||
let mut values: Vec<Value> = vec![tenant_id.into()];
|
||||
|
||||
// 处理 filter(与 build_filtered_query_sql 保持一致)
|
||||
if let Some(f) = filter {
|
||||
if let Some(obj) = f.as_object() {
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
if let Some(f) = filter
|
||||
&& let Some(obj) = f.as_object()
|
||||
{
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,19 +544,19 @@ impl DynamicTableManager {
|
||||
let mut values: Vec<Value> = vec![tenant_id.into()];
|
||||
|
||||
// 处理 filter
|
||||
if let Some(f) = filter {
|
||||
if let Some(obj) = f.as_object() {
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
if let Some(f) = filter
|
||||
&& let Some(obj) = f.as_object()
|
||||
{
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,19 +595,19 @@ impl DynamicTableManager {
|
||||
let mut param_idx = 2;
|
||||
let mut values: Vec<Value> = vec![tenant_id.into()];
|
||||
|
||||
if let Some(f) = filter {
|
||||
if let Some(obj) = f.as_object() {
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
if let Some(f) = filter
|
||||
&& let Some(obj) = f.as_object()
|
||||
{
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,16 +621,20 @@ impl DynamicTableManager {
|
||||
let func_lower = func.to_lowercase();
|
||||
match func_lower.as_str() {
|
||||
"sum" => select_parts.push(format!(
|
||||
"COALESCE(SUM(\"_f_{}\"), 0) as sum_{}", clean_field, clean_field
|
||||
"COALESCE(SUM(\"_f_{}\"), 0) as sum_{}",
|
||||
clean_field, clean_field
|
||||
)),
|
||||
"avg" => select_parts.push(format!(
|
||||
"COALESCE(AVG(\"_f_{}\"), 0) as avg_{}", clean_field, clean_field
|
||||
"COALESCE(AVG(\"_f_{}\"), 0) as avg_{}",
|
||||
clean_field, clean_field
|
||||
)),
|
||||
"min" => select_parts.push(format!(
|
||||
"MIN(\"_f_{}\") as min_{}", clean_field, clean_field
|
||||
"MIN(\"_f_{}\") as min_{}",
|
||||
clean_field, clean_field
|
||||
)),
|
||||
"max" => select_parts.push(format!(
|
||||
"MAX(\"_f_{}\") as max_{}", clean_field, clean_field
|
||||
"MAX(\"_f_{}\") as max_{}",
|
||||
clean_field, clean_field
|
||||
)),
|
||||
_ => {}
|
||||
}
|
||||
@@ -641,6 +656,7 @@ impl DynamicTableManager {
|
||||
}
|
||||
|
||||
/// 构建带过滤条件的查询 SQL
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn build_filtered_query_sql(
|
||||
table_name: &str,
|
||||
tenant_id: Uuid,
|
||||
@@ -659,19 +675,19 @@ impl DynamicTableManager {
|
||||
let mut values: Vec<Value> = vec![tenant_id.into()];
|
||||
|
||||
// 处理 filter
|
||||
if let Some(f) = filter {
|
||||
if let Some(obj) = f.as_object() {
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
if let Some(f) = filter
|
||||
&& let Some(obj) = f.as_object()
|
||||
{
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("\"data\"->>'{}' = ${}", clean_key, param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,6 +750,7 @@ impl DynamicTableManager {
|
||||
}
|
||||
|
||||
/// 扩展版查询构建 — 支持 Generated Column 路由
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn build_filtered_query_sql_ex(
|
||||
table_name: &str,
|
||||
tenant_id: Uuid,
|
||||
@@ -755,19 +772,19 @@ impl DynamicTableManager {
|
||||
let mut values: Vec<Value> = vec![tenant_id.into()];
|
||||
|
||||
// filter
|
||||
if let Some(f) = filter {
|
||||
if let Some(obj) = f.as_object() {
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("{} = ${}", ref_fn(&clean_key), param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
if let Some(f) = filter
|
||||
&& let Some(obj) = f.as_object()
|
||||
{
|
||||
for (key, val) in obj {
|
||||
let clean_key = sanitize_identifier(key);
|
||||
if clean_key.is_empty() {
|
||||
return Err(format!("无效的过滤字段名: {}", key));
|
||||
}
|
||||
conditions.push(format!("{} = ${}", ref_fn(&clean_key), param_idx));
|
||||
values.push(Value::String(Some(Box::new(
|
||||
val.as_str().unwrap_or("").to_string(),
|
||||
))));
|
||||
param_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,7 +892,8 @@ impl DynamicTableManager {
|
||||
)
|
||||
}
|
||||
}
|
||||
"all" | _ => (String::new(), vec![]),
|
||||
"all" => (String::new(), vec![]),
|
||||
_ => (String::new(), vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,8 +911,8 @@ impl DynamicTableManager {
|
||||
let json_str = BASE64
|
||||
.decode(cursor)
|
||||
.map_err(|e| format!("游标 Base64 解码失败: {}", e))?;
|
||||
let obj: serde_json::Value = serde_json::from_slice(&json_str)
|
||||
.map_err(|e| format!("游标 JSON 解析失败: {}", e))?;
|
||||
let obj: serde_json::Value =
|
||||
serde_json::from_slice(&json_str).map_err(|e| format!("游标 JSON 解析失败: {}", e))?;
|
||||
let values = obj["v"]
|
||||
.as_array()
|
||||
.ok_or("游标缺少 v 字段")?
|
||||
@@ -923,7 +941,7 @@ impl DynamicTableManager {
|
||||
let ref_fn = Self::field_reference_fn(generated_fields);
|
||||
let sort_col = sort_column
|
||||
.as_deref()
|
||||
.map(|s| ref_fn(s))
|
||||
.map(ref_fn)
|
||||
.unwrap_or("\"created_at\"".to_string());
|
||||
|
||||
let mut values: Vec<Value> = vec![tenant_id.into()];
|
||||
@@ -1098,7 +1116,10 @@ mod tests {
|
||||
assert!(sql.contains("ILIKE"), "Expected ILIKE in SQL, got: {}", sql);
|
||||
// 验证搜索参数值包含 %...%
|
||||
if let Value::String(Some(s)) = &values[1] {
|
||||
assert!(s.contains("测试关键词"), "Search value should contain keyword");
|
||||
assert!(
|
||||
s.contains("测试关键词"),
|
||||
"Search value should contain keyword"
|
||||
);
|
||||
assert!(s.starts_with('%'), "Search value should start with %");
|
||||
}
|
||||
}
|
||||
@@ -1188,7 +1209,11 @@ mod tests {
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(sql.contains("\"data\"->>'status' ="), "Expected filter, got: {}", sql);
|
||||
assert!(
|
||||
sql.contains("\"data\"->>'status' ="),
|
||||
"Expected filter, got: {}",
|
||||
sql
|
||||
);
|
||||
assert_eq!(values.len(), 2); // tenant_id + filter_value
|
||||
}
|
||||
|
||||
@@ -1231,8 +1256,16 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(sql.contains("GROUP BY"), "Expected GROUP BY, got: {}", sql);
|
||||
assert!(sql.contains("\"data\"->>'status'"), "Expected group field, got: {}", sql);
|
||||
assert!(sql.contains("ORDER BY count DESC"), "Expected ORDER BY count DESC, got: {}", sql);
|
||||
assert!(
|
||||
sql.contains("\"data\"->>'status'"),
|
||||
"Expected group field, got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("ORDER BY count DESC"),
|
||||
"Expected ORDER BY count DESC, got: {}",
|
||||
sql
|
||||
);
|
||||
assert_eq!(values.len(), 1); // 仅 tenant_id
|
||||
}
|
||||
|
||||
@@ -1245,8 +1278,16 @@ mod tests {
|
||||
Some(serde_json::json!({"status": "active"})),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(sql.contains("\"data\"->>'region'"), "Expected group field, got: {}", sql);
|
||||
assert!(sql.contains("\"data\"->>'status' ="), "Expected filter, got: {}", sql);
|
||||
assert!(
|
||||
sql.contains("\"data\"->>'region'"),
|
||||
"Expected group field, got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("\"data\"->>'status' ="),
|
||||
"Expected filter, got: {}",
|
||||
sql
|
||||
);
|
||||
assert_eq!(values.len(), 2); // tenant_id + filter_value
|
||||
}
|
||||
|
||||
@@ -1260,7 +1301,11 @@ mod tests {
|
||||
);
|
||||
let (sql, _) = result.unwrap();
|
||||
assert!(!sql.contains("DROP TABLE"), "SQL 不应包含注入: {}", sql);
|
||||
assert!(sql.contains("evil___DROP_TABLE__"), "字段名应被清理: {}", sql);
|
||||
assert!(
|
||||
sql.contains("evil___DROP_TABLE__"),
|
||||
"字段名应被清理: {}",
|
||||
sql
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1317,14 +1362,8 @@ mod tests {
|
||||
};
|
||||
|
||||
let sql = DynamicTableManager::build_create_table_sql("erp_crm", &entity);
|
||||
assert!(
|
||||
sql.contains("_f_code"),
|
||||
"应包含 _f_code Generated Column"
|
||||
);
|
||||
assert!(
|
||||
sql.contains("_f_level"),
|
||||
"应包含 _f_level Generated Column"
|
||||
);
|
||||
assert!(sql.contains("_f_code"), "应包含 _f_code Generated Column");
|
||||
assert!(sql.contains("_f_level"), "应包含 _f_level Generated Column");
|
||||
assert!(
|
||||
sql.contains("_f_sort_order"),
|
||||
"应包含 _f_sort_order Generated Column"
|
||||
@@ -1333,10 +1372,7 @@ mod tests {
|
||||
sql.contains("GENERATED ALWAYS AS"),
|
||||
"应包含 GENERATED ALWAYS AS"
|
||||
);
|
||||
assert!(
|
||||
sql.contains("::INTEGER"),
|
||||
"Integer 字段应有类型转换"
|
||||
);
|
||||
assert!(sql.contains("::INTEGER"), "Integer 字段应有类型转换");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1476,10 +1512,7 @@ mod tests {
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
sql.contains("ROW("),
|
||||
"cursor 条件应使用 ROW 比较"
|
||||
);
|
||||
assert!(sql.contains("ROW("), "cursor 条件应使用 ROW 比较");
|
||||
assert!(
|
||||
values.len() >= 4,
|
||||
"应有 tenant_id + cursor_val + cursor_id + limit"
|
||||
@@ -1530,16 +1563,8 @@ mod tests {
|
||||
"department 应使用 IN 条件, got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("$2"),
|
||||
"参数索引应从 2 开始, got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(
|
||||
sql.contains("$3"),
|
||||
"第二个参数索引应为 3, got: {}",
|
||||
sql
|
||||
);
|
||||
assert!(sql.contains("$2"), "参数索引应从 2 开始, got: {}", sql);
|
||||
assert!(sql.contains("$3"), "第二个参数索引应为 3, got: {}", sql);
|
||||
assert_eq!(values.len(), 2);
|
||||
}
|
||||
|
||||
@@ -1684,35 +1709,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_sanitize_removes_special_chars() {
|
||||
let result = sanitize_identifier("table;name'here\"with`special");
|
||||
assert!(
|
||||
!result.contains(';'),
|
||||
"分号应被替换: {}",
|
||||
result
|
||||
);
|
||||
assert!(
|
||||
!result.contains('\''),
|
||||
"单引号应被替换: {}",
|
||||
result
|
||||
);
|
||||
assert!(
|
||||
!result.contains('"'),
|
||||
"双引号应被替换: {}",
|
||||
result
|
||||
);
|
||||
assert!(
|
||||
!result.contains('`'),
|
||||
"反引号应被替换: {}",
|
||||
result
|
||||
);
|
||||
assert!(!result.contains(';'), "分号应被替换: {}", result);
|
||||
assert!(!result.contains('\''), "单引号应被替换: {}", result);
|
||||
assert!(!result.contains('"'), "双引号应被替换: {}", result);
|
||||
assert!(!result.contains('`'), "反引号应被替换: {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_allows_alphanumeric_underscore() {
|
||||
let result = sanitize_identifier("my_table_123");
|
||||
assert_eq!(
|
||||
result, "my_table_123",
|
||||
"合法标识符应原样保留"
|
||||
);
|
||||
assert_eq!(result, "my_table_123", "合法标识符应原样保留");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1723,26 +1729,14 @@ mod tests {
|
||||
"DROP TABLE 注入应被清理为下划线: {}",
|
||||
result
|
||||
);
|
||||
assert!(
|
||||
!result.contains(';'),
|
||||
"不应包含分号: {}",
|
||||
result
|
||||
);
|
||||
assert!(!result.contains(';'), "不应包含分号: {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_handles_sql_comment() {
|
||||
let result = sanitize_identifier("users--");
|
||||
assert_eq!(
|
||||
result, "users__",
|
||||
"SQL 注释应被替换为下划线: {}",
|
||||
result
|
||||
);
|
||||
assert!(
|
||||
!result.contains('-'),
|
||||
"不应包含连字符: {}",
|
||||
result
|
||||
);
|
||||
assert_eq!(result, "users__", "SQL 注释应被替换为下划线: {}", result);
|
||||
assert!(!result.contains('-'), "不应包含连字符: {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1753,11 +1747,7 @@ mod tests {
|
||||
"UNION 注入中空格应被替换为下划线: {}",
|
||||
result
|
||||
);
|
||||
assert!(
|
||||
!result.contains(' '),
|
||||
"不应包含空格: {}",
|
||||
result
|
||||
);
|
||||
assert!(!result.contains(' '), "不应包含空格: {}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user