Skip to main content

fraiseql_server/webhooks/
traits.rs

1//! Testing seams for webhook dependencies.
2//!
3//! All external dependencies are abstracted behind traits for easy testing.
4
5use async_trait::async_trait;
6use serde_json::Value;
7use sqlx::{Postgres, Transaction};
8
9use super::{Result, signature::SignatureError};
10
11/// Signature verification abstraction for testing
12#[async_trait]
13pub trait SignatureVerifier: Send + Sync {
14    /// Provider name (e.g., "stripe", "github")
15    fn name(&self) -> &'static str;
16
17    /// Header name containing the signature
18    fn signature_header(&self) -> &'static str;
19
20    /// Verify the signature
21    ///
22    /// # Arguments
23    ///
24    /// * `payload` - Raw request body bytes
25    /// * `signature` - Signature from header
26    /// * `secret` - Webhook signing secret
27    /// * `timestamp` - Optional timestamp from headers (for replay protection)
28    ///
29    /// # Returns
30    ///
31    /// `Ok(true)` if signature is valid, `Ok(false)` if invalid, `Err` for format errors
32    fn verify(
33        &self,
34        payload: &[u8],
35        signature: &str,
36        secret: &str,
37        timestamp: Option<&str>,
38    ) -> std::result::Result<bool, SignatureError>;
39
40    /// Optional: Extract timestamp from signature or headers
41    fn extract_timestamp(&self, _signature: &str) -> Option<i64> {
42        None
43    }
44}
45
46/// Idempotency store abstraction for testing
47#[async_trait]
48pub trait IdempotencyStore: Send + Sync {
49    /// Check if event has already been processed
50    async fn check(&self, provider: &str, event_id: &str) -> Result<bool>;
51
52    /// Record processed event
53    async fn record(
54        &self,
55        provider: &str,
56        event_id: &str,
57        event_type: &str,
58        status: &str,
59    ) -> Result<uuid::Uuid>;
60
61    /// Update event status
62    async fn update_status(
63        &self,
64        provider: &str,
65        event_id: &str,
66        status: &str,
67        error: Option<&str>,
68    ) -> Result<()>;
69}
70
71/// Secret provider abstraction for testing
72#[async_trait]
73pub trait SecretProvider: Send + Sync {
74    /// Get webhook secret by name
75    async fn get_secret(&self, name: &str) -> Result<String>;
76}
77
78/// Event handler abstraction for testing
79#[async_trait]
80pub trait EventHandler: Send + Sync {
81    /// Handle webhook event by calling database function
82    async fn handle(
83        &self,
84        function_name: &str,
85        params: Value,
86        tx: &mut Transaction<'_, Postgres>,
87    ) -> Result<Value>;
88}
89
90/// Clock abstraction for testing timestamp validation
91pub trait Clock: Send + Sync {
92    /// Get current Unix timestamp
93    fn now(&self) -> i64;
94}
95
96/// System clock implementation
97pub struct SystemClock;
98
99impl Clock for SystemClock {
100    fn now(&self) -> i64 {
101        std::time::SystemTime::now()
102            .duration_since(std::time::UNIX_EPOCH)
103            .unwrap()
104            .as_secs() as i64
105    }
106}