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:
@@ -176,3 +176,319 @@ pub struct UpdateSubscriptionReq {
|
||||
pub dnd_end: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
// ============ SendMessageReq 测试 ============
|
||||
|
||||
fn valid_send_message_req() -> SendMessageReq {
|
||||
SendMessageReq {
|
||||
title: "系统通知".to_string(),
|
||||
body: "您有一条新消息".to_string(),
|
||||
recipient_id: Uuid::now_v7(),
|
||||
recipient_type: "user".to_string(),
|
||||
priority: "normal".to_string(),
|
||||
template_id: None,
|
||||
business_type: None,
|
||||
business_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_valid() {
|
||||
let req = valid_send_message_req();
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_empty_title_fails() {
|
||||
let mut req = valid_send_message_req();
|
||||
req.title = "".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_title_too_long_fails() {
|
||||
let mut req = valid_send_message_req();
|
||||
req.title = "x".repeat(201);
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_title_max_length_ok() {
|
||||
let mut req = valid_send_message_req();
|
||||
req.title = "x".repeat(200);
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_empty_body_fails() {
|
||||
let mut req = valid_send_message_req();
|
||||
req.body = "".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_valid_recipient_types() {
|
||||
for rt in &["user", "role", "department", "all"] {
|
||||
let mut req = valid_send_message_req();
|
||||
req.recipient_type = rt.to_string();
|
||||
assert!(req.validate().is_ok(), "recipient_type '{}' should be valid", rt);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_invalid_recipient_type_fails() {
|
||||
let mut req = valid_send_message_req();
|
||||
req.recipient_type = "invalid".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_valid_priorities() {
|
||||
for p in &["normal", "important", "urgent"] {
|
||||
let mut req = valid_send_message_req();
|
||||
req.priority = p.to_string();
|
||||
assert!(req.validate().is_ok(), "priority '{}' should be valid", p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_invalid_priority_fails() {
|
||||
let mut req = valid_send_message_req();
|
||||
req.priority = "critical".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_default_recipient_type_is_user() {
|
||||
assert_eq!(default_recipient_type(), "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_message_req_default_priority_is_normal() {
|
||||
assert_eq!(default_priority(), "normal");
|
||||
}
|
||||
|
||||
// ============ MessageQuery 测试 ============
|
||||
|
||||
#[test]
|
||||
fn message_query_safe_page_size_default() {
|
||||
let query = MessageQuery {
|
||||
page: None,
|
||||
page_size: None,
|
||||
is_read: None,
|
||||
priority: None,
|
||||
business_type: None,
|
||||
status: None,
|
||||
};
|
||||
assert_eq!(query.safe_page_size(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_query_safe_page_size_custom() {
|
||||
let query = MessageQuery {
|
||||
page: None,
|
||||
page_size: Some(50),
|
||||
is_read: None,
|
||||
priority: None,
|
||||
business_type: None,
|
||||
status: None,
|
||||
};
|
||||
assert_eq!(query.safe_page_size(), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_query_safe_page_size_capped_at_100() {
|
||||
let query = MessageQuery {
|
||||
page: None,
|
||||
page_size: Some(200),
|
||||
is_read: None,
|
||||
priority: None,
|
||||
business_type: None,
|
||||
status: None,
|
||||
};
|
||||
assert_eq!(query.safe_page_size(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_query_safe_page_size_exactly_100() {
|
||||
let query = MessageQuery {
|
||||
page: None,
|
||||
page_size: Some(100),
|
||||
is_read: None,
|
||||
priority: None,
|
||||
business_type: None,
|
||||
status: None,
|
||||
};
|
||||
assert_eq!(query.safe_page_size(), 100);
|
||||
}
|
||||
|
||||
// ============ CreateTemplateReq 测试 ============
|
||||
|
||||
fn valid_create_template_req() -> CreateTemplateReq {
|
||||
CreateTemplateReq {
|
||||
name: "欢迎模板".to_string(),
|
||||
code: "WELCOME".to_string(),
|
||||
channel: "in_app".to_string(),
|
||||
title_template: "欢迎加入".to_string(),
|
||||
body_template: "您好,{{name}},欢迎加入平台".to_string(),
|
||||
language: "zh-CN".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_valid() {
|
||||
let req = valid_create_template_req();
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_empty_name_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.name = "".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_name_too_long_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.name = "x".repeat(101);
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_name_max_length_ok() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.name = "x".repeat(100);
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_empty_code_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.code = "".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_code_too_long_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.code = "X".repeat(51);
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_code_max_length_ok() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.code = "X".repeat(50);
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_valid_channels() {
|
||||
for ch in &["in_app", "email", "sms", "wechat"] {
|
||||
let mut req = valid_create_template_req();
|
||||
req.channel = ch.to_string();
|
||||
assert!(req.validate().is_ok(), "channel '{}' should be valid", ch);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_invalid_channel_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.channel = "telegram".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_empty_title_template_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.title_template = "".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_title_template_too_long_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.title_template = "x".repeat(201);
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_empty_body_template_fails() {
|
||||
let mut req = valid_create_template_req();
|
||||
req.body_template = "".to_string();
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_default_channel_is_in_app() {
|
||||
assert_eq!(default_channel(), "in_app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_req_default_language_is_zh_cn() {
|
||||
assert_eq!(default_language(), "zh-CN");
|
||||
}
|
||||
|
||||
// ============ 自定义验证函数测试 ============
|
||||
|
||||
#[test]
|
||||
fn validate_recipient_type_valid() {
|
||||
for rt in &["user", "role", "department", "all"] {
|
||||
assert!(
|
||||
validate_recipient_type(rt).is_ok(),
|
||||
"'{}' should be a valid recipient type",
|
||||
rt
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_recipient_type_invalid() {
|
||||
assert!(validate_recipient_type("invalid").is_err());
|
||||
assert!(validate_recipient_type("").is_err());
|
||||
assert!(validate_recipient_type("USER").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_priority_valid() {
|
||||
for p in &["normal", "important", "urgent"] {
|
||||
assert!(
|
||||
validate_priority(p).is_ok(),
|
||||
"'{}' should be a valid priority",
|
||||
p
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_priority_invalid() {
|
||||
assert!(validate_priority("critical").is_err());
|
||||
assert!(validate_priority("").is_err());
|
||||
assert!(validate_priority("NORMAL").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_channel_valid() {
|
||||
for ch in &["in_app", "email", "sms", "wechat"] {
|
||||
assert!(
|
||||
validate_channel(ch).is_ok(),
|
||||
"'{}' should be a valid channel",
|
||||
ch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_channel_invalid() {
|
||||
assert!(validate_channel("slack").is_err());
|
||||
assert!(validate_channel("").is_err());
|
||||
assert!(validate_channel("EMAIL").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,3 +43,78 @@ impl From<sea_orm::TransactionError<MessageError>> for MessageError {
|
||||
}
|
||||
|
||||
pub type MessageResult<T> = Result<T, MessageError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use erp_core::error::AppError;
|
||||
|
||||
#[test]
|
||||
fn validation_maps_to_app_validation() {
|
||||
let app: AppError = MessageError::Validation("标题不能为空".to_string()).into();
|
||||
match app {
|
||||
AppError::Validation(msg) => assert_eq!(msg, "标题不能为空"),
|
||||
other => panic!("Expected AppError::Validation, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_found_maps_to_app_not_found() {
|
||||
let app: AppError = MessageError::NotFound("消息不存在".to_string()).into();
|
||||
match app {
|
||||
AppError::NotFound(msg) => assert_eq!(msg, "消息不存在"),
|
||||
other => panic!("Expected AppError::NotFound, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_template_code_maps_to_app_conflict() {
|
||||
let app: AppError = MessageError::DuplicateTemplateCode("WELCOME".to_string()).into();
|
||||
match app {
|
||||
AppError::Conflict(msg) => assert_eq!(msg, "WELCOME"),
|
||||
other => panic!("Expected AppError::Conflict, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_render_error_maps_to_app_internal() {
|
||||
let app: AppError = MessageError::TemplateRenderError("变量缺失".to_string()).into();
|
||||
match app {
|
||||
AppError::Internal(msg) => assert_eq!(msg, "变量缺失"),
|
||||
other => panic!("Expected AppError::Internal, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_mismatch_maps_to_app_version_mismatch() {
|
||||
let app: AppError = MessageError::VersionMismatch.into();
|
||||
match app {
|
||||
AppError::VersionMismatch => {}
|
||||
other => panic!("Expected AppError::VersionMismatch, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_display_format() {
|
||||
assert_eq!(
|
||||
MessageError::Validation("字段为空".to_string()).to_string(),
|
||||
"验证失败: 字段为空"
|
||||
);
|
||||
assert_eq!(
|
||||
MessageError::NotFound("id=123".to_string()).to_string(),
|
||||
"未找到: id=123"
|
||||
);
|
||||
assert_eq!(
|
||||
MessageError::DuplicateTemplateCode("CODE".to_string()).to_string(),
|
||||
"模板编码已存在: CODE"
|
||||
);
|
||||
assert_eq!(
|
||||
MessageError::TemplateRenderError("解析失败".to_string()).to_string(),
|
||||
"渲染失败: 解析失败"
|
||||
);
|
||||
assert_eq!(
|
||||
MessageError::VersionMismatch.to_string(),
|
||||
"版本冲突: 数据已被其他操作修改,请刷新后重试"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,3 +113,81 @@ impl TemplateService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn render_replaces_single_variable() {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("name".to_string(), "张三".to_string());
|
||||
let result = TemplateService::render("您好,{{name}}", &vars);
|
||||
assert_eq!(result, "您好,张三");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_replaces_multiple_variables() {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("name".to_string(), "李四".to_string());
|
||||
vars.insert("code".to_string(), "ORD-001".to_string());
|
||||
let result = TemplateService::render("{{name}},您的订单 {{code}} 已发货", &vars);
|
||||
assert_eq!(result, "李四,您的订单 ORD-001 已发货");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_no_variables_returns_original() {
|
||||
let vars = std::collections::HashMap::new();
|
||||
let result = TemplateService::render("没有变量的模板", &vars);
|
||||
assert_eq!(result, "没有变量的模板");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_missing_variable_leaves_placeholder() {
|
||||
let vars = std::collections::HashMap::new();
|
||||
let result = TemplateService::render("您好,{{name}}", &vars);
|
||||
assert_eq!(result, "您好,{{name}}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_same_variable_multiple_times() {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("user".to_string(), "王五".to_string());
|
||||
let result = TemplateService::render("{{user}} 你好,{{user}} 的订单已确认", &vars);
|
||||
assert_eq!(result, "王五 你好,王五 的订单已确认");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_empty_template() {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("name".to_string(), "test".to_string());
|
||||
let result = TemplateService::render("", &vars);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_empty_variable_value() {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("name".to_string(), "".to_string());
|
||||
let result = TemplateService::render("您好,{{name}}!", &vars);
|
||||
assert_eq!(result, "您好,!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_adjacent_variables() {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("a".to_string(), "1".to_string());
|
||||
vars.insert("b".to_string(), "2".to_string());
|
||||
let result = TemplateService::render("{{a}}{{b}}", &vars);
|
||||
assert_eq!(result, "12");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_extra_variables_not_in_template_are_ignored() {
|
||||
let mut vars = std::collections::HashMap::new();
|
||||
vars.insert("name".to_string(), "赵六".to_string());
|
||||
vars.insert("unused".to_string(), "ignore".to_string());
|
||||
let result = TemplateService::render("你好 {{name}}", &vars);
|
||||
assert_eq!(result, "你好 赵六");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user