1use crate::client::Client;
8use crate::error::{Error, Result};
9use crate::models::{
10 WebhookAlert, WebhookAlertCreateInput, WebhookAlertUpdateInput, WebhookEndpoint,
11 WebhookEndpointCreateInput, WebhookEndpointUpdateInput, WebhookEventTypesResponse,
12 WebhookSamplePayloadResponse, WebhookTestDeliveryResult,
13};
14use crate::pagination::Page;
15use crate::resources::agencies::urlencoding;
16use crate::ListOptions;
17use serde::Serialize;
18
19#[derive(Serialize)]
22struct TestDeliveryBody<'a> {
23 endpoint: &'a str,
24}
25
26impl Client {
31 pub async fn list_webhook_event_types(&self) -> Result<WebhookEventTypesResponse> {
33 self.get_json::<WebhookEventTypesResponse>("/api/webhooks/event-types/", &[])
34 .await
35 }
36
37 pub async fn list_webhook_endpoints(&self, opts: ListOptions) -> Result<Page<WebhookEndpoint>> {
40 let mut q = Vec::new();
41 opts.apply(&mut q);
42 self.get_page("/api/webhooks/endpoints/", &q).await
43 }
44
45 pub async fn get_webhook_endpoint(&self, id: &str) -> Result<WebhookEndpoint> {
47 if id.is_empty() {
48 return Err(Error::Validation {
49 message: "GetWebhookEndpoint: id is required".into(),
50 response: None,
51 });
52 }
53 let path = format!("/api/webhooks/endpoints/{}/", urlencoding(id));
54 self.get_json::<WebhookEndpoint>(&path, &[]).await
55 }
56
57 pub async fn create_webhook_endpoint(
63 &self,
64 input: WebhookEndpointCreateInput,
65 ) -> Result<WebhookEndpoint> {
66 if input.name.is_empty() {
67 return Err(Error::Validation {
68 message: "CreateWebhookEndpoint: name is required (the Tango API enforces unique(user, name) on endpoints)".into(),
69 response: None,
70 });
71 }
72 if input.callback_url.is_empty() {
73 return Err(Error::Validation {
74 message: "CreateWebhookEndpoint: callback_url is required".into(),
75 response: None,
76 });
77 }
78 self.post_json::<_, WebhookEndpoint>("/api/webhooks/endpoints/", &input)
79 .await
80 }
81
82 pub async fn update_webhook_endpoint(
85 &self,
86 id: &str,
87 input: WebhookEndpointUpdateInput,
88 ) -> Result<WebhookEndpoint> {
89 if id.is_empty() {
90 return Err(Error::Validation {
91 message: "UpdateWebhookEndpoint: id is required".into(),
92 response: None,
93 });
94 }
95 let path = format!("/api/webhooks/endpoints/{}/", urlencoding(id));
96 self.patch_json::<_, WebhookEndpoint>(&path, &input).await
97 }
98
99 pub async fn delete_webhook_endpoint(&self, id: &str) -> Result<()> {
101 if id.is_empty() {
102 return Err(Error::Validation {
103 message: "DeleteWebhookEndpoint: id is required".into(),
104 response: None,
105 });
106 }
107 let path = format!("/api/webhooks/endpoints/{}/", urlencoding(id));
108 self.delete_no_content(&path).await
109 }
110
111 pub async fn test_webhook_endpoint(
116 &self,
117 endpoint_id: &str,
118 ) -> Result<WebhookTestDeliveryResult> {
119 if endpoint_id.is_empty() {
120 return Err(Error::Validation {
121 message: "TestWebhookEndpoint: endpoint_id is required".into(),
122 response: None,
123 });
124 }
125 let body = TestDeliveryBody {
126 endpoint: endpoint_id,
127 };
128 self.post_json::<_, WebhookTestDeliveryResult>(
129 "/api/webhooks/endpoints/test-delivery/",
130 &body,
131 )
132 .await
133 }
134
135 pub async fn get_webhook_sample_payload(
139 &self,
140 event_type: Option<&str>,
141 ) -> Result<WebhookSamplePayloadResponse> {
142 let mut q = Vec::new();
143 if let Some(ev) = event_type.filter(|s| !s.is_empty()) {
144 q.push(("event_type".into(), ev.into()));
145 }
146 self.get_json::<WebhookSamplePayloadResponse>("/api/webhooks/endpoints/sample-payload/", &q)
147 .await
148 }
149
150 pub async fn list_webhook_alerts(&self, opts: ListOptions) -> Result<Page<WebhookAlert>> {
156 let mut q = Vec::new();
157 opts.apply(&mut q);
158 self.get_page("/api/webhooks/alerts/", &q).await
159 }
160
161 pub async fn get_webhook_alert(&self, id: &str) -> Result<WebhookAlert> {
163 if id.is_empty() {
164 return Err(Error::Validation {
165 message: "GetWebhookAlert: id is required".into(),
166 response: None,
167 });
168 }
169 let path = format!("/api/webhooks/alerts/{}/", urlencoding(id));
170 self.get_json::<WebhookAlert>(&path, &[]).await
171 }
172
173 pub async fn create_webhook_alert(
180 &self,
181 input: WebhookAlertCreateInput,
182 ) -> Result<WebhookAlert> {
183 if input.name.is_empty() {
184 return Err(Error::Validation {
185 message: "CreateWebhookAlert: name is required".into(),
186 response: None,
187 });
188 }
189 if input.query_type.is_empty() {
190 return Err(Error::Validation {
191 message:
192 r#"CreateWebhookAlert: query_type is required (singular, e.g. "contract")"#
193 .into(),
194 response: None,
195 });
196 }
197 let empty = match &input.filters {
198 serde_json::Value::Null => true,
199 serde_json::Value::Object(m) => m.is_empty(),
200 serde_json::Value::Array(a) => a.is_empty(),
201 _ => false,
202 };
203 if empty {
204 return Err(Error::Validation {
205 message: "CreateWebhookAlert: filters must be a non-empty object".into(),
206 response: None,
207 });
208 }
209 self.post_json::<_, WebhookAlert>("/api/webhooks/alerts/", &input)
210 .await
211 }
212
213 pub async fn update_webhook_alert(
216 &self,
217 id: &str,
218 input: WebhookAlertUpdateInput,
219 ) -> Result<WebhookAlert> {
220 if id.is_empty() {
221 return Err(Error::Validation {
222 message: "UpdateWebhookAlert: id is required".into(),
223 response: None,
224 });
225 }
226 let path = format!("/api/webhooks/alerts/{}/", urlencoding(id));
227 self.patch_json::<_, WebhookAlert>(&path, &input).await
228 }
229
230 pub async fn delete_webhook_alert(&self, id: &str) -> Result<()> {
232 if id.is_empty() {
233 return Err(Error::Validation {
234 message: "DeleteWebhookAlert: id is required".into(),
235 response: None,
236 });
237 }
238 let path = format!("/api/webhooks/alerts/{}/", urlencoding(id));
239 self.delete_no_content(&path).await
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use serde_json::json;
247
248 fn client() -> Client {
249 Client::builder()
252 .api_key("k")
253 .base_url("http://localhost:1".to_string())
254 .build()
255 .expect("build client")
256 }
257
258 #[tokio::test]
261 async fn get_endpoint_rejects_empty_id() {
262 let err = client().get_webhook_endpoint("").await.unwrap_err();
263 assert!(matches!(err, Error::Validation { .. }));
264 }
265
266 #[tokio::test]
267 async fn update_endpoint_rejects_empty_id() {
268 let err = client()
269 .update_webhook_endpoint("", WebhookEndpointUpdateInput::default())
270 .await
271 .unwrap_err();
272 assert!(matches!(err, Error::Validation { .. }));
273 }
274
275 #[tokio::test]
276 async fn delete_endpoint_rejects_empty_id() {
277 let err = client().delete_webhook_endpoint("").await.unwrap_err();
278 assert!(matches!(err, Error::Validation { .. }));
279 }
280
281 #[tokio::test]
282 async fn test_endpoint_rejects_empty_id() {
283 let err = client().test_webhook_endpoint("").await.unwrap_err();
284 assert!(matches!(err, Error::Validation { .. }));
285 }
286
287 #[tokio::test]
290 async fn create_endpoint_rejects_empty_name() {
291 let err = client()
292 .create_webhook_endpoint(WebhookEndpointCreateInput {
293 name: String::new(),
294 callback_url: "https://example.com/hook".into(),
295 is_active: None,
296 event_types: vec![],
297 })
298 .await
299 .unwrap_err();
300 assert!(matches!(err, Error::Validation { .. }));
301 }
302
303 #[tokio::test]
304 async fn create_endpoint_rejects_empty_callback_url() {
305 let err = client()
306 .create_webhook_endpoint(WebhookEndpointCreateInput {
307 name: "my-hook".into(),
308 callback_url: String::new(),
309 is_active: None,
310 event_types: vec![],
311 })
312 .await
313 .unwrap_err();
314 assert!(matches!(err, Error::Validation { .. }));
315 }
316
317 #[tokio::test]
320 async fn get_alert_rejects_empty_id() {
321 let err = client().get_webhook_alert("").await.unwrap_err();
322 assert!(matches!(err, Error::Validation { .. }));
323 }
324
325 #[tokio::test]
326 async fn update_alert_rejects_empty_id() {
327 let err = client()
328 .update_webhook_alert("", WebhookAlertUpdateInput::default())
329 .await
330 .unwrap_err();
331 assert!(matches!(err, Error::Validation { .. }));
332 }
333
334 #[tokio::test]
335 async fn delete_alert_rejects_empty_id() {
336 let err = client().delete_webhook_alert("").await.unwrap_err();
337 assert!(matches!(err, Error::Validation { .. }));
338 }
339
340 #[tokio::test]
343 async fn create_alert_rejects_empty_name() {
344 let err = client()
345 .create_webhook_alert(WebhookAlertCreateInput {
346 name: String::new(),
347 query_type: "contract".into(),
348 filters: json!({"piid": "X"}),
349 frequency: None,
350 cron_expression: None,
351 endpoint: None,
352 })
353 .await
354 .unwrap_err();
355 assert!(matches!(err, Error::Validation { .. }));
356 }
357
358 #[tokio::test]
359 async fn create_alert_rejects_empty_query_type() {
360 let err = client()
361 .create_webhook_alert(WebhookAlertCreateInput {
362 name: "n".into(),
363 query_type: String::new(),
364 filters: json!({"piid": "X"}),
365 frequency: None,
366 cron_expression: None,
367 endpoint: None,
368 })
369 .await
370 .unwrap_err();
371 assert!(matches!(err, Error::Validation { .. }));
372 }
373
374 #[tokio::test]
375 async fn create_alert_rejects_null_filters() {
376 let err = client()
377 .create_webhook_alert(WebhookAlertCreateInput {
378 name: "n".into(),
379 query_type: "contract".into(),
380 filters: serde_json::Value::Null,
381 frequency: None,
382 cron_expression: None,
383 endpoint: None,
384 })
385 .await
386 .unwrap_err();
387 assert!(matches!(err, Error::Validation { .. }));
388 }
389
390 #[tokio::test]
391 async fn create_alert_rejects_empty_object_filters() {
392 let err = client()
393 .create_webhook_alert(WebhookAlertCreateInput {
394 name: "n".into(),
395 query_type: "contract".into(),
396 filters: json!({}),
397 frequency: None,
398 cron_expression: None,
399 endpoint: None,
400 })
401 .await
402 .unwrap_err();
403 assert!(matches!(err, Error::Validation { .. }));
404 }
405
406 #[tokio::test]
407 async fn create_alert_rejects_empty_array_filters() {
408 let err = client()
409 .create_webhook_alert(WebhookAlertCreateInput {
410 name: "n".into(),
411 query_type: "contract".into(),
412 filters: json!([]),
413 frequency: None,
414 cron_expression: None,
415 endpoint: None,
416 })
417 .await
418 .unwrap_err();
419 assert!(matches!(err, Error::Validation { .. }));
420 }
421}