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