fraiseql_server/webhooks/
testing.rs1pub 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 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 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 #[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 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 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}