fraiseql_webhooks/
testing.rs1#![allow(clippy::unwrap_used)] pub mod mocks {
6 use std::{
7 collections::HashMap,
8 sync::{
9 Mutex,
10 atomic::{AtomicU64, Ordering},
11 },
12 };
13
14 use crate::{
15 Clock, IdempotencyStore, Result, SecretProvider, SignatureVerifier, WebhookError,
16 signature::SignatureError,
17 };
18
19 pub struct MockSignatureVerifier {
25 pub should_succeed: bool,
27 pub calls: Mutex<Vec<MockVerifyCall>>,
29 }
30
31 #[derive(Debug, Clone)]
33 pub struct MockVerifyCall {
34 pub payload: Vec<u8>,
36 pub signature: String,
38 }
39
40 impl MockSignatureVerifier {
41 #[must_use]
43 pub fn succeeding() -> Self {
44 Self {
45 should_succeed: true,
46 calls: Mutex::new(Vec::new()),
47 }
48 }
49
50 #[must_use]
52 pub fn failing() -> Self {
53 Self {
54 should_succeed: false,
55 calls: Mutex::new(Vec::new()),
56 }
57 }
58
59 #[must_use]
66 pub fn get_calls(&self) -> Vec<MockVerifyCall> {
67 self.calls.lock().unwrap().clone()
68 }
69 }
70
71 impl SignatureVerifier for MockSignatureVerifier {
72 fn name(&self) -> &'static str {
73 "mock"
74 }
75
76 fn signature_header(&self) -> &'static str {
77 "X-Mock-Signature"
78 }
79
80 fn verify(
81 &self,
82 payload: &[u8],
83 signature: &str,
84 _secret: &str,
85 _timestamp: Option<&str>,
86 _url: Option<&str>,
87 ) -> std::result::Result<bool, SignatureError> {
88 self.calls.lock().unwrap().push(MockVerifyCall {
89 payload: payload.to_vec(),
90 signature: signature.to_string(),
91 });
92 Ok(self.should_succeed)
93 }
94 }
95
96 pub struct MockIdempotencyStore {
98 events: Mutex<HashMap<(String, String), IdempotencyRecord>>,
99 }
100
101 #[derive(Debug, Clone)]
103 pub struct IdempotencyRecord {
104 pub id: uuid::Uuid,
106 pub event_type: String,
108 pub status: String,
110 pub error: Option<String>,
112 }
113
114 impl MockIdempotencyStore {
115 #[must_use]
117 pub fn new() -> Self {
118 Self {
119 events: Mutex::new(HashMap::new()),
120 }
121 }
122
123 #[must_use]
130 pub fn with_existing_events(events: Vec<(&str, &str)>) -> Self {
131 let store = Self::new();
132 let mut map = store.events.lock().unwrap();
133 for (provider, event_id) in events {
134 map.insert(
135 (provider.to_string(), event_id.to_string()),
136 IdempotencyRecord {
137 id: uuid::Uuid::new_v4(),
138 event_type: "test".to_string(),
139 status: "success".to_string(),
140 error: None,
141 },
142 );
143 }
144 drop(map);
145 store
146 }
147
148 #[must_use]
155 pub fn get_record(&self, provider: &str, event_id: &str) -> Option<IdempotencyRecord> {
156 self.events
157 .lock()
158 .unwrap()
159 .get(&(provider.to_string(), event_id.to_string()))
160 .cloned()
161 }
162 }
163
164 impl Default for MockIdempotencyStore {
165 fn default() -> Self {
166 Self::new()
167 }
168 }
169
170 impl IdempotencyStore for MockIdempotencyStore {
171 async fn check(&self, provider: &str, event_id: &str) -> Result<bool> {
172 Ok(self
173 .events
174 .lock()
175 .unwrap()
176 .contains_key(&(provider.to_string(), event_id.to_string())))
177 }
178
179 async fn record(
180 &self,
181 provider: &str,
182 event_id: &str,
183 event_type: &str,
184 status: &str,
185 ) -> Result<uuid::Uuid> {
186 let id = uuid::Uuid::new_v4();
187 self.events.lock().unwrap().insert(
188 (provider.to_string(), event_id.to_string()),
189 IdempotencyRecord {
190 id,
191 event_type: event_type.to_string(),
192 status: status.to_string(),
193 error: None,
194 },
195 );
196 Ok(id)
197 }
198
199 async fn update_status(
200 &self,
201 provider: &str,
202 event_id: &str,
203 status: &str,
204 error: Option<&str>,
205 ) -> Result<()> {
206 if let Some(record) = self
207 .events
208 .lock()
209 .unwrap()
210 .get_mut(&(provider.to_string(), event_id.to_string()))
211 {
212 record.status = status.to_string();
213 record.error = error.map(std::string::ToString::to_string);
214 }
215 Ok(())
216 }
217 }
218
219 pub struct MockSecretProvider {
221 secrets: HashMap<String, String>,
222 }
223
224 impl MockSecretProvider {
225 #[must_use]
227 pub fn new() -> Self {
228 Self {
229 secrets: HashMap::new(),
230 }
231 }
232
233 #[must_use]
235 pub fn with_secret(mut self, name: &str, value: &str) -> Self {
236 self.secrets.insert(name.to_string(), value.to_string());
237 self
238 }
239 }
240
241 impl Default for MockSecretProvider {
242 fn default() -> Self {
243 Self::new()
244 }
245 }
246
247 impl SecretProvider for MockSecretProvider {
248 async fn get_secret(&self, name: &str) -> Result<String> {
249 self.secrets
250 .get(name)
251 .cloned()
252 .ok_or_else(|| WebhookError::MissingSecret(name.to_string()))
253 }
254 }
255
256 pub struct MockClock {
258 current_time: AtomicU64,
259 }
260
261 impl MockClock {
262 #[must_use]
264 pub fn new(timestamp: u64) -> Self {
265 Self {
266 current_time: AtomicU64::new(timestamp),
267 }
268 }
269
270 pub fn advance(&self, seconds: u64) {
272 self.current_time.fetch_add(seconds, Ordering::SeqCst);
273 }
274
275 pub fn set(&self, timestamp: u64) {
277 self.current_time.store(timestamp, Ordering::SeqCst);
278 }
279 }
280
281 impl Clock for MockClock {
282 fn now(&self) -> i64 {
283 self.current_time.load(Ordering::SeqCst) as i64
284 }
285 }
286}