Skip to main content

fraiseql_server/webhooks/
testing.rs

1//! Mock implementations for testing.
2
3pub mod mocks {
4    use std::{
5        collections::HashMap,
6        sync::{
7            Mutex,
8            atomic::{AtomicU64, Ordering},
9        },
10    };
11
12    use async_trait::async_trait;
13
14    use crate::webhooks::{
15        Clock, IdempotencyStore, Result, SecretProvider, SignatureVerifier, WebhookError,
16        signature::SignatureError,
17    };
18
19    /// Mock signature verifier that always succeeds or fails based on configuration
20    pub struct MockSignatureVerifier {
21        pub should_succeed: bool,
22        pub calls:          Mutex<Vec<MockVerifyCall>>,
23    }
24
25    #[derive(Debug, Clone)]
26    pub struct MockVerifyCall {
27        pub payload:   Vec<u8>,
28        pub signature: String,
29    }
30
31    impl MockSignatureVerifier {
32        #[must_use]
33        pub fn succeeding() -> Self {
34            Self {
35                should_succeed: true,
36                calls:          Mutex::new(Vec::new()),
37            }
38        }
39
40        #[must_use]
41        pub fn failing() -> Self {
42            Self {
43                should_succeed: false,
44                calls:          Mutex::new(Vec::new()),
45            }
46        }
47
48        #[must_use]
49        pub fn get_calls(&self) -> Vec<MockVerifyCall> {
50            self.calls.lock().unwrap().clone()
51        }
52    }
53
54    impl SignatureVerifier for MockSignatureVerifier {
55        fn name(&self) -> &'static str {
56            "mock"
57        }
58
59        fn signature_header(&self) -> &'static str {
60            "X-Mock-Signature"
61        }
62
63        fn verify(
64            &self,
65            payload: &[u8],
66            signature: &str,
67            _secret: &str,
68            _timestamp: Option<&str>,
69        ) -> std::result::Result<bool, SignatureError> {
70            self.calls.lock().unwrap().push(MockVerifyCall {
71                payload:   payload.to_vec(),
72                signature: signature.to_string(),
73            });
74            Ok(self.should_succeed)
75        }
76    }
77
78    /// Mock idempotency store with in-memory storage
79    pub struct MockIdempotencyStore {
80        events: Mutex<HashMap<(String, String), IdempotencyRecord>>,
81    }
82
83    #[derive(Debug, Clone)]
84    pub struct IdempotencyRecord {
85        pub id:         uuid::Uuid,
86        pub event_type: String,
87        pub status:     String,
88        pub error:      Option<String>,
89    }
90
91    impl MockIdempotencyStore {
92        #[must_use]
93        pub fn new() -> Self {
94            Self {
95                events: Mutex::new(HashMap::new()),
96            }
97        }
98
99        /// Pre-populate with existing events for testing duplicates
100        #[must_use]
101        pub fn with_existing_events(events: Vec<(&str, &str)>) -> Self {
102            let store = Self::new();
103            let mut map = store.events.lock().unwrap();
104            for (provider, event_id) in events {
105                map.insert(
106                    (provider.to_string(), event_id.to_string()),
107                    IdempotencyRecord {
108                        id:         uuid::Uuid::new_v4(),
109                        event_type: "test".to_string(),
110                        status:     "success".to_string(),
111                        error:      None,
112                    },
113                );
114            }
115            drop(map);
116            store
117        }
118
119        #[must_use]
120        pub fn get_record(&self, provider: &str, event_id: &str) -> Option<IdempotencyRecord> {
121            self.events
122                .lock()
123                .unwrap()
124                .get(&(provider.to_string(), event_id.to_string()))
125                .cloned()
126        }
127    }
128
129    impl Default for MockIdempotencyStore {
130        fn default() -> Self {
131            Self::new()
132        }
133    }
134
135    #[async_trait]
136    impl IdempotencyStore for MockIdempotencyStore {
137        async fn check(&self, provider: &str, event_id: &str) -> Result<bool> {
138            Ok(self
139                .events
140                .lock()
141                .unwrap()
142                .contains_key(&(provider.to_string(), event_id.to_string())))
143        }
144
145        async fn record(
146            &self,
147            provider: &str,
148            event_id: &str,
149            event_type: &str,
150            status: &str,
151        ) -> Result<uuid::Uuid> {
152            let id = uuid::Uuid::new_v4();
153            self.events.lock().unwrap().insert(
154                (provider.to_string(), event_id.to_string()),
155                IdempotencyRecord {
156                    id,
157                    event_type: event_type.to_string(),
158                    status: status.to_string(),
159                    error: None,
160                },
161            );
162            Ok(id)
163        }
164
165        async fn update_status(
166            &self,
167            provider: &str,
168            event_id: &str,
169            status: &str,
170            error: Option<&str>,
171        ) -> Result<()> {
172            if let Some(record) = self
173                .events
174                .lock()
175                .unwrap()
176                .get_mut(&(provider.to_string(), event_id.to_string()))
177            {
178                record.status = status.to_string();
179                record.error = error.map(std::string::ToString::to_string);
180            }
181            Ok(())
182        }
183    }
184
185    /// Mock secret provider with configurable secrets
186    pub struct MockSecretProvider {
187        secrets: HashMap<String, String>,
188    }
189
190    impl MockSecretProvider {
191        #[must_use]
192        pub fn new() -> Self {
193            Self {
194                secrets: HashMap::new(),
195            }
196        }
197
198        #[must_use]
199        pub fn with_secret(mut self, name: &str, value: &str) -> Self {
200            self.secrets.insert(name.to_string(), value.to_string());
201            self
202        }
203    }
204
205    impl Default for MockSecretProvider {
206        fn default() -> Self {
207            Self::new()
208        }
209    }
210
211    #[async_trait]
212    impl SecretProvider for MockSecretProvider {
213        async fn get_secret(&self, name: &str) -> Result<String> {
214            self.secrets
215                .get(name)
216                .cloned()
217                .ok_or_else(|| WebhookError::MissingSecret(name.to_string()))
218        }
219    }
220
221    /// Mock clock for testing timestamp validation
222    pub struct MockClock {
223        current_time: AtomicU64,
224    }
225
226    impl MockClock {
227        #[must_use]
228        pub fn new(timestamp: u64) -> Self {
229            Self {
230                current_time: AtomicU64::new(timestamp),
231            }
232        }
233
234        pub fn advance(&self, seconds: u64) {
235            self.current_time.fetch_add(seconds, Ordering::SeqCst);
236        }
237
238        pub fn set(&self, timestamp: u64) {
239            self.current_time.store(timestamp, Ordering::SeqCst);
240        }
241    }
242
243    impl Clock for MockClock {
244        fn now(&self) -> i64 {
245            self.current_time.load(Ordering::SeqCst) as i64
246        }
247    }
248}