821537186e
- Reorganized import statements in adapter tests for better readability - Replaced or_insert_with(Vec::new) with or_default() in test closures - Updated Cargo.lock with new dependency versions and checksums - Added TLS features to tonic dependency configuration - Included sqlx, chrono, and uuid dependencies with specific features - Added jsonwebtoken and arc-swap as project dependencies - Reformatted assertion statements to comply with line length limits - Adjusted base64 import order in engine codec module - Updated protobuf include statement formatting
167 lines
5.7 KiB
Rust
167 lines
5.7 KiB
Rust
//! 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<String> = 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<String> = 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<String> = MessageService::parse_optional(&json, "name").unwrap();
|
|
assert_eq!(val, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_optional_missing() {
|
|
let json = serde_json::json!({});
|
|
let val: Option<String> = 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::<String, Instant>::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::<String, Instant>::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<Instant>>::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<Instant>>::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<Instant>>::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<Instant>>::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);
|
|
}
|
|
}
|