use std::sync::Arc; use async_trait::async_trait; use vapora_channels::{ config::{DiscordConfig, SlackConfig, TelegramConfig}, discord::DiscordChannel, error::ChannelError, message::Message, registry::ChannelRegistry, slack::SlackChannel, telegram::TelegramChannel, NotificationChannel, Result, }; use wiremock::{ matchers::{method, path}, Mock, MockServer, ResponseTemplate, }; // ── Slack ───────────────────────────────────────────────────────────────────── #[tokio::test] async fn slack_send_returns_ok_on_200() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/hooks/slack")) .respond_with(ResponseTemplate::new(200).set_body_string("ok")) .mount(&server) .await; let cfg = SlackConfig { webhook_url: format!("{}/hooks/slack", server.uri()), channel: None, username: None, }; let channel = SlackChannel::new("slack", cfg, reqwest::Client::new()); let msg = Message::success("Deploy complete", "v1.2.0 → production"); channel.send(&msg).await.expect("should succeed on 200"); } #[tokio::test] async fn slack_send_returns_api_error_on_500() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/hooks/slack")) .respond_with(ResponseTemplate::new(500).set_body_string("internal_error")) .mount(&server) .await; let cfg = SlackConfig { webhook_url: format!("{}/hooks/slack", server.uri()), channel: None, username: None, }; let channel = SlackChannel::new("slack", cfg, reqwest::Client::new()); let err = channel .send(&Message::info("Test", "Body")) .await .unwrap_err(); assert!( matches!( err, ChannelError::ApiError { status: 500, ref body, .. } if body == "internal_error" ), "unexpected error variant: {err}" ); } // ── Discord // ─────────────────────────────────────────────────────────────────── #[tokio::test] async fn discord_send_returns_ok_on_204() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/webhooks/discord")) .respond_with(ResponseTemplate::new(204)) .mount(&server) .await; let cfg = DiscordConfig { webhook_url: format!("{}/webhooks/discord", server.uri()), username: None, avatar_url: None, }; let channel = DiscordChannel::new("discord", cfg, reqwest::Client::new()); channel .send(&Message::warning("High latency", "p99 > 500 ms")) .await .expect("should succeed on 204"); } #[tokio::test] async fn discord_send_returns_api_error_on_400() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/webhooks/discord")) .respond_with(ResponseTemplate::new(400).set_body_string("{\"code\":50006}")) .mount(&server) .await; let cfg = DiscordConfig { webhook_url: format!("{}/webhooks/discord", server.uri()), username: None, avatar_url: None, }; let channel = DiscordChannel::new("discord", cfg, reqwest::Client::new()); let err = channel .send(&Message::info("Test", "Body")) .await .unwrap_err(); assert!( matches!(err, ChannelError::ApiError { status: 400, .. }), "unexpected error variant: {err}" ); } // ── Telegram // ────────────────────────────────────────────────────────────────── #[tokio::test] async fn telegram_send_returns_ok_on_200() { let server = MockServer::start().await; // Telegram returns {"ok": true, "result": {...}} with HTTP 200. Mock::given(method("POST")) .and(path("/botTEST_TOKEN/sendMessage")) .respond_with( ResponseTemplate::new(200).set_body_string(r#"{"ok":true,"result":{"message_id":1}}"#), ) .mount(&server) .await; let cfg = TelegramConfig { bot_token: "TEST_TOKEN".to_string(), chat_id: "-100999".to_string(), api_base: Some(server.uri()), }; let channel = TelegramChannel::new("telegram", cfg, reqwest::Client::new()); channel .send(&Message::error("Service down", "Critical alert")) .await .expect("should succeed on 200"); } #[tokio::test] async fn telegram_send_returns_api_error_on_400() { let server = MockServer::start().await; Mock::given(method("POST")) .and(path("/botBAD_TOKEN/sendMessage")) .respond_with( ResponseTemplate::new(400) .set_body_string(r#"{"ok":false,"description":"Unauthorized"}"#), ) .mount(&server) .await; let cfg = TelegramConfig { bot_token: "BAD_TOKEN".to_string(), chat_id: "-100".to_string(), api_base: Some(server.uri()), }; let channel = TelegramChannel::new("telegram", cfg, reqwest::Client::new()); let err = channel .send(&Message::info("Test", "Body")) .await .unwrap_err(); assert!( matches!(err, ChannelError::ApiError { status: 400, .. }), "unexpected error variant: {err}" ); } // ── Registry // ────────────────────────────────────────────────────────────────── struct AlwaysOkChannel { name: String, } #[async_trait] impl NotificationChannel for AlwaysOkChannel { fn name(&self) -> &str { &self.name } async fn send(&self, _msg: &Message) -> Result<()> { Ok(()) } } struct AlwaysFailChannel { name: String, } #[async_trait] impl NotificationChannel for AlwaysFailChannel { fn name(&self) -> &str { &self.name } async fn send(&self, _msg: &Message) -> Result<()> { Err(ChannelError::ApiError { channel: self.name.clone(), status: 503, body: "unavailable".to_string(), }) } } #[tokio::test] async fn registry_send_routes_to_named_channel() { let mut registry = ChannelRegistry::new(); registry.register(Arc::new(AlwaysOkChannel { name: "ok-channel".to_string(), })); registry .send("ok-channel", Message::info("Test", "Body")) .await .expect("should route to ok-channel and succeed"); } #[tokio::test] async fn registry_send_returns_not_found_for_unknown_channel() { let registry = ChannelRegistry::new(); let err = registry .send("does-not-exist", Message::info("Test", "Body")) .await .unwrap_err(); assert!( matches!(err, ChannelError::NotFound(ref n) if n == "does-not-exist"), "unexpected error: {err}" ); } #[tokio::test] async fn registry_broadcast_delivers_to_all_channels() { let mut registry = ChannelRegistry::new(); registry.register(Arc::new(AlwaysOkChannel { name: "ch-a".to_string(), })); registry.register(Arc::new(AlwaysOkChannel { name: "ch-b".to_string(), })); let results = registry .broadcast(Message::success("All systems green", "")) .await; assert_eq!(results.len(), 2); assert!(results.iter().all(|(_, r)| r.is_ok())); } #[tokio::test] async fn registry_broadcast_continues_after_partial_failure() { let mut registry = ChannelRegistry::new(); registry.register(Arc::new(AlwaysOkChannel { name: "good".to_string(), })); registry.register(Arc::new(AlwaysFailChannel { name: "bad".to_string(), })); let results = registry.broadcast(Message::info("Test", "Body")).await; assert_eq!(results.len(), 2); let ok_count = results.iter().filter(|(_, r)| r.is_ok()).count(); let err_count = results.iter().filter(|(_, r)| r.is_err()).count(); assert_eq!(ok_count, 1); assert_eq!(err_count, 1); }