forge_core/testing/context/
webhook.rs1#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
4
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use sqlx::PgPool;
9use uuid::Uuid;
10
11use super::super::mock_dispatch::MockJobDispatch;
12use super::super::mock_http::{MockHttp, MockRequest, MockResponse};
13use crate::Result;
14use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
15
16pub struct TestWebhookContext {
36 pub webhook_name: String,
38 pub request_id: String,
40 pub idempotency_key: Option<String>,
42 headers: HashMap<String, String>,
44 pool: Option<PgPool>,
46 http: Arc<MockHttp>,
48 job_dispatch: Arc<MockJobDispatch>,
50 env_provider: Arc<MockEnvProvider>,
52}
53
54impl TestWebhookContext {
55 pub fn builder(webhook_name: impl Into<String>) -> TestWebhookContextBuilder {
57 TestWebhookContextBuilder::new(webhook_name)
58 }
59
60 pub fn db(&self) -> Option<&PgPool> {
62 self.pool.as_ref()
63 }
64
65 pub fn http(&self) -> &MockHttp {
67 &self.http
68 }
69
70 pub fn header(&self, name: &str) -> Option<&str> {
72 self.headers.get(&name.to_lowercase()).map(|s| s.as_str())
73 }
74
75 pub fn headers(&self) -> &HashMap<String, String> {
77 &self.headers
78 }
79
80 pub fn job_dispatch(&self) -> &MockJobDispatch {
82 &self.job_dispatch
83 }
84
85 pub async fn dispatch_job<T: serde::Serialize>(&self, job_type: &str, args: T) -> Result<Uuid> {
87 self.job_dispatch.dispatch(job_type, args).await
88 }
89
90 pub fn env_mock(&self) -> &MockEnvProvider {
92 &self.env_provider
93 }
94}
95
96impl EnvAccess for TestWebhookContext {
97 fn env_provider(&self) -> &dyn EnvProvider {
98 self.env_provider.as_ref()
99 }
100}
101
102pub struct TestWebhookContextBuilder {
104 webhook_name: String,
105 request_id: Option<String>,
106 idempotency_key: Option<String>,
107 headers: HashMap<String, String>,
108 pool: Option<PgPool>,
109 http: MockHttp,
110 job_dispatch: Arc<MockJobDispatch>,
111 env_vars: HashMap<String, String>,
112}
113
114impl TestWebhookContextBuilder {
115 pub fn new(webhook_name: impl Into<String>) -> Self {
117 Self {
118 webhook_name: webhook_name.into(),
119 request_id: None,
120 idempotency_key: None,
121 headers: HashMap::new(),
122 pool: None,
123 http: MockHttp::new(),
124 job_dispatch: Arc::new(MockJobDispatch::new()),
125 env_vars: HashMap::new(),
126 }
127 }
128
129 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
131 self.request_id = Some(id.into());
132 self
133 }
134
135 pub fn with_idempotency_key(mut self, key: impl Into<String>) -> Self {
137 self.idempotency_key = Some(key.into());
138 self
139 }
140
141 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
143 self.headers
144 .insert(name.into().to_lowercase(), value.into());
145 self
146 }
147
148 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
150 for (k, v) in headers {
151 self.headers.insert(k.to_lowercase(), v);
152 }
153 self
154 }
155
156 pub fn with_pool(mut self, pool: PgPool) -> Self {
158 self.pool = Some(pool);
159 self
160 }
161
162 pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
164 where
165 F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
166 {
167 self.http.add_mock_sync(pattern, handler);
168 self
169 }
170
171 pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
173 let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
174 self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
175 }
176
177 pub fn with_job_dispatch(mut self, dispatch: Arc<MockJobDispatch>) -> Self {
179 self.job_dispatch = dispatch;
180 self
181 }
182
183 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
185 self.env_vars.insert(key.into(), value.into());
186 self
187 }
188
189 pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
191 self.env_vars.extend(vars);
192 self
193 }
194
195 pub fn build(self) -> TestWebhookContext {
197 TestWebhookContext {
198 webhook_name: self.webhook_name,
199 request_id: self
200 .request_id
201 .unwrap_or_else(|| Uuid::new_v4().to_string()),
202 idempotency_key: self.idempotency_key,
203 headers: self.headers,
204 pool: self.pool,
205 http: Arc::new(self.http),
206 job_dispatch: self.job_dispatch,
207 env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_webhook_context_creation() {
218 let ctx = TestWebhookContext::builder("github_webhook")
219 .with_header("X-GitHub-Event", "push")
220 .with_idempotency_key("delivery-123")
221 .build();
222
223 assert_eq!(ctx.webhook_name, "github_webhook");
224 assert_eq!(ctx.idempotency_key, Some("delivery-123".to_string()));
225 assert_eq!(ctx.header("X-GitHub-Event"), Some("push"));
226 assert_eq!(ctx.header("x-github-event"), Some("push")); }
228
229 #[tokio::test]
230 async fn test_dispatch_job() {
231 let ctx = TestWebhookContext::builder("test").build();
232
233 let job_id = ctx
234 .dispatch_job("process_event", serde_json::json!({"action": "push"}))
235 .await
236 .unwrap();
237
238 assert!(!job_id.is_nil());
239 ctx.job_dispatch().assert_dispatched("process_event");
240 }
241
242 #[test]
243 fn test_headers_case_insensitive() {
244 let ctx = TestWebhookContext::builder("test")
245 .with_header("Content-Type", "application/json")
246 .build();
247
248 assert_eq!(ctx.header("content-type"), Some("application/json"));
249 assert_eq!(ctx.header("CONTENT-TYPE"), Some("application/json"));
250 }
251}