mockforge_http/handlers/
webhook_test.rs1use axum::extract::State;
7use axum::http::StatusCode;
8use axum::response::Json;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12
13#[derive(Clone)]
15pub struct WebhookTestState {
16 pub received_webhooks: Arc<tokio::sync::RwLock<Vec<ReceivedWebhook>>>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ReceivedWebhook {
23 pub received_at: String,
25 pub url: String,
27 pub event: String,
29 pub payload: serde_json::Value,
31 pub headers: HashMap<String, String>,
33}
34
35impl Default for WebhookTestState {
36 fn default() -> Self {
37 Self {
38 received_webhooks: Arc::new(tokio::sync::RwLock::new(Vec::new())),
39 }
40 }
41}
42
43#[derive(Debug, Deserialize, Serialize)]
45pub struct TestWebhookRequest {
46 pub url: String,
48 pub event: String,
50 pub payload: serde_json::Value,
52 pub headers: Option<HashMap<String, String>>,
54}
55
56#[derive(Debug, Serialize)]
58pub struct TestWebhookResponse {
59 pub success: bool,
61 pub status_code: Option<u16>,
63 pub response_body: Option<String>,
65 pub error: Option<String>,
67}
68
69pub async fn test_webhook(
73 State(_state): State<WebhookTestState>,
74 Json(request): Json<TestWebhookRequest>,
75) -> Result<Json<TestWebhookResponse>, StatusCode> {
76 let client = reqwest::Client::new();
77
78 let mut req = client.post(&request.url).json(&request.payload);
79
80 if let Some(headers) = &request.headers {
82 for (key, value) in headers {
83 req = req.header(key, value);
84 }
85 }
86
87 match req.send().await {
88 Ok(response) => {
89 let status_code = response.status().as_u16();
90 let response_body = response.text().await.ok();
91
92 Ok(Json(TestWebhookResponse {
93 success: status_code < 400,
94 status_code: Some(status_code),
95 response_body,
96 error: None,
97 }))
98 }
99 Err(e) => Ok(Json(TestWebhookResponse {
100 success: false,
101 status_code: None,
102 response_body: None,
103 error: Some(e.to_string()),
104 })),
105 }
106}
107
108pub async fn receive_webhook(
112 State(state): State<WebhookTestState>,
113 headers: axum::http::HeaderMap,
114 Json(payload): Json<serde_json::Value>,
115) -> Result<Json<serde_json::Value>, StatusCode> {
116 let event = headers
118 .get("x-webhook-event")
119 .and_then(|h| h.to_str().ok())
120 .map(|s| s.to_string())
121 .or_else(|| payload.get("event").and_then(|v| v.as_str()).map(|s| s.to_string()))
122 .unwrap_or_else(|| "unknown".to_string());
123
124 let url = headers
126 .get("x-webhook-url")
127 .and_then(|h| h.to_str().ok())
128 .map(|s| s.to_string())
129 .unwrap_or_else(|| "unknown".to_string());
130
131 let mut header_map = HashMap::new();
133 for (key, value) in headers.iter() {
134 let key_str = key.as_str().to_string();
135 if let Ok(value_str) = value.to_str() {
136 header_map.insert(key_str, value_str.to_string());
137 }
138 }
139
140 let received = ReceivedWebhook {
142 received_at: chrono::Utc::now().to_rfc3339(),
143 url,
144 event,
145 payload,
146 headers: header_map,
147 };
148
149 state.received_webhooks.write().await.push(received.clone());
150
151 Ok(Json(serde_json::json!({
152 "status": "received",
153 "event": received.event,
154 "received_at": received.received_at,
155 })))
156}
157
158pub async fn get_received_webhooks(
162 State(state): State<WebhookTestState>,
163) -> Result<Json<Vec<ReceivedWebhook>>, StatusCode> {
164 let webhooks = state.received_webhooks.read().await.clone();
165 Ok(Json(webhooks))
166}
167
168pub async fn clear_received_webhooks(
172 State(state): State<WebhookTestState>,
173) -> Result<Json<serde_json::Value>, StatusCode> {
174 state.received_webhooks.write().await.clear();
175 Ok(Json(serde_json::json!({"status": "cleared"})))
176}
177
178pub fn webhook_test_router(state: WebhookTestState) -> axum::Router {
180 use axum::routing::{delete, get, post};
181
182 axum::Router::new()
183 .route("/api/v1/webhooks/test", post(test_webhook))
184 .route("/api/v1/webhooks/receive", post(receive_webhook))
185 .route("/api/v1/webhooks/received", get(get_received_webhooks))
186 .route("/api/v1/webhooks/received", delete(clear_received_webhooks))
187 .with_state(state)
188}