use crate::domain::error::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEvent {
pub method: String,
pub path: String,
pub headers: HashMap<String, String>,
pub body: String,
pub received_at_ms: u64,
pub signature: Option<String>,
pub source_ip: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookConfig {
pub bind_address: String,
pub path_prefix: String,
pub secret: Option<String>,
pub max_body_size: usize,
}
impl Default for WebhookConfig {
fn default() -> Self {
Self {
bind_address: "0.0.0.0:9090".into(),
path_prefix: "/webhooks".into(),
secret: None,
max_body_size: 1_048_576, }
}
}
pub struct WebhookListenerHandle {
pub id: String,
}
#[async_trait]
pub trait WebhookTrigger: Send + Sync + 'static {
async fn start_listener(&self, config: WebhookConfig) -> Result<WebhookListenerHandle>;
async fn stop_listener(&self, handle: WebhookListenerHandle) -> Result<()>;
async fn recv_event(&self) -> Result<Option<WebhookEvent>>;
fn verify_signature(&self, secret: &str, signature: &str, body: &[u8]) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_webhook_event_creation() {
let event = WebhookEvent {
method: "POST".into(),
path: "/hooks/github".into(),
headers: HashMap::new(),
body: r#"{"ref":"refs/heads/main"}"#.into(),
received_at_ms: 1_700_000_000_000,
signature: Some("sha256=abc".into()),
source_ip: None,
};
assert_eq!(event.method, "POST");
assert_eq!(event.path, "/hooks/github");
assert!(event.signature.is_some());
}
#[test]
fn test_webhook_config_default() {
let cfg = WebhookConfig::default();
assert_eq!(cfg.bind_address, "0.0.0.0:9090");
assert_eq!(cfg.path_prefix, "/webhooks");
assert!(cfg.secret.is_none());
assert_eq!(cfg.max_body_size, 1_048_576);
}
#[test]
fn test_webhook_event_serialisation() {
let event = WebhookEvent {
method: "POST".into(),
path: "/trigger".into(),
headers: [("x-hub-signature-256".into(), "sha256=abc".into())].into(),
body: "{}".into(),
received_at_ms: 0,
signature: None,
source_ip: Some("127.0.0.1".into()),
};
let json = serde_json::to_string(&event).unwrap();
let back: WebhookEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back.method, "POST");
assert_eq!(back.source_ip.as_deref(), Some("127.0.0.1"));
}
#[test]
fn test_webhook_config_with_secret() {
let cfg = WebhookConfig {
secret: Some("my-secret".into()),
..Default::default()
};
assert_eq!(cfg.secret.as_deref(), Some("my-secret"));
}
}