286 lines
8.2 KiB
Rust
Raw Normal View History

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);
}