test(message): erp-message 从 45 增至 69 个单元测试 — DND 时间窗 + TransactionError + model_to_resp
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- module.rs: 提取 is_in_dnd_window 纯函数 + 14 个 DND 时间窗测试(正常范围/跨午夜/边界)
- error.rs: 2 个 TransactionError 转换测试(Connection/Transaction)
- message_service: 2 个 model_to_resp 字段映射测试
- template_service: 1 个 model_to_resp 字段映射测试
- subscription_service: 1 个 model_to_resp 字段映射测试
This commit is contained in:
iven
2026-04-28 18:26:36 +08:00
parent 50e63530d9
commit 26aa66d6e3
5 changed files with 236 additions and 4 deletions

View File

@@ -117,4 +117,28 @@ mod tests {
"版本冲突: 数据已被其他操作修改,请刷新后重试"
);
}
#[test]
fn transaction_connection_error_maps_to_validation() {
let db_err = sea_orm::DbErr::Conn(sea_orm::RuntimeErr::Internal("连接超时".to_string()));
let tx_err: sea_orm::TransactionError<MessageError> =
sea_orm::TransactionError::Connection(db_err);
let msg_err: MessageError = tx_err.into();
match msg_err {
MessageError::Validation(msg) => assert!(msg.contains("连接超时")),
other => panic!("期望 Validation得到 {:?}", other),
}
}
#[test]
fn transaction_inner_error_passthrough() {
let inner = MessageError::NotFound("模板不存在".to_string());
let tx_err: sea_orm::TransactionError<MessageError> =
sea_orm::TransactionError::Transaction(inner);
let msg_err: MessageError = tx_err.into();
match msg_err {
MessageError::NotFound(msg) => assert_eq!(msg, "模板不存在"),
other => panic!("期望 NotFound得到 {:?}", other),
}
}
}

View File

@@ -171,7 +171,11 @@ async fn should_skip_for_dnd(
};
let now = chrono::Local::now();
let now_time = now.format("%H:%M").to_string();
// DND 窗口比较(支持跨午夜,如 22:00-08:00
is_in_dnd_window(&now_time, &start, &end)
}
/// 判断当前时间是否在 DND 窗口内。支持跨午夜窗口(如 22:00-06:00
pub(crate) fn is_in_dnd_window(now_time: &str, start: &str, end: &str) -> bool {
if start <= end {
now_time >= start && now_time < end
} else {
@@ -602,3 +606,89 @@ async fn handle_workflow_event(
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
// ---- DND 时间窗逻辑 ----
#[test]
fn dnd_normal_range_inside() {
// 09:00-17:00当前 12:00 → 在窗口内
assert!(is_in_dnd_window("12:00", "09:00", "17:00"));
}
#[test]
fn dnd_normal_range_before() {
// 09:00-17:00当前 08:00 → 不在窗口内
assert!(!is_in_dnd_window("08:00", "09:00", "17:00"));
}
#[test]
fn dnd_normal_range_after() {
// 09:00-17:00当前 18:00 → 不在窗口内
assert!(!is_in_dnd_window("18:00", "09:00", "17:00"));
}
#[test]
fn dnd_normal_range_at_start() {
// 09:00-17:00当前 09:00 → 在窗口内(>= start
assert!(is_in_dnd_window("09:00", "09:00", "17:00"));
}
#[test]
fn dnd_normal_range_at_end() {
// 09:00-17:00当前 17:00 → 不在窗口内(< end 排除了 end 本身)
assert!(!is_in_dnd_window("17:00", "09:00", "17:00"));
}
#[test]
fn dnd_cross_midnight_night_time() {
// 22:00-06:00当前 23:30 → 在窗口内
assert!(is_in_dnd_window("23:30", "22:00", "06:00"));
}
#[test]
fn dnd_cross_midnight_early_morning() {
// 22:00-06:00当前 03:00 → 在窗口内
assert!(is_in_dnd_window("03:00", "22:00", "06:00"));
}
#[test]
fn dnd_cross_midnight_daytime() {
// 22:00-06:00当前 14:00 → 不在窗口内
assert!(!is_in_dnd_window("14:00", "22:00", "06:00"));
}
#[test]
fn dnd_cross_midnight_at_start() {
assert!(is_in_dnd_window("22:00", "22:00", "06:00"));
}
#[test]
fn dnd_cross_midnight_at_end() {
assert!(!is_in_dnd_window("06:00", "22:00", "06:00"));
}
#[test]
fn dnd_cross_midnight_just_before_end() {
assert!(is_in_dnd_window("05:59", "22:00", "06:00"));
}
#[test]
fn dnd_same_start_end_always_in() {
// start == end 意味着 start <= end所以 now >= start && now < end
// "00:00" >= "00:00" && "00:00" < "00:00" → false
assert!(!is_in_dnd_window("00:00", "12:00", "12:00"));
// "15:00" >= "12:00" && "15:00" < "12:00" → false
assert!(!is_in_dnd_window("15:00", "12:00", "12:00"));
}
#[test]
fn dnd_single_minute_window() {
// 23:59-00:00跨午夜 1 分钟)
assert!(is_in_dnd_window("23:59", "23:59", "00:00"));
assert!(!is_in_dnd_window("00:00", "23:59", "00:00"));
}
}

View File

@@ -488,7 +488,7 @@ impl MessageService {
Ok(())
}
fn model_to_resp(m: &message::Model) -> MessageResp {
pub(crate) fn model_to_resp(m: &message::Model) -> MessageResp {
MessageResp {
id: m.id,
tenant_id: m.tenant_id,
@@ -513,3 +513,63 @@ impl MessageService {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn sample_model() -> message::Model {
message::Model {
id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
tenant_id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(),
template_id: None,
sender_id: None,
sender_type: "system".to_string(),
recipient_id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(),
recipient_type: "user".to_string(),
title: "测试消息".to_string(),
body: "消息内容".to_string(),
priority: "normal".to_string(),
business_type: None,
business_id: None,
is_read: false,
read_at: None,
is_archived: false,
archived_at: None,
sent_at: None,
status: "sent".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000004").unwrap(),
updated_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000004").unwrap(),
deleted_at: None,
version: 1,
}
}
#[test]
fn model_to_resp_maps_all_fields() {
let m = sample_model();
let resp = MessageService::model_to_resp(&m);
assert_eq!(resp.id, m.id);
assert_eq!(resp.tenant_id, m.tenant_id);
assert_eq!(resp.title, "测试消息");
assert_eq!(resp.body, "消息内容");
assert_eq!(resp.priority, "normal");
assert_eq!(resp.is_read, false);
assert_eq!(resp.status, "sent");
assert_eq!(resp.version, 1);
}
#[test]
fn model_to_resp_preserves_optional_fields() {
let m = sample_model();
let resp = MessageService::model_to_resp(&m);
assert_eq!(resp.template_id, None);
assert_eq!(resp.sender_id, None);
assert_eq!(resp.business_type, None);
assert_eq!(resp.read_at, None);
assert_eq!(resp.sent_at, None);
}
}

View File

@@ -105,7 +105,7 @@ impl SubscriptionService {
}
}
fn model_to_resp(m: &message_subscription::Model) -> MessageSubscriptionResp {
pub(crate) fn model_to_resp(m: &message_subscription::Model) -> MessageSubscriptionResp {
MessageSubscriptionResp {
id: m.id,
tenant_id: m.tenant_id,
@@ -121,3 +121,35 @@ impl SubscriptionService {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn model_to_resp_maps_all_fields() {
let m = message_subscription::Model {
id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
tenant_id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(),
user_id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(),
notification_types: Some(serde_json::json!(["appointment"])),
channel_preferences: Some(serde_json::json!(["in_app"])),
dnd_enabled: true,
dnd_start: Some("22:00".to_string()),
dnd_end: Some("08:00".to_string()),
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(),
updated_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(),
deleted_at: None,
version: 1,
};
let resp = SubscriptionService::model_to_resp(&m);
assert_eq!(resp.user_id, m.user_id);
assert_eq!(resp.dnd_enabled, true);
assert_eq!(resp.dnd_start, Some("22:00".to_string()));
assert_eq!(resp.dnd_end, Some("08:00".to_string()));
assert_eq!(resp.version, 1);
}
}

View File

@@ -174,7 +174,7 @@ impl TemplateService {
result
}
fn model_to_resp(m: &message_template::Model) -> MessageTemplateResp {
pub(crate) fn model_to_resp(m: &message_template::Model) -> MessageTemplateResp {
MessageTemplateResp {
id: m.id,
tenant_id: m.tenant_id,
@@ -267,4 +267,30 @@ mod tests {
let result = TemplateService::render("你好 {{name}}", &vars);
assert_eq!(result, "你好 赵六");
}
#[test]
fn model_to_resp_maps_all_fields() {
let m = message_template::Model {
id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(),
tenant_id: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap(),
name: "欢迎消息".to_string(),
code: "WELCOME".to_string(),
channel: "in_app".to_string(),
title_template: "欢迎 {{name}}".to_string(),
body_template: "{{name}},欢迎使用".to_string(),
language: "zh-CN".to_string(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
created_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(),
updated_by: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap(),
deleted_at: None,
version: 2,
};
let resp = TemplateService::model_to_resp(&m);
assert_eq!(resp.name, "欢迎消息");
assert_eq!(resp.code, "WELCOME");
assert_eq!(resp.channel, "in_app");
assert_eq!(resp.language, "zh-CN");
assert_eq!(resp.version, 2);
}
}