discourse_webhooks/
lib.rs

1//! # Discourse Webhooks
2//!
3//! A type-safe Rust library for handling Discourse webhook events.
4//!
5//! This crate provides:
6//! - Type-safe event parsing for Discourse webhooks
7//! - HMAC-SHA256 signature verification
8//! - Trait-based event handling system with async support
9//! - Support for all major Discourse webhook events
10//!
11//! ## Quick Start
12//!
13//! ```rust
14//! use discourse_webhooks::{WebhookEventHandler, WebhookProcessor, TopicWebhookEvent};
15//! use async_trait::async_trait;
16//!
17//! struct MyHandler;
18//!
19//! #[async_trait]
20//! impl WebhookEventHandler for MyHandler {
21//!     type Error = String;
22//!
23//!     async fn handle_topic_created(&mut self, event: &TopicWebhookEvent) -> Result<(), Self::Error> {
24//!         println!("New topic: {}", event.topic.title);
25//!         Ok(())
26//!     }
27//! }
28//!
29//! // Process webhook events
30//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
31//! let processor = WebhookProcessor::new();
32//! let mut handler = MyHandler;
33//! // processor.process_json(&mut handler, "topic_created", payload, None).await?;
34//! # Ok(())
35//! # }
36//! ```
37
38pub mod error;
39pub mod events;
40pub mod signature;
41
42#[cfg(feature = "async")]
43pub use async_trait::async_trait;
44pub use error::{Result, WebhookError};
45pub use events::{
46    parse_webhook_payload, PostWebhookEvent, TopicWebhookEvent, WebhookEventPayload, WebhookPost,
47    WebhookTopic, WebhookUser,
48};
49pub use signature::{verify_json_signature, verify_signature, SignatureVerificationError};
50
51use serde::{Deserialize, Serialize};
52
53/// Represents a Discourse webhook payload structure
54#[derive(Debug, Serialize, Deserialize, Clone)]
55pub struct DiscourseWebhookPayload {
56    /// The event type (e.g., "topic_created", "post_edited")
57    #[serde(default)]
58    pub event: Option<String>,
59    /// The webhook data payload
60    #[serde(default)]
61    pub data: Option<serde_json::Value>,
62    /// Unix timestamp when the event occurred
63    #[serde(default)]
64    pub timestamp: Option<i64>,
65}
66
67/// Trait for handling different types of webhook events
68///
69/// Implement this trait to define custom behavior for each event type.
70/// All methods have default implementations that do nothing, so you only
71/// need to implement the events you care about.
72///
73/// # Examples (Sync)
74/// ```rust
75/// use discourse_webhooks::{WebhookEventHandler, TopicWebhookEvent};
76///
77/// struct MyHandler;
78///
79/// impl WebhookEventHandler for MyHandler {
80///     type Error = String;
81///
82///     fn handle_topic_created(&mut self, event: &TopicWebhookEvent) -> Result<(), Self::Error> {
83///         println!("Topic created: {}", event.topic.title);
84///         Ok(())
85///     }
86/// }
87/// ```
88///
89/// # Examples (Async - requires "async" feature)
90/// ```rust
91/// # #[cfg(feature = "async")]
92/// # {
93/// use discourse_webhooks::{WebhookEventHandler, TopicWebhookEvent, async_trait};
94///
95/// struct MyHandler;
96///
97/// #[async_trait]
98/// impl WebhookEventHandler for MyHandler {
99///     type Error = String;
100///
101///     async fn handle_topic_created(&mut self, event: &TopicWebhookEvent) -> Result<(), Self::Error> {
102///         // Your async logic here
103///         println!("Topic created: {}", event.topic.title);
104///         Ok(())
105///     }
106/// }
107/// # }
108/// ```
109#[cfg(not(feature = "async"))]
110pub trait WebhookEventHandler {
111    /// The error type returned by event handlers
112    type Error;
113
114    /// Called when a new topic is created
115    fn handle_topic_created(
116        &mut self,
117        event: &TopicWebhookEvent,
118    ) -> std::result::Result<(), Self::Error> {
119        let _ = event;
120        Ok(())
121    }
122
123    /// Called when a topic is edited
124    fn handle_topic_edited(
125        &mut self,
126        event: &TopicWebhookEvent,
127    ) -> std::result::Result<(), Self::Error> {
128        let _ = event;
129        Ok(())
130    }
131
132    /// Called when a topic is deleted/destroyed
133    fn handle_topic_destroyed(
134        &mut self,
135        event: &TopicWebhookEvent,
136    ) -> std::result::Result<(), Self::Error> {
137        let _ = event;
138        Ok(())
139    }
140
141    /// Called when a deleted topic is recovered
142    fn handle_topic_recovered(
143        &mut self,
144        event: &TopicWebhookEvent,
145    ) -> std::result::Result<(), Self::Error> {
146        let _ = event;
147        Ok(())
148    }
149
150    /// Called when a new post is created
151    fn handle_post_created(
152        &mut self,
153        event: &PostWebhookEvent,
154    ) -> std::result::Result<(), Self::Error> {
155        let _ = event;
156        Ok(())
157    }
158
159    /// Called when a post is edited
160    fn handle_post_edited(
161        &mut self,
162        event: &PostWebhookEvent,
163    ) -> std::result::Result<(), Self::Error> {
164        let _ = event;
165        Ok(())
166    }
167
168    /// Called when a post is deleted/destroyed
169    fn handle_post_destroyed(
170        &mut self,
171        event: &PostWebhookEvent,
172    ) -> std::result::Result<(), Self::Error> {
173        let _ = event;
174        Ok(())
175    }
176
177    /// Called when a deleted post is recovered
178    fn handle_post_recovered(
179        &mut self,
180        event: &PostWebhookEvent,
181    ) -> std::result::Result<(), Self::Error> {
182        let _ = event;
183        Ok(())
184    }
185
186    /// Called when a ping event is received
187    fn handle_ping(&mut self) -> std::result::Result<(), Self::Error> {
188        Ok(())
189    }
190}
191
192#[cfg(feature = "async")]
193#[async_trait]
194pub trait WebhookEventHandler {
195    /// The error type returned by event handlers
196    type Error;
197
198    /// Called when a new topic is created
199    async fn handle_topic_created(
200        &mut self,
201        event: &TopicWebhookEvent,
202    ) -> std::result::Result<(), Self::Error> {
203        let _ = event;
204        Ok(())
205    }
206
207    /// Called when a topic is edited
208    async fn handle_topic_edited(
209        &mut self,
210        event: &TopicWebhookEvent,
211    ) -> std::result::Result<(), Self::Error> {
212        let _ = event;
213        Ok(())
214    }
215
216    /// Called when a topic is deleted/destroyed
217    async fn handle_topic_destroyed(
218        &mut self,
219        event: &TopicWebhookEvent,
220    ) -> std::result::Result<(), Self::Error> {
221        let _ = event;
222        Ok(())
223    }
224
225    /// Called when a deleted topic is recovered
226    async fn handle_topic_recovered(
227        &mut self,
228        event: &TopicWebhookEvent,
229    ) -> std::result::Result<(), Self::Error> {
230        let _ = event;
231        Ok(())
232    }
233
234    /// Called when a new post is created
235    async fn handle_post_created(
236        &mut self,
237        event: &PostWebhookEvent,
238    ) -> std::result::Result<(), Self::Error> {
239        let _ = event;
240        Ok(())
241    }
242
243    /// Called when a post is edited
244    async fn handle_post_edited(
245        &mut self,
246        event: &PostWebhookEvent,
247    ) -> std::result::Result<(), Self::Error> {
248        let _ = event;
249        Ok(())
250    }
251
252    /// Called when a post is deleted/destroyed
253    async fn handle_post_destroyed(
254        &mut self,
255        event: &PostWebhookEvent,
256    ) -> std::result::Result<(), Self::Error> {
257        let _ = event;
258        Ok(())
259    }
260
261    /// Called when a deleted post is recovered
262    async fn handle_post_recovered(
263        &mut self,
264        event: &PostWebhookEvent,
265    ) -> std::result::Result<(), Self::Error> {
266        let _ = event;
267        Ok(())
268    }
269
270    /// Called when a ping event is received
271    async fn handle_ping(&mut self) -> std::result::Result<(), Self::Error> {
272        Ok(())
273    }
274}
275
276/// Process a webhook event using the provided handler (synchronous version)
277///
278/// This function parses the payload based on the event type and calls
279/// the appropriate handler method.
280///
281/// # Arguments
282/// * `handler` - Mutable reference to an event handler
283/// * `event_type` - The type of event (e.g., "topic_created")
284/// * `payload` - The JSON payload from the webhook
285///
286/// # Returns
287/// * `Ok(())` if the event was processed successfully
288/// * `Err(WebhookError)` if parsing or handling failed
289#[cfg(not(feature = "async"))]
290pub fn process_webhook_event<H: WebhookEventHandler>(
291    handler: &mut H,
292    event_type: &str,
293    payload: serde_json::Value,
294) -> std::result::Result<(), WebhookError<H::Error>> {
295    match event_type {
296        "topic_created" | "topic_edited" | "topic_destroyed" | "topic_recovered" => {
297            let event = parse_webhook_payload(event_type, payload)?;
298            if let WebhookEventPayload::TopicEvent(topic_event) = event {
299                let result = match event_type {
300                    "topic_created" => handler.handle_topic_created(&topic_event),
301                    "topic_edited" => handler.handle_topic_edited(&topic_event),
302                    "topic_destroyed" => handler.handle_topic_destroyed(&topic_event),
303                    "topic_recovered" => handler.handle_topic_recovered(&topic_event),
304                    _ => unreachable!(),
305                };
306                result.map_err(WebhookError::HandlerError)?;
307            }
308        }
309        "post_created" | "post_edited" | "post_destroyed" | "post_recovered" => {
310            let event = parse_webhook_payload(event_type, payload)?;
311            if let WebhookEventPayload::PostEvent(post_event) = event {
312                let result = match event_type {
313                    "post_created" => handler.handle_post_created(&post_event),
314                    "post_edited" => handler.handle_post_edited(&post_event),
315                    "post_destroyed" => handler.handle_post_destroyed(&post_event),
316                    "post_recovered" => handler.handle_post_recovered(&post_event),
317                    _ => unreachable!(),
318                };
319                result.map_err(WebhookError::HandlerError)?;
320            }
321        }
322        "ping" => {
323            handler.handle_ping().map_err(WebhookError::HandlerError)?;
324        }
325        _ => {
326            return Err(WebhookError::UnknownEventType(event_type.to_string()));
327        }
328    }
329
330    Ok(())
331}
332
333/// Process a webhook event using the provided handler (asynchronous version)
334///
335/// This function parses the payload based on the event type and calls
336/// the appropriate handler method.
337///
338/// # Arguments
339/// * `handler` - Mutable reference to an event handler
340/// * `event_type` - The type of event (e.g., "topic_created")
341/// * `payload` - The JSON payload from the webhook
342///
343/// # Returns
344/// * `Ok(())` if the event was processed successfully
345/// * `Err(WebhookError)` if parsing or handling failed
346#[cfg(feature = "async")]
347pub async fn process_webhook_event<H: WebhookEventHandler + Send>(
348    handler: &mut H,
349    event_type: &str,
350    payload: serde_json::Value,
351) -> std::result::Result<(), WebhookError<H::Error>> {
352    match event_type {
353        "topic_created" | "topic_edited" | "topic_destroyed" | "topic_recovered" => {
354            let event = parse_webhook_payload(event_type, payload)?;
355            if let WebhookEventPayload::TopicEvent(topic_event) = event {
356                let result = match event_type {
357                    "topic_created" => handler.handle_topic_created(&topic_event).await,
358                    "topic_edited" => handler.handle_topic_edited(&topic_event).await,
359                    "topic_destroyed" => handler.handle_topic_destroyed(&topic_event).await,
360                    "topic_recovered" => handler.handle_topic_recovered(&topic_event).await,
361                    _ => unreachable!(),
362                };
363                result.map_err(WebhookError::HandlerError)?;
364            }
365        }
366        "post_created" | "post_edited" | "post_destroyed" | "post_recovered" => {
367            let event = parse_webhook_payload(event_type, payload)?;
368            if let WebhookEventPayload::PostEvent(post_event) = event {
369                let result = match event_type {
370                    "post_created" => handler.handle_post_created(&post_event).await,
371                    "post_edited" => handler.handle_post_edited(&post_event).await,
372                    "post_destroyed" => handler.handle_post_destroyed(&post_event).await,
373                    "post_recovered" => handler.handle_post_recovered(&post_event).await,
374                    _ => unreachable!(),
375                };
376                result.map_err(WebhookError::HandlerError)?;
377            }
378        }
379        "ping" => {
380            handler
381                .handle_ping()
382                .await
383                .map_err(WebhookError::HandlerError)?;
384        }
385        _ => {
386            return Err(WebhookError::UnknownEventType(event_type.to_string()));
387        }
388    }
389
390    Ok(())
391}
392
393/// A webhook processor that handles signature verification and event dispatching
394///
395/// This struct provides a convenient way to process webhook events with
396/// optional signature verification.
397///
398/// # Examples
399///
400/// ```rust
401/// use discourse_webhooks::WebhookProcessor;
402///
403/// // Without signature verification
404/// let processor = WebhookProcessor::new();
405///
406/// // With signature verification
407/// let processor = WebhookProcessor::new()
408///     .with_secret("your_webhook_secret");
409/// ```
410#[derive(Debug, Clone)]
411pub struct WebhookProcessor {
412    secret: Option<String>,
413    verify_signatures: bool,
414}
415
416impl WebhookProcessor {
417    /// Create a new webhook processor with default settings
418    ///
419    /// By default, signature verification is disabled.
420    pub fn new() -> Self {
421        Self {
422            secret: None,
423            verify_signatures: false,
424        }
425    }
426
427    /// Enable signature verification with the provided secret
428    ///
429    /// # Arguments
430    /// * `secret` - The shared secret key for HMAC verification
431    pub fn with_secret<S: Into<String>>(mut self, secret: S) -> Self {
432        self.secret = Some(secret.into());
433        self.verify_signatures = true;
434        self
435    }
436
437    /// Disable signature verification
438    ///
439    /// This can be useful for development or when webhooks are received
440    /// through a trusted channel.
441    pub fn without_signature_verification(mut self) -> Self {
442        self.verify_signatures = false;
443        self
444    }
445
446    /// Check if signature verification is enabled
447    pub fn verifies_signatures(&self) -> bool {
448        self.verify_signatures
449    }
450
451    /// Get the configured secret (if any)
452    pub fn secret(&self) -> Option<&str> {
453        self.secret.as_deref()
454    }
455
456    /// Process a webhook from a string payload
457    ///
458    /// # Arguments
459    /// * `handler` - Mutable reference to an event handler
460    /// * `event_type` - The type of event (e.g., "topic_created")
461    /// * `payload` - The raw JSON payload as a string
462    /// * `signature` - Optional signature header for verification
463    #[cfg(not(feature = "async"))]
464    pub fn process<H: WebhookEventHandler>(
465        &self,
466        handler: &mut H,
467        event_type: &str,
468        payload: &str,
469        signature: Option<&str>,
470    ) -> Result<(), H::Error> {
471        if self.verify_signatures {
472            if let Some(secret) = &self.secret {
473                if let Some(sig) = signature {
474                    signature::verify_signature(secret, payload, sig)
475                        .map_err(|_| WebhookError::InvalidSignature)?;
476                } else {
477                    return Err(WebhookError::InvalidSignature);
478                }
479            }
480        }
481
482        let json_payload: serde_json::Value = serde_json::from_str(payload)?;
483        process_webhook_event(handler, event_type, json_payload)
484    }
485
486    /// Process a webhook from a string payload (async)
487    ///
488    /// # Arguments
489    /// * `handler` - Mutable reference to an event handler
490    /// * `event_type` - The type of event (e.g., "topic_created")
491    /// * `payload` - The raw JSON payload as a string
492    /// * `signature` - Optional signature header for verification
493    #[cfg(feature = "async")]
494    pub async fn process<H: WebhookEventHandler + Send>(
495        &self,
496        handler: &mut H,
497        event_type: &str,
498        payload: &str,
499        signature: Option<&str>,
500    ) -> Result<(), H::Error> {
501        if self.verify_signatures {
502            if let Some(secret) = &self.secret {
503                if let Some(sig) = signature {
504                    signature::verify_signature(secret, payload, sig)
505                        .map_err(|_| WebhookError::InvalidSignature)?;
506                } else {
507                    return Err(WebhookError::InvalidSignature);
508                }
509            }
510        }
511
512        let json_payload: serde_json::Value = serde_json::from_str(payload)?;
513        process_webhook_event(handler, event_type, json_payload).await
514    }
515
516    /// Process a webhook from a JSON value
517    ///
518    /// # Arguments
519    /// * `handler` - Mutable reference to an event handler
520    /// * `event_type` - The type of event (e.g., "topic_created")
521    /// * `payload` - The JSON payload as a serde_json::Value
522    /// * `signature` - Optional signature header for verification
523    #[cfg(not(feature = "async"))]
524    pub fn process_json<H: WebhookEventHandler>(
525        &self,
526        handler: &mut H,
527        event_type: &str,
528        payload: serde_json::Value,
529        signature: Option<&str>,
530    ) -> Result<(), H::Error> {
531        if self.verify_signatures {
532            if let Some(secret) = &self.secret {
533                if let Some(sig) = signature {
534                    signature::verify_json_signature(secret, &payload, sig)
535                        .map_err(|_| WebhookError::InvalidSignature)?;
536                } else {
537                    return Err(WebhookError::InvalidSignature);
538                }
539            }
540        }
541
542        process_webhook_event(handler, event_type, payload)
543    }
544
545    /// Process a webhook from a JSON value (async)
546    ///
547    /// # Arguments
548    /// * `handler` - Mutable reference to an event handler
549    /// * `event_type` - The type of event (e.g., "topic_created")
550    /// * `payload` - The JSON payload as a serde_json::Value
551    /// * `signature` - Optional signature header for verification
552    #[cfg(feature = "async")]
553    pub async fn process_json<H: WebhookEventHandler + Send>(
554        &self,
555        handler: &mut H,
556        event_type: &str,
557        payload: serde_json::Value,
558        signature: Option<&str>,
559    ) -> Result<(), H::Error> {
560        if self.verify_signatures {
561            if let Some(secret) = &self.secret {
562                if let Some(sig) = signature {
563                    signature::verify_json_signature(secret, &payload, sig)
564                        .map_err(|_| WebhookError::InvalidSignature)?;
565                } else {
566                    return Err(WebhookError::InvalidSignature);
567                }
568            }
569        }
570
571        process_webhook_event(handler, event_type, payload).await
572    }
573}
574
575impl Default for WebhookProcessor {
576    fn default() -> Self {
577        Self::new()
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584    use hmac::Mac;
585    use serde_json::json;
586
587    struct TestHandler {
588        pub topic_created_count: usize,
589        pub post_created_count: usize,
590        pub ping_count: usize,
591    }
592
593    impl TestHandler {
594        fn new() -> Self {
595            Self {
596                topic_created_count: 0,
597                post_created_count: 0,
598                ping_count: 0,
599            }
600        }
601    }
602
603    // Sync tests
604    #[cfg(not(feature = "async"))]
605    impl WebhookEventHandler for TestHandler {
606        type Error = String;
607
608        fn handle_topic_created(
609            &mut self,
610            _event: &TopicWebhookEvent,
611        ) -> std::result::Result<(), Self::Error> {
612            self.topic_created_count += 1;
613            Ok(())
614        }
615
616        fn handle_post_created(
617            &mut self,
618            _event: &PostWebhookEvent,
619        ) -> std::result::Result<(), Self::Error> {
620            self.post_created_count += 1;
621            Ok(())
622        }
623
624        fn handle_ping(&mut self) -> std::result::Result<(), Self::Error> {
625            self.ping_count += 1;
626            Ok(())
627        }
628    }
629
630    #[cfg(not(feature = "async"))]
631    #[test]
632    fn test_webhook_handler_ping() {
633        let mut handler = TestHandler::new();
634        let result = process_webhook_event(&mut handler, "ping", json!({}));
635        assert!(result.is_ok());
636        assert_eq!(handler.ping_count, 1);
637    }
638
639    #[cfg(not(feature = "async"))]
640    #[test]
641    fn test_webhook_handler_invalid_event_type() {
642        let mut handler = TestHandler::new();
643        let result = process_webhook_event(&mut handler, "hatasj", json!({}));
644        assert!(result.is_err());
645
646        if let Err(WebhookError::UnknownEventType(event)) = result {
647            assert_eq!(event, "hatasj");
648        } else {
649            panic!("Expected UnknownEventType error");
650        }
651    }
652
653    #[cfg(not(feature = "async"))]
654    #[test]
655    fn test_webhook_processor() {
656        let processor = WebhookProcessor::new();
657        let mut handler = TestHandler::new();
658
659        let result = processor.process_json(&mut handler, "ping", json!({}), None);
660
661        assert!(result.is_ok());
662        assert_eq!(handler.ping_count, 1);
663    }
664
665    #[cfg(not(feature = "async"))]
666    #[test]
667    fn test_webhook_processor_with_signature() {
668        let secret = "test_secret";
669        let processor = WebhookProcessor::new().with_secret(secret);
670        let mut handler = TestHandler::new();
671
672        let payload = json!({});
673        let payload_str = serde_json::to_string(&payload).unwrap();
674
675        let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(secret.as_bytes()).unwrap();
676        mac.update(payload_str.as_bytes());
677        let signature = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
678
679        let result = processor.process_json(&mut handler, "ping", payload, Some(&signature));
680
681        assert!(result.is_ok());
682        assert_eq!(handler.ping_count, 1);
683    }
684
685    #[cfg(not(feature = "async"))]
686    #[test]
687    fn test_unknown_event_type() {
688        let mut handler = TestHandler::new();
689        let result = process_webhook_event(&mut handler, "unknown_event", json!({}));
690        assert!(result.is_err());
691
692        if let Err(WebhookError::UnknownEventType(event)) = result {
693            assert_eq!(event, "unknown_event");
694        } else {
695            panic!("Expected UnknownEventType error");
696        }
697    }
698
699    // Async tests
700    #[cfg(feature = "async")]
701    #[async_trait]
702    impl WebhookEventHandler for TestHandler {
703        type Error = String;
704
705        async fn handle_topic_created(
706            &mut self,
707            _event: &TopicWebhookEvent,
708        ) -> std::result::Result<(), Self::Error> {
709            self.topic_created_count += 1;
710            Ok(())
711        }
712
713        async fn handle_post_created(
714            &mut self,
715            _event: &PostWebhookEvent,
716        ) -> std::result::Result<(), Self::Error> {
717            self.post_created_count += 1;
718            Ok(())
719        }
720
721        async fn handle_ping(&mut self) -> std::result::Result<(), Self::Error> {
722            self.ping_count += 1;
723            Ok(())
724        }
725    }
726
727    #[cfg(feature = "async")]
728    #[tokio::test]
729    async fn test_webhook_handler_ping() {
730        let mut handler = TestHandler::new();
731        let result = process_webhook_event(&mut handler, "ping", json!({})).await;
732        assert!(result.is_ok());
733        assert_eq!(handler.ping_count, 1);
734    }
735
736    #[cfg(feature = "async")]
737    #[tokio::test]
738    async fn test_webhook_handler_invalid_event_type() {
739        let mut handler = TestHandler::new();
740        let result = process_webhook_event(&mut handler, "hatasj", json!({})).await;
741        assert!(result.is_err());
742
743        if let Err(WebhookError::UnknownEventType(event)) = result {
744            assert_eq!(event, "hatasj");
745        } else {
746            panic!("Expected UnknownEventType error");
747        }
748    }
749
750    #[cfg(feature = "async")]
751    #[tokio::test]
752    async fn test_webhook_processor() {
753        let processor = WebhookProcessor::new();
754        let mut handler = TestHandler::new();
755
756        let result = processor
757            .process_json(&mut handler, "ping", json!({}), None)
758            .await;
759
760        assert!(result.is_ok());
761        assert_eq!(handler.ping_count, 1);
762    }
763
764    #[cfg(feature = "async")]
765    #[tokio::test]
766    async fn test_webhook_processor_with_signature() {
767        let secret = "test_secret";
768        let processor = WebhookProcessor::new().with_secret(secret);
769        let mut handler = TestHandler::new();
770
771        let payload = json!({});
772        let payload_str = serde_json::to_string(&payload).unwrap();
773
774        let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(secret.as_bytes()).unwrap();
775        mac.update(payload_str.as_bytes());
776        let signature = format!("sha256={}", hex::encode(mac.finalize().into_bytes()));
777
778        let result = processor
779            .process_json(&mut handler, "ping", payload, Some(&signature))
780            .await;
781
782        assert!(result.is_ok());
783        assert_eq!(handler.ping_count, 1);
784    }
785
786    #[cfg(feature = "async")]
787    #[tokio::test]
788    async fn test_unknown_event_type() {
789        let mut handler = TestHandler::new();
790        let result = process_webhook_event(&mut handler, "unknown_event", json!({})).await;
791        assert!(result.is_err());
792
793        if let Err(WebhookError::UnknownEventType(event)) = result {
794            assert_eq!(event, "unknown_event");
795        } else {
796            panic!("Expected UnknownEventType error");
797        }
798    }
799}