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:
@@ -209,3 +209,183 @@ pub struct UpdatePositionReq {
|
||||
pub sort_order: Option<i32>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use validator::Validate;
|
||||
|
||||
#[test]
|
||||
fn login_req_valid() {
|
||||
let req = LoginReq {
|
||||
username: "admin".to_string(),
|
||||
password: "password123".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_req_empty_username_fails() {
|
||||
let req = LoginReq {
|
||||
username: "".to_string(),
|
||||
password: "password123".to_string(),
|
||||
};
|
||||
let result = req.validate();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_req_empty_password_fails() {
|
||||
let req = LoginReq {
|
||||
username: "admin".to_string(),
|
||||
password: "".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_req_valid() {
|
||||
let req = CreateUserReq {
|
||||
username: "alice".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
email: Some("alice@example.com".to_string()),
|
||||
phone: None,
|
||||
display_name: Some("Alice".to_string()),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_req_short_password_fails() {
|
||||
let req = CreateUserReq {
|
||||
username: "bob".to_string(),
|
||||
password: "12345".to_string(), // min 6
|
||||
email: None,
|
||||
phone: None,
|
||||
display_name: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_req_empty_username_fails() {
|
||||
let req = CreateUserReq {
|
||||
username: "".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
email: None,
|
||||
phone: None,
|
||||
display_name: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_req_invalid_email_fails() {
|
||||
let req = CreateUserReq {
|
||||
username: "charlie".to_string(),
|
||||
password: "secret123".to_string(),
|
||||
email: Some("not-an-email".to_string()),
|
||||
phone: None,
|
||||
display_name: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_req_long_username_fails() {
|
||||
let req = CreateUserReq {
|
||||
username: "a".repeat(51), // max 50
|
||||
password: "secret123".to_string(),
|
||||
email: None,
|
||||
phone: None,
|
||||
display_name: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_role_req_valid() {
|
||||
let req = CreateRoleReq {
|
||||
name: "管理员".to_string(),
|
||||
code: "admin".to_string(),
|
||||
description: Some("系统管理员".to_string()),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_role_req_empty_name_fails() {
|
||||
let req = CreateRoleReq {
|
||||
name: "".to_string(),
|
||||
code: "admin".to_string(),
|
||||
description: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_role_req_empty_code_fails() {
|
||||
let req = CreateRoleReq {
|
||||
name: "管理员".to_string(),
|
||||
code: "".to_string(),
|
||||
description: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_org_req_valid() {
|
||||
let req = CreateOrganizationReq {
|
||||
name: "总部".to_string(),
|
||||
code: Some("HQ".to_string()),
|
||||
parent_id: None,
|
||||
sort_order: Some(0),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_org_req_empty_name_fails() {
|
||||
let req = CreateOrganizationReq {
|
||||
name: "".to_string(),
|
||||
code: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_dept_req_valid() {
|
||||
let req = CreateDepartmentReq {
|
||||
name: "技术部".to_string(),
|
||||
code: Some("TECH".to_string()),
|
||||
parent_id: None,
|
||||
manager_id: None,
|
||||
sort_order: Some(1),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_position_req_valid() {
|
||||
let req = CreatePositionReq {
|
||||
name: "高级工程师".to_string(),
|
||||
code: Some("SENIOR".to_string()),
|
||||
level: Some(3),
|
||||
sort_order: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_position_req_empty_name_fails() {
|
||||
let req = CreatePositionReq {
|
||||
name: "".to_string(),
|
||||
code: None,
|
||||
level: None,
|
||||
sort_order: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,3 +53,79 @@ impl From<AppError> for AuthError {
|
||||
}
|
||||
|
||||
pub type AuthResult<T> = Result<T, AuthError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use erp_core::error::AppError;
|
||||
|
||||
#[test]
|
||||
fn auth_error_invalid_credentials_maps_to_unauthorized() {
|
||||
let app: AppError = AuthError::InvalidCredentials.into();
|
||||
match app {
|
||||
AppError::Unauthorized => {}
|
||||
other => panic!("Expected Unauthorized, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_error_token_expired_maps_to_unauthorized() {
|
||||
let app: AppError = AuthError::TokenExpired.into();
|
||||
match app {
|
||||
AppError::Unauthorized => {}
|
||||
other => panic!("Expected Unauthorized, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_error_user_disabled_maps_to_forbidden() {
|
||||
let app: AppError = AuthError::UserDisabled("已禁用".to_string()).into();
|
||||
match app {
|
||||
AppError::Forbidden(msg) => assert_eq!(msg, "已禁用"),
|
||||
other => panic!("Expected Forbidden, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_error_hash_error_maps_to_internal() {
|
||||
let app: AppError = AuthError::HashError("argon2 failed".to_string()).into();
|
||||
match app {
|
||||
AppError::Internal(_) => {}
|
||||
other => panic!("Expected Internal, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_error_validation_maps_to_validation() {
|
||||
let app: AppError = AuthError::Validation("用户名已存在".to_string()).into();
|
||||
match app {
|
||||
AppError::Validation(msg) => assert_eq!(msg, "用户名已存在"),
|
||||
other => panic!("Expected Validation, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_error_version_mismatch_roundtrip() {
|
||||
// AuthError::VersionMismatch -> AppError::VersionMismatch -> AuthError::VersionMismatch
|
||||
let app: AppError = AuthError::VersionMismatch.into();
|
||||
match app {
|
||||
AppError::VersionMismatch => {}
|
||||
other => panic!("Expected VersionMismatch, got {:?}", other),
|
||||
}
|
||||
// And back
|
||||
let auth: AuthError = AppError::VersionMismatch.into();
|
||||
match auth {
|
||||
AuthError::VersionMismatch => {}
|
||||
other => panic!("Expected VersionMismatch, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_error_other_maps_to_auth_validation() {
|
||||
let auth: AuthError = AppError::NotFound("not found".to_string()).into();
|
||||
match auth {
|
||||
AuthError::Validation(msg) => assert!(msg.contains("not found")),
|
||||
other => panic!("Expected Validation, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ impl OrgService {
|
||||
///
|
||||
/// Root nodes (parent_id = None) form the top level. Each node recursively
|
||||
/// includes its children grouped by parent_id.
|
||||
fn build_org_tree(items: &[organization::Model]) -> Vec<OrganizationResp> {
|
||||
pub(crate) fn build_org_tree(items: &[organization::Model]) -> Vec<OrganizationResp> {
|
||||
let mut children_map: HashMap<Option<Uuid>, Vec<&organization::Model>> = HashMap::new();
|
||||
for item in items {
|
||||
children_map.entry(item.parent_id).or_default().push(item);
|
||||
@@ -329,3 +329,121 @@ fn build_org_tree(items: &[organization::Model]) -> Vec<OrganizationResp> {
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entity::organization;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn make_org(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
name: &str,
|
||||
parent_id: Option<Uuid>,
|
||||
level: i32,
|
||||
version: i32,
|
||||
) -> organization::Model {
|
||||
organization::Model {
|
||||
id,
|
||||
tenant_id,
|
||||
name: name.to_string(),
|
||||
code: None,
|
||||
parent_id,
|
||||
path: None,
|
||||
level,
|
||||
sort_order: 0,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
created_by: Uuid::now_v7(),
|
||||
updated_by: Uuid::now_v7(),
|
||||
deleted_at: None,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_org_tree_empty() {
|
||||
let tree = build_org_tree(&[]);
|
||||
assert!(tree.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_org_tree_single_root() {
|
||||
let tid = Uuid::now_v7();
|
||||
let root_id = Uuid::now_v7();
|
||||
let items = vec![make_org(root_id, tid, "总公司", None, 1, 1)];
|
||||
|
||||
let tree = build_org_tree(&items);
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert_eq!(tree[0].name, "总公司");
|
||||
assert!(tree[0].children.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_org_tree_multiple_roots() {
|
||||
let tid = Uuid::now_v7();
|
||||
let items = vec![
|
||||
make_org(Uuid::now_v7(), tid, "公司A", None, 1, 1),
|
||||
make_org(Uuid::now_v7(), tid, "公司B", None, 1, 1),
|
||||
];
|
||||
|
||||
let tree = build_org_tree(&items);
|
||||
assert_eq!(tree.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_org_tree_nested_children() {
|
||||
let tid = Uuid::now_v7();
|
||||
let root_id = Uuid::now_v7();
|
||||
let child1_id = Uuid::now_v7();
|
||||
let child2_id = Uuid::now_v7();
|
||||
let grandchild_id = Uuid::now_v7();
|
||||
|
||||
let items = vec![
|
||||
make_org(root_id, tid, "总公司", None, 1, 1),
|
||||
make_org(child1_id, tid, "分公司A", Some(root_id), 2, 1),
|
||||
make_org(child2_id, tid, "分公司B", Some(root_id), 2, 1),
|
||||
make_org(grandchild_id, tid, "部门A1", Some(child1_id), 3, 1),
|
||||
];
|
||||
|
||||
let tree = build_org_tree(&items);
|
||||
assert_eq!(tree.len(), 1); // one root
|
||||
assert_eq!(tree[0].children.len(), 2); // two children
|
||||
assert_eq!(tree[0].children[0].children.len(), 1); // one grandchild
|
||||
assert_eq!(tree[0].children[0].children[0].name, "部门A1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_org_tree_deep_nesting() {
|
||||
let tid = Uuid::now_v7();
|
||||
let l1 = Uuid::now_v7();
|
||||
let l2 = Uuid::now_v7();
|
||||
let l3 = Uuid::now_v7();
|
||||
let l4 = Uuid::now_v7();
|
||||
|
||||
let items = vec![
|
||||
make_org(l1, tid, "L1", None, 1, 1),
|
||||
make_org(l2, tid, "L2", Some(l1), 2, 1),
|
||||
make_org(l3, tid, "L3", Some(l2), 3, 1),
|
||||
make_org(l4, tid, "L4", Some(l3), 4, 1),
|
||||
];
|
||||
|
||||
let tree = build_org_tree(&items);
|
||||
assert_eq!(tree.len(), 1);
|
||||
assert_eq!(tree[0].children[0].children[0].children[0].name, "L4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_org_tree_preserves_version() {
|
||||
let tid = Uuid::now_v7();
|
||||
let root_id = Uuid::now_v7();
|
||||
let items = vec![make_org(root_id, tid, "测试", None, 1, 5)];
|
||||
|
||||
let tree = build_org_tree(&items);
|
||||
assert_eq!(tree[0].version, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ impl UserService {
|
||||
}
|
||||
|
||||
/// Convert a SeaORM user Model and its role DTOs into a UserResp.
|
||||
fn model_to_resp(m: &user::Model, roles: Vec<RoleResp>) -> UserResp {
|
||||
pub(crate) fn model_to_resp(m: &user::Model, roles: Vec<RoleResp>) -> UserResp {
|
||||
UserResp {
|
||||
id: m.id,
|
||||
username: m.username.clone(),
|
||||
@@ -406,3 +406,82 @@ fn model_to_resp(m: &user::Model, roles: Vec<RoleResp>) -> UserResp {
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::RoleResp;
|
||||
use crate::entity::user;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn make_user_model(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
username: &str,
|
||||
status: &str,
|
||||
version: i32,
|
||||
) -> user::Model {
|
||||
user::Model {
|
||||
id,
|
||||
tenant_id,
|
||||
username: username.to_string(),
|
||||
email: None,
|
||||
phone: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
status: status.to_string(),
|
||||
last_login_at: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
created_by: Uuid::now_v7(),
|
||||
updated_by: Uuid::now_v7(),
|
||||
deleted_at: None,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_maps_basic_fields() {
|
||||
let id = Uuid::now_v7();
|
||||
let tid = Uuid::now_v7();
|
||||
let m = make_user_model(id, tid, "alice", "active", 1);
|
||||
let resp = model_to_resp(&m, vec![]);
|
||||
assert_eq!(resp.id, id);
|
||||
assert_eq!(resp.username, "alice");
|
||||
assert_eq!(resp.status, "active");
|
||||
assert_eq!(resp.version, 1);
|
||||
assert!(resp.roles.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_includes_roles() {
|
||||
let id = Uuid::now_v7();
|
||||
let tid = Uuid::now_v7();
|
||||
let m = make_user_model(id, tid, "bob", "active", 2);
|
||||
let roles = vec![
|
||||
RoleResp {
|
||||
id: Uuid::now_v7(),
|
||||
name: "管理员".to_string(),
|
||||
code: "admin".to_string(),
|
||||
description: None,
|
||||
is_system: true,
|
||||
version: 1,
|
||||
},
|
||||
RoleResp {
|
||||
id: Uuid::now_v7(),
|
||||
name: "用户".to_string(),
|
||||
code: "user".to_string(),
|
||||
description: None,
|
||||
is_system: false,
|
||||
version: 1,
|
||||
},
|
||||
];
|
||||
let resp = model_to_resp(&m, roles);
|
||||
assert_eq!(resp.roles.len(), 2);
|
||||
assert_eq!(resp.roles[0].code, "admin");
|
||||
assert_eq!(resp.version, 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user