test: add 149 unit tests across core, auth, config, message crates
Test coverage increased from ~34 to 183 tests (zero failures): - erp-core (21): version check, pagination, API response, error mapping - erp-auth (39): org tree building, DTO validation, error conversion, password hashing, user model mapping - erp-config (57): DTO validation, numbering reset logic, menu tree building, error conversion. Fixed BatchSaveMenusReq nested validation - erp-message (50): DTO validation, template rendering, query defaults, error conversion - erp-workflow (16): unchanged (parser + expression tests) All tests are pure unit tests requiring no database.
This commit is contained in:
@@ -113,7 +113,7 @@ pub struct UpdateMenuReq {
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct BatchSaveMenusReq {
|
||||
#[validate(length(min = 1, message = "菜单列表不能为空"))]
|
||||
#[validate(length(min = 1, message = "菜单列表不能为空"), nested)]
|
||||
pub menus: Vec<MenuItemReq>,
|
||||
}
|
||||
|
||||
@@ -239,3 +239,431 @@ pub struct LanguageResp {
|
||||
pub struct UpdateLanguageReq {
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use validator::Validate;
|
||||
|
||||
// ---- CreateDictionaryReq 验证 ----
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_req_valid() {
|
||||
let req = CreateDictionaryReq {
|
||||
name: "状态字典".to_string(),
|
||||
code: "status".to_string(),
|
||||
description: Some("通用状态".to_string()),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_req_empty_name_fails() {
|
||||
let req = CreateDictionaryReq {
|
||||
name: "".to_string(),
|
||||
code: "status".to_string(),
|
||||
description: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_req_empty_code_fails() {
|
||||
let req = CreateDictionaryReq {
|
||||
name: "状态字典".to_string(),
|
||||
code: "".to_string(),
|
||||
description: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_req_name_too_long_fails() {
|
||||
let req = CreateDictionaryReq {
|
||||
name: "x".repeat(101),
|
||||
code: "status".to_string(),
|
||||
description: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_req_code_too_long_fails() {
|
||||
let req = CreateDictionaryReq {
|
||||
name: "状态字典".to_string(),
|
||||
code: "x".repeat(51),
|
||||
description: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_req_max_boundary_ok() {
|
||||
let req = CreateDictionaryReq {
|
||||
name: "x".repeat(100),
|
||||
code: "x".repeat(50),
|
||||
description: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
// ---- CreateDictionaryItemReq 验证 ----
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_item_req_valid() {
|
||||
let req = CreateDictionaryItemReq {
|
||||
label: "启用".to_string(),
|
||||
value: "active".to_string(),
|
||||
sort_order: Some(1),
|
||||
color: Some("#00FF00".to_string()),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_item_req_empty_label_fails() {
|
||||
let req = CreateDictionaryItemReq {
|
||||
label: "".to_string(),
|
||||
value: "active".to_string(),
|
||||
sort_order: None,
|
||||
color: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_item_req_empty_value_fails() {
|
||||
let req = CreateDictionaryItemReq {
|
||||
label: "启用".to_string(),
|
||||
value: "".to_string(),
|
||||
sort_order: None,
|
||||
color: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_item_req_label_too_long_fails() {
|
||||
let req = CreateDictionaryItemReq {
|
||||
label: "x".repeat(101),
|
||||
value: "active".to_string(),
|
||||
sort_order: None,
|
||||
color: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_item_req_value_too_long_fails() {
|
||||
let req = CreateDictionaryItemReq {
|
||||
label: "启用".to_string(),
|
||||
value: "x".repeat(101),
|
||||
sort_order: None,
|
||||
color: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_item_req_min_boundary_ok() {
|
||||
let req = CreateDictionaryItemReq {
|
||||
label: "x".to_string(),
|
||||
value: "x".to_string(),
|
||||
sort_order: None,
|
||||
color: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dictionary_item_req_max_boundary_ok() {
|
||||
let req = CreateDictionaryItemReq {
|
||||
label: "x".repeat(100),
|
||||
value: "x".repeat(100),
|
||||
sort_order: Some(99),
|
||||
color: Some("#FFFFFF".to_string()),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
// ---- CreateMenuReq 验证 ----
|
||||
|
||||
#[test]
|
||||
fn create_menu_req_valid() {
|
||||
let req = CreateMenuReq {
|
||||
parent_id: None,
|
||||
title: "系统设置".to_string(),
|
||||
path: Some("/settings".to_string()),
|
||||
icon: Some("SettingOutlined".to_string()),
|
||||
sort_order: Some(1),
|
||||
visible: Some(true),
|
||||
menu_type: Some("menu".to_string()),
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_menu_req_empty_title_fails() {
|
||||
let req = CreateMenuReq {
|
||||
parent_id: None,
|
||||
title: "".to_string(),
|
||||
path: None,
|
||||
icon: None,
|
||||
sort_order: None,
|
||||
visible: None,
|
||||
menu_type: None,
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_menu_req_title_too_long_fails() {
|
||||
let req = CreateMenuReq {
|
||||
parent_id: None,
|
||||
title: "x".repeat(101),
|
||||
path: None,
|
||||
icon: None,
|
||||
sort_order: None,
|
||||
visible: None,
|
||||
menu_type: None,
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_menu_req_title_max_boundary_ok() {
|
||||
let req = CreateMenuReq {
|
||||
parent_id: None,
|
||||
title: "x".repeat(100),
|
||||
path: None,
|
||||
icon: None,
|
||||
sort_order: None,
|
||||
visible: None,
|
||||
menu_type: None,
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
// ---- BatchSaveMenusReq 验证 ----
|
||||
|
||||
#[test]
|
||||
fn batch_save_menus_req_valid() {
|
||||
let req = BatchSaveMenusReq {
|
||||
menus: vec![MenuItemReq {
|
||||
id: None,
|
||||
parent_id: None,
|
||||
title: "首页".to_string(),
|
||||
path: Some("/home".to_string()),
|
||||
icon: None,
|
||||
sort_order: Some(0),
|
||||
visible: Some(true),
|
||||
menu_type: Some("menu".to_string()),
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
version: None,
|
||||
}],
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_save_menus_req_empty_list_fails() {
|
||||
let req = BatchSaveMenusReq { menus: vec![] };
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_save_menus_req_item_empty_title_fails() {
|
||||
let req = BatchSaveMenusReq {
|
||||
menus: vec![MenuItemReq {
|
||||
id: None,
|
||||
parent_id: None,
|
||||
title: "".to_string(),
|
||||
path: None,
|
||||
icon: None,
|
||||
sort_order: None,
|
||||
visible: None,
|
||||
menu_type: None,
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
version: None,
|
||||
}],
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_save_menus_req_item_title_too_long_fails() {
|
||||
let req = BatchSaveMenusReq {
|
||||
menus: vec![MenuItemReq {
|
||||
id: None,
|
||||
parent_id: None,
|
||||
title: "x".repeat(101),
|
||||
path: None,
|
||||
icon: None,
|
||||
sort_order: None,
|
||||
visible: None,
|
||||
menu_type: None,
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
version: None,
|
||||
}],
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_save_menus_req_multiple_items_ok() {
|
||||
let req = BatchSaveMenusReq {
|
||||
menus: vec![
|
||||
MenuItemReq {
|
||||
id: None,
|
||||
parent_id: None,
|
||||
title: "菜单A".to_string(),
|
||||
path: Some("/a".to_string()),
|
||||
icon: None,
|
||||
sort_order: Some(0),
|
||||
visible: Some(true),
|
||||
menu_type: Some("menu".to_string()),
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
version: None,
|
||||
},
|
||||
MenuItemReq {
|
||||
id: None,
|
||||
parent_id: None,
|
||||
title: "菜单B".to_string(),
|
||||
path: Some("/b".to_string()),
|
||||
icon: None,
|
||||
sort_order: Some(1),
|
||||
visible: Some(true),
|
||||
menu_type: Some("menu".to_string()),
|
||||
permission: None,
|
||||
role_ids: None,
|
||||
version: Some(1),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
// ---- CreateNumberingRuleReq 验证 ----
|
||||
|
||||
#[test]
|
||||
fn create_numbering_rule_req_valid() {
|
||||
let req = CreateNumberingRuleReq {
|
||||
name: "订单编号".to_string(),
|
||||
code: "ORDER".to_string(),
|
||||
prefix: Some("ORD".to_string()),
|
||||
date_format: Some("%Y%m%d".to_string()),
|
||||
seq_length: Some(4),
|
||||
seq_start: Some(1),
|
||||
separator: Some("-".to_string()),
|
||||
reset_cycle: Some("daily".to_string()),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_numbering_rule_req_empty_name_fails() {
|
||||
let req = CreateNumberingRuleReq {
|
||||
name: "".to_string(),
|
||||
code: "ORDER".to_string(),
|
||||
prefix: None,
|
||||
date_format: None,
|
||||
seq_length: None,
|
||||
seq_start: None,
|
||||
separator: None,
|
||||
reset_cycle: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_numbering_rule_req_empty_code_fails() {
|
||||
let req = CreateNumberingRuleReq {
|
||||
name: "订单编号".to_string(),
|
||||
code: "".to_string(),
|
||||
prefix: None,
|
||||
date_format: None,
|
||||
seq_length: None,
|
||||
seq_start: None,
|
||||
separator: None,
|
||||
reset_cycle: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_numbering_rule_req_name_too_long_fails() {
|
||||
let req = CreateNumberingRuleReq {
|
||||
name: "x".repeat(101),
|
||||
code: "ORDER".to_string(),
|
||||
prefix: None,
|
||||
date_format: None,
|
||||
seq_length: None,
|
||||
seq_start: None,
|
||||
separator: None,
|
||||
reset_cycle: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_numbering_rule_req_code_too_long_fails() {
|
||||
let req = CreateNumberingRuleReq {
|
||||
name: "订单编号".to_string(),
|
||||
code: "x".repeat(51),
|
||||
prefix: None,
|
||||
date_format: None,
|
||||
seq_length: None,
|
||||
seq_start: None,
|
||||
separator: None,
|
||||
reset_cycle: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_numbering_rule_req_max_boundary_ok() {
|
||||
let req = CreateNumberingRuleReq {
|
||||
name: "x".repeat(100),
|
||||
code: "x".repeat(50),
|
||||
prefix: None,
|
||||
date_format: None,
|
||||
seq_length: None,
|
||||
seq_start: None,
|
||||
separator: None,
|
||||
reset_cycle: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
// ---- UpdateSettingReq 验证 ----
|
||||
|
||||
#[test]
|
||||
fn update_setting_req_valid() {
|
||||
let req = UpdateSettingReq {
|
||||
setting_value: serde_json::json!({"key": "value"}),
|
||||
version: Some(1),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_setting_req_without_version_ok() {
|
||||
let req = UpdateSettingReq {
|
||||
setting_value: serde_json::json!("hello"),
|
||||
version: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,76 @@ impl From<ConfigError> for AppError {
|
||||
}
|
||||
|
||||
pub type ConfigResult<T> = Result<T, ConfigError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use erp_core::error::AppError;
|
||||
|
||||
#[test]
|
||||
fn config_error_validation_maps_to_app_validation() {
|
||||
let app: AppError = ConfigError::Validation("字段不能为空".to_string()).into();
|
||||
match app {
|
||||
AppError::Validation(msg) => assert_eq!(msg, "字段不能为空"),
|
||||
other => panic!("期望 Validation,实际得到 {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_error_not_found_maps_to_app_not_found() {
|
||||
let app: AppError = ConfigError::NotFound("字典不存在".to_string()).into();
|
||||
match app {
|
||||
AppError::NotFound(msg) => assert_eq!(msg, "字典不存在"),
|
||||
other => panic!("期望 NotFound,实际得到 {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_error_duplicate_key_maps_to_app_conflict() {
|
||||
let app: AppError = ConfigError::DuplicateKey("编码已存在".to_string()).into();
|
||||
match app {
|
||||
AppError::Conflict(msg) => assert_eq!(msg, "编码已存在"),
|
||||
other => panic!("期望 Conflict,实际得到 {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_error_numbering_exhausted_maps_to_app_internal() {
|
||||
let app: AppError = ConfigError::NumberingExhausted("序列已耗尽".to_string()).into();
|
||||
match app {
|
||||
AppError::Internal(msg) => assert!(msg.contains("序列已耗尽")),
|
||||
other => panic!("期望 Internal,实际得到 {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_error_version_mismatch_maps_to_app_version_mismatch() {
|
||||
let app: AppError = ConfigError::VersionMismatch.into();
|
||||
match app {
|
||||
AppError::VersionMismatch => {}
|
||||
other => panic!("期望 VersionMismatch,实际得到 {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_error_display_messages() {
|
||||
// 验证各变体的 Display 输出包含中文描述
|
||||
assert!(ConfigError::Validation("test".into()).to_string().contains("验证失败"));
|
||||
assert!(ConfigError::NotFound("test".into()).to_string().contains("资源未找到"));
|
||||
assert!(ConfigError::DuplicateKey("test".into()).to_string().contains("键已存在"));
|
||||
assert!(ConfigError::NumberingExhausted("test".into()).to_string().contains("编号序列耗尽"));
|
||||
assert!(ConfigError::VersionMismatch.to_string().contains("版本冲突"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_error_connection_maps_to_validation() {
|
||||
// TransactionError::Connection 应该转换为 ConfigError::Validation
|
||||
let config_err: ConfigError =
|
||||
sea_orm::TransactionError::Connection(sea_orm::DbErr::Conn(sea_orm::RuntimeErr::Internal("连接失败".to_string())))
|
||||
.into();
|
||||
match config_err {
|
||||
ConfigError::Validation(msg) => assert!(msg.contains("连接失败")),
|
||||
other => panic!("期望 Validation,实际得到 {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,3 +387,163 @@ impl MenuService {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
|
||||
/// 辅助:构造 menu::Model
|
||||
fn make_menu(id: Uuid, parent_id: Option<Uuid>, title: &str, sort_order: i32) -> menu::Model {
|
||||
let now = Utc::now();
|
||||
let tenant_id = Uuid::now_v7();
|
||||
menu::Model {
|
||||
id,
|
||||
tenant_id,
|
||||
parent_id,
|
||||
title: title.to_string(),
|
||||
path: Some(format!("/{}", title.to_lowercase())),
|
||||
icon: None,
|
||||
sort_order,
|
||||
visible: true,
|
||||
menu_type: "menu".to_string(),
|
||||
permission: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: tenant_id,
|
||||
updated_by: tenant_id,
|
||||
deleted_at: None,
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tree_empty_input() {
|
||||
let nodes: Vec<&menu::Model> = vec![];
|
||||
let children_map: HashMap<Option<Uuid>, Vec<&menu::Model>> = HashMap::new();
|
||||
let tree = MenuService::build_tree(&nodes, &children_map);
|
||||
assert!(tree.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tree_single_root() {
|
||||
let root_id = Uuid::now_v7();
|
||||
let root = make_menu(root_id, None, "首页", 0);
|
||||
|
||||
let children_map: HashMap<Option<Uuid>, Vec<&menu::Model>> = HashMap::new();
|
||||
let roots: Vec<&menu::Model> = vec![&root];
|
||||
let tree = MenuService::build_tree(&roots, &children_map);
|
||||
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert_eq!(tree[0].id, root_id);
|
||||
assert_eq!(tree[0].title, "首页");
|
||||
assert!(tree[0].children.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tree_two_levels() {
|
||||
// 根节点 -> 子节点1, 子节点2
|
||||
let root_id = Uuid::now_v7();
|
||||
let child1_id = Uuid::now_v7();
|
||||
let child2_id = Uuid::now_v7();
|
||||
|
||||
let root = make_menu(root_id, None, "系统管理", 0);
|
||||
let child1 = make_menu(child1_id, Some(root_id), "用户管理", 1);
|
||||
let child2 = make_menu(child2_id, Some(root_id), "角色管理", 2);
|
||||
|
||||
let mut children_map: HashMap<Option<Uuid>, Vec<&menu::Model>> = HashMap::new();
|
||||
children_map.insert(Some(root_id), vec![&child1, &child2]);
|
||||
|
||||
let roots: Vec<&menu::Model> = vec![&root];
|
||||
let tree = MenuService::build_tree(&roots, &children_map);
|
||||
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert_eq!(tree[0].children.len(), 2);
|
||||
assert_eq!(tree[0].children[0].title, "用户管理");
|
||||
assert_eq!(tree[0].children[1].title, "角色管理");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tree_three_levels() {
|
||||
// 根 -> 子 -> 孙
|
||||
let root_id = Uuid::now_v7();
|
||||
let child_id = Uuid::now_v7();
|
||||
let grandchild_id = Uuid::now_v7();
|
||||
|
||||
let root = make_menu(root_id, None, "系统管理", 0);
|
||||
let child = make_menu(child_id, Some(root_id), "用户管理", 1);
|
||||
let grandchild = make_menu(grandchild_id, Some(child_id), "用户详情", 0);
|
||||
|
||||
let mut children_map: HashMap<Option<Uuid>, Vec<&menu::Model>> = HashMap::new();
|
||||
children_map.insert(Some(root_id), vec![&child]);
|
||||
children_map.insert(Some(child_id), vec![&grandchild]);
|
||||
|
||||
let roots: Vec<&menu::Model> = vec![&root];
|
||||
let tree = MenuService::build_tree(&roots, &children_map);
|
||||
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert_eq!(tree[0].children.len(), 1);
|
||||
assert_eq!(tree[0].children[0].children.len(), 1);
|
||||
assert_eq!(tree[0].children[0].children[0].title, "用户详情");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tree_multiple_roots() {
|
||||
// 两个独立的根节点
|
||||
let root1_id = Uuid::now_v7();
|
||||
let root2_id = Uuid::now_v7();
|
||||
|
||||
let root1 = make_menu(root1_id, None, "首页", 0);
|
||||
let root2 = make_menu(root2_id, None, "系统管理", 1);
|
||||
|
||||
let children_map: HashMap<Option<Uuid>, Vec<&menu::Model>> = HashMap::new();
|
||||
let roots: Vec<&menu::Model> = vec![&root1, &root2];
|
||||
let tree = MenuService::build_tree(&roots, &children_map);
|
||||
|
||||
assert_eq!(tree.len(), 2);
|
||||
assert_eq!(tree[0].title, "首页");
|
||||
assert_eq!(tree[1].title, "系统管理");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tree_preserves_model_fields() {
|
||||
let root_id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
let tenant_id = Uuid::now_v7();
|
||||
|
||||
let root = menu::Model {
|
||||
id: root_id,
|
||||
tenant_id,
|
||||
parent_id: None,
|
||||
title: "设置".to_string(),
|
||||
path: Some("/settings".to_string()),
|
||||
icon: Some("SettingOutlined".to_string()),
|
||||
sort_order: 5,
|
||||
visible: false,
|
||||
menu_type: "directory".to_string(),
|
||||
permission: Some("settings:view".to_string()),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: tenant_id,
|
||||
updated_by: tenant_id,
|
||||
deleted_at: None,
|
||||
version: 3,
|
||||
};
|
||||
|
||||
let children_map: HashMap<Option<Uuid>, Vec<&menu::Model>> = HashMap::new();
|
||||
let roots: Vec<&menu::Model> = vec![&root];
|
||||
let tree = MenuService::build_tree(&roots, &children_map);
|
||||
|
||||
assert_eq!(tree.len(), 1);
|
||||
let node = &tree[0];
|
||||
assert_eq!(node.id, root_id);
|
||||
assert_eq!(node.title, "设置");
|
||||
assert_eq!(node.path, Some("/settings".to_string()));
|
||||
assert_eq!(node.icon, Some("SettingOutlined".to_string()));
|
||||
assert_eq!(node.sort_order, 5);
|
||||
assert!(!node.visible);
|
||||
assert_eq!(node.menu_type, "directory");
|
||||
assert_eq!(node.permission, Some("settings:view".to_string()));
|
||||
assert_eq!(node.version, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,3 +440,236 @@ impl NumberingService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
/// 辅助:构造 NaiveDate
|
||||
fn date(y: i32, m: u32, d: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(y, m, d).unwrap()
|
||||
}
|
||||
|
||||
// ---- maybe_reset_sequence 测试 ----
|
||||
|
||||
#[test]
|
||||
fn reset_never_keeps_current() {
|
||||
// "never" 周期:永远不重置,保持 seq_current
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
100,
|
||||
1,
|
||||
"never",
|
||||
Some(date(2025, 1, 1)),
|
||||
date(2026, 4, 15),
|
||||
);
|
||||
assert_eq!(result, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_unknown_cycle_keeps_current() {
|
||||
// 未知周期值等同于不重置
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
50,
|
||||
1,
|
||||
"weekly",
|
||||
Some(date(2025, 1, 1)),
|
||||
date(2026, 4, 15),
|
||||
);
|
||||
assert_eq!(result, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_daily_same_day_keeps_current() {
|
||||
// 同一天内不重置
|
||||
let today = date(2026, 4, 15);
|
||||
let result = NumberingService::maybe_reset_sequence(42, 1, "daily", Some(today), today);
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_daily_different_day_resets() {
|
||||
// 不同天重置为 seq_start
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
42,
|
||||
1,
|
||||
"daily",
|
||||
Some(date(2026, 4, 14)),
|
||||
date(2026, 4, 15),
|
||||
);
|
||||
assert_eq!(result, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_daily_resets_with_custom_start() {
|
||||
// 重置时使用自定义 seq_start
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
99,
|
||||
10,
|
||||
"daily",
|
||||
Some(date(2026, 4, 10)),
|
||||
date(2026, 4, 15),
|
||||
);
|
||||
assert_eq!(result, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_monthly_same_month_keeps_current() {
|
||||
// 同月不重置
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
30,
|
||||
1,
|
||||
"monthly",
|
||||
Some(date(2026, 4, 1)),
|
||||
date(2026, 4, 15),
|
||||
);
|
||||
assert_eq!(result, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_monthly_different_month_resets() {
|
||||
// 不同月份重置
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
30,
|
||||
1,
|
||||
"monthly",
|
||||
Some(date(2026, 3, 31)),
|
||||
date(2026, 4, 1),
|
||||
);
|
||||
assert_eq!(result, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_monthly_same_month_different_year_resets() {
|
||||
// 不同年份但相同月份数字,仍然重置
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
20,
|
||||
5,
|
||||
"monthly",
|
||||
Some(date(2025, 4, 15)),
|
||||
date(2026, 4, 15),
|
||||
);
|
||||
assert_eq!(result, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_yearly_same_year_keeps_current() {
|
||||
// 同年不重置
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
50,
|
||||
1,
|
||||
"yearly",
|
||||
Some(date(2026, 1, 1)),
|
||||
date(2026, 12, 31),
|
||||
);
|
||||
assert_eq!(result, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_yearly_different_year_resets() {
|
||||
// 不同年份重置
|
||||
let result = NumberingService::maybe_reset_sequence(
|
||||
50,
|
||||
1,
|
||||
"yearly",
|
||||
Some(date(2025, 12, 31)),
|
||||
date(2026, 1, 1),
|
||||
);
|
||||
assert_eq!(result, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_no_last_reset_date_returns_seq_start() {
|
||||
// 从未重置过,使用 seq_start
|
||||
let result = NumberingService::maybe_reset_sequence(999, 1, "daily", None, date(2026, 4, 15));
|
||||
assert_eq!(result, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_no_last_reset_date_uses_custom_start() {
|
||||
// 从未重置过,使用自定义 seq_start
|
||||
let result =
|
||||
NumberingService::maybe_reset_sequence(999, 42, "monthly", None, date(2026, 4, 15));
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
// ---- model_to_resp 测试 ----
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_maps_fields_correctly() {
|
||||
let id = Uuid::now_v7();
|
||||
let tenant_id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
let today = now.date_naive();
|
||||
|
||||
let model = numbering_rule::Model {
|
||||
id,
|
||||
tenant_id,
|
||||
name: "订单编号".to_string(),
|
||||
code: "ORDER".to_string(),
|
||||
prefix: "ORD".to_string(),
|
||||
date_format: Some("%Y%m%d".to_string()),
|
||||
seq_length: 6,
|
||||
seq_start: 1,
|
||||
seq_current: 42,
|
||||
separator: "-".to_string(),
|
||||
reset_cycle: "daily".to_string(),
|
||||
last_reset_date: Some(today),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: tenant_id,
|
||||
updated_by: tenant_id,
|
||||
deleted_at: None,
|
||||
version: 3,
|
||||
};
|
||||
|
||||
let resp = NumberingService::model_to_resp(&model);
|
||||
|
||||
assert_eq!(resp.id, id);
|
||||
assert_eq!(resp.name, "订单编号");
|
||||
assert_eq!(resp.code, "ORDER");
|
||||
assert_eq!(resp.prefix, "ORD");
|
||||
assert_eq!(resp.date_format, Some("%Y%m%d".to_string()));
|
||||
assert_eq!(resp.seq_length, 6);
|
||||
assert_eq!(resp.seq_start, 1);
|
||||
assert_eq!(resp.seq_current, 42);
|
||||
assert_eq!(resp.separator, "-");
|
||||
assert_eq!(resp.reset_cycle, "daily");
|
||||
assert_eq!(resp.last_reset_date, Some(today.to_string()));
|
||||
assert_eq!(resp.version, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_none_fields() {
|
||||
let id = Uuid::now_v7();
|
||||
let tenant_id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
|
||||
let model = numbering_rule::Model {
|
||||
id,
|
||||
tenant_id,
|
||||
name: "简单编号".to_string(),
|
||||
code: "SIMPLE".to_string(),
|
||||
prefix: "".to_string(),
|
||||
date_format: None,
|
||||
seq_length: 4,
|
||||
seq_start: 1,
|
||||
seq_current: 1,
|
||||
separator: "-".to_string(),
|
||||
reset_cycle: "never".to_string(),
|
||||
last_reset_date: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: tenant_id,
|
||||
updated_by: tenant_id,
|
||||
deleted_at: None,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
let resp = NumberingService::model_to_resp(&model);
|
||||
|
||||
assert_eq!(resp.date_format, None);
|
||||
assert_eq!(resp.last_reset_date, None);
|
||||
assert_eq!(resp.prefix, "");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user