Skip to main content

fraiseql_webhooks/
traits.rs

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