From 26aa66d6e303279f1b44786955bbc129fd15e8bd Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 28 Apr 2026 18:26:36 +0800 Subject: [PATCH] =?UTF-8?q?test(message):=20erp-message=20=E4=BB=8E=2045?= =?UTF-8?q?=20=E5=A2=9E=E8=87=B3=2069=20=E4=B8=AA=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=20=E2=80=94=20DND=20=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=AA=97=20+=20TransactionError=20+=20model=5Fto=5Fresp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 字段映射测试 --- crates/erp-message/src/error.rs | 24 +++++ crates/erp-message/src/module.rs | 92 ++++++++++++++++++- .../src/service/message_service.rs | 62 ++++++++++++- .../src/service/subscription_service.rs | 34 ++++++- .../src/service/template_service.rs | 28 +++++- 5 files changed, 236 insertions(+), 4 deletions(-) diff --git a/crates/erp-message/src/error.rs b/crates/erp-message/src/error.rs index 03620bf..e708a64 100644 --- a/crates/erp-message/src/error.rs +++ b/crates/erp-message/src/error.rs @@ -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 = + 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 = + 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), + } + } } diff --git a/crates/erp-message/src/module.rs b/crates/erp-message/src/module.rs index 5f3520e..8d0c958 100644 --- a/crates/erp-message/src/module.rs +++ b/crates/erp-message/src/module.rs @@ -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")); + } +} diff --git a/crates/erp-message/src/service/message_service.rs b/crates/erp-message/src/service/message_service.rs index f783f82..cd8431d 100644 --- a/crates/erp-message/src/service/message_service.rs +++ b/crates/erp-message/src/service/message_service.rs @@ -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); + } +} diff --git a/crates/erp-message/src/service/subscription_service.rs b/crates/erp-message/src/service/subscription_service.rs index e2194de..0990a92 100644 --- a/crates/erp-message/src/service/subscription_service.rs +++ b/crates/erp-message/src/service/subscription_service.rs @@ -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); + } +} diff --git a/crates/erp-message/src/service/template_service.rs b/crates/erp-message/src/service/template_service.rs index 58b0abf..718372e 100644 --- a/crates/erp-message/src/service/template_service.rs +++ b/crates/erp-message/src/service/template_service.rs @@ -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); + } }