//! Service layer unit tests for `MessageService`. //! //! Tests parsing, nonce dedup, rate limiting — all in-memory without //! requiring a real database or gRPC connection. #[cfg(test)] #[allow(clippy::module_inception)] mod tests { use std::sync::Arc; use std::time::{Duration, Instant}; use uuid::Uuid; use crate::svc::message::MessageService; #[test] fn test_parse_field_valid() { let json = serde_json::json!({"message_id": "01909abc-def0-7000-8000-000000000001"}); let id: Uuid = MessageService::parse_field(&json, "message_id").unwrap(); assert_eq!(id.to_string(), "01909abc-def0-7000-8000-000000000001"); } #[test] fn test_parse_field_missing() { let json = serde_json::json!({}); let result: crate::ImksResult = MessageService::parse_field(&json, "missing"); assert!(result.is_err()); assert!( result .unwrap_err() .to_string() .contains("Missing required field") ); } #[test] fn test_parse_optional_present() { let json = serde_json::json!({"name": "alice"}); let val: Option = MessageService::parse_optional(&json, "name").unwrap(); assert_eq!(val, Some("alice".into())); } #[test] fn test_parse_optional_null() { let json = serde_json::json!({"name": null}); let val: Option = MessageService::parse_optional(&json, "name").unwrap(); assert_eq!(val, None); } #[test] fn test_parse_optional_missing() { let json = serde_json::json!({}); let val: Option = MessageService::parse_optional(&json, "name").unwrap(); assert_eq!(val, None); } #[test] fn test_parse_send_payload_basic_shape() { let json = serde_json::json!([{ "channel_id": "01909abc-def0-7000-8000-000000000001", "body": "hello world" }]); let arr = json.as_array().unwrap(); let payload = &arr[0]; assert_eq!(payload["body"], "hello world"); } #[test] fn test_parse_send_payload_with_rich_content() { let json = serde_json::json!([{ "channel_id": "01909abc-def0-7000-8000-000000000001", "body": "hey @alice", "thread_id": "01909def-abc0-7000-8000-000000000002", "nonce": "nonce-001", "mentioned_user_ids": ["01909abc-def0-7000-8000-000000000003"], "attachments": [{"filename": "img.png", "url": "https://cdn/img.png", "size": 1024, "content_type": "image/png"}], "embeds": [{"embed_type": "link", "title": "Example", "url": "https://example.com", "fields": [{"name": "k", "value": "v", "inline": true}]}], "sticker": {"sticker_id": "01909abc-def0-7000-8000-000000000004", "name": "Hype!", "image_url": "https://cdn/sticker.png"}, "forward": {"source_message_id": "01909abc-def0-7000-8000-000000000005", "source_channel_id": "01909abc-def0-7000-8000-000000000006"} }]); let arr = json.as_array().unwrap(); let payload = &arr[0]; assert_eq!(payload["body"], "hey @alice"); assert!(payload["attachments"].is_array()); assert!(payload["embeds"].is_array()); assert!(payload["sticker"].is_object()); assert!(payload["forward"].is_object()); } #[test] fn test_nonce_dedup_first_accepted() { use dashmap::DashMap; let nonces = Arc::new(DashMap::::new()); assert!(!nonces.contains_key("nonce-1")); nonces.insert("nonce-1".to_string(), Instant::now()); assert!(nonces.contains_key("nonce-1")); } #[test] fn test_nonce_dedup_rejects_duplicate() { use dashmap::DashMap; let nonces = Arc::new(DashMap::::new()); nonces.insert("nonce-1".to_string(), Instant::now()); // After insert, it should exist assert!(nonces.contains_key("nonce-1")); } #[test] fn test_rate_limit_within_window() { use dashmap::DashMap; let limits = Arc::new(DashMap::<(Uuid, Uuid), Vec>::new()); let user = Uuid::now_v7(); let channel = Uuid::now_v7(); // Should be empty initially assert!(limits.get(&(user, channel)).is_none()); } #[test] fn test_rate_limit_approaches_threshold() { use dashmap::DashMap; let limits = Arc::new(DashMap::<(Uuid, Uuid), Vec>::new()); let now = Instant::now(); let user = Uuid::now_v7(); let channel = Uuid::now_v7(); let mut entry = limits.entry((user, channel)).or_default(); for _ in 0..9 { entry.push(now); } assert_eq!(entry.len(), 9); } #[test] fn test_rate_limit_exceeded() { use dashmap::DashMap; let limits = Arc::new(DashMap::<(Uuid, Uuid), Vec>::new()); let now = Instant::now(); let user = Uuid::now_v7(); let channel = Uuid::now_v7(); let mut entry = limits.entry((user, channel)).or_default(); for _ in 0..10 { entry.push(now); } assert!(entry.len() >= 10); } #[test] fn test_rate_limit_window_expiry_eviction() { use dashmap::DashMap; let limits = Arc::new(DashMap::<(Uuid, Uuid), Vec>::new()); let window = Duration::from_secs(10); let user = Uuid::now_v7(); let channel = Uuid::now_v7(); let old = Instant::now() - Duration::from_secs(15); let now = Instant::now(); let mut entry = limits.entry((user, channel)).or_default(); entry.push(old); entry.push(now); entry.retain(|t| now.duration_since(*t) < window); assert_eq!(entry.len(), 1); } }