1use anyhow::{Context, Result};
11use url::Url;
12
13use crate::datadog::client::DatadogClient;
14use crate::datadog::types::{Monitor, MonitorSearchResult};
15
16pub const HARD_CAP: usize = 10_000;
19
20pub const LIST_PAGE_SIZE: usize = 100;
26const SEARCH_PAGE_SIZE: usize = 30;
27
28#[derive(Debug, Default, Clone)]
35pub struct MonitorListFilter {
36 pub name: Option<String>,
38 pub tags: Option<String>,
40 pub monitor_tags: Option<String>,
42}
43
44#[derive(Debug)]
46pub struct MonitorsApi<'a> {
47 client: &'a DatadogClient,
48}
49
50impl<'a> MonitorsApi<'a> {
51 #[must_use]
53 pub fn new(client: &'a DatadogClient) -> Self {
54 Self { client }
55 }
56
57 pub async fn list(&self, filter: &MonitorListFilter, limit: usize) -> Result<Vec<Monitor>> {
64 let cap = effective_cap(limit);
65 let mut out: Vec<Monitor> = Vec::new();
66 let mut page: u32 = 0;
67 loop {
68 let remaining = cap - out.len();
69 let page_size = remaining.min(LIST_PAGE_SIZE);
70 let url = build_list_url(self.client.base_url(), filter, page, page_size)?;
71 let response = self.client.get_json(url.as_str()).await?;
72 if !response.status().is_success() {
73 return Err(DatadogClient::response_to_error(response).await.into());
74 }
75 let batch: Vec<Monitor> = response
76 .json()
77 .await
78 .context("Failed to parse /api/v1/monitor response")?;
79 let exhausted = batch.len() < page_size;
80 out.extend(batch);
81 if out.len() >= cap || exhausted {
82 break;
83 }
84 page += 1;
85 }
86 out.truncate(cap);
87 Ok(out)
88 }
89
90 pub async fn get(&self, id: i64) -> Result<Monitor> {
92 let url = build_get_url(self.client.base_url(), id)?;
93 let response = self.client.get_json(url.as_str()).await?;
94 if !response.status().is_success() {
95 return Err(DatadogClient::response_to_error(response).await.into());
96 }
97 response
98 .json::<Monitor>()
99 .await
100 .context("Failed to parse /api/v1/monitor/<id> response")
101 }
102
103 pub async fn search(&self, query: &str, limit: usize) -> Result<MonitorSearchResult> {
111 let cap = effective_cap(limit);
112 let mut acc: Option<MonitorSearchResult> = None;
113 let mut page: u32 = 0;
114 loop {
115 let collected = acc.as_ref().map_or(0, |r| r.monitors.len());
116 let remaining = cap - collected;
117 let per_page = remaining.min(SEARCH_PAGE_SIZE);
118 let url = build_search_url(self.client.base_url(), query, page, per_page)?;
119 let response = self.client.get_json(url.as_str()).await?;
120 if !response.status().is_success() {
121 return Err(DatadogClient::response_to_error(response).await.into());
122 }
123 let batch: MonitorSearchResult = response
124 .json()
125 .await
126 .context("Failed to parse /api/v1/monitor/search response")?;
127 let batch_len = batch.monitors.len();
128 let page_count = batch.metadata.as_ref().and_then(|m| m.page_count);
129 let exhausted_by_size = batch_len < per_page;
130 let exhausted_by_metadata = page_count.is_some_and(|pc| i64::from(page) + 1 >= pc);
131
132 match acc.as_mut() {
133 Some(existing) => existing.monitors.extend(batch.monitors),
134 None => acc = Some(batch),
135 }
136
137 let collected = acc.as_ref().map_or(0, |r| r.monitors.len());
138 if collected >= cap || exhausted_by_size || exhausted_by_metadata {
139 break;
140 }
141 page += 1;
142 }
143 let mut result = acc.unwrap_or_default();
144 result.monitors.truncate(cap);
145 Ok(result)
146 }
147}
148
149fn effective_cap(limit: usize) -> usize {
152 if limit == 0 {
153 HARD_CAP
154 } else {
155 limit.min(HARD_CAP)
156 }
157}
158
159fn build_list_url(
161 base_url: &str,
162 filter: &MonitorListFilter,
163 page: u32,
164 page_size: usize,
165) -> Result<Url> {
166 let mut url =
167 Url::parse(&format!("{base_url}/api/v1/monitor")).context("Invalid Datadog base URL")?;
168 {
169 let mut q = url.query_pairs_mut();
170 if let Some(name) = filter.name.as_deref() {
171 q.append_pair("name", name);
172 }
173 if let Some(tags) = filter.tags.as_deref() {
174 q.append_pair("tags", tags);
175 }
176 if let Some(monitor_tags) = filter.monitor_tags.as_deref() {
177 q.append_pair("monitor_tags", monitor_tags);
178 }
179 q.append_pair("page", &page.to_string());
180 q.append_pair("page_size", &page_size.to_string());
181 }
182 Ok(url)
183}
184
185fn build_get_url(base_url: &str, id: i64) -> Result<Url> {
187 Url::parse(&format!("{base_url}/api/v1/monitor/{id}")).context("Invalid Datadog base URL")
188}
189
190fn build_search_url(base_url: &str, query: &str, page: u32, per_page: usize) -> Result<Url> {
192 let mut url = Url::parse(&format!("{base_url}/api/v1/monitor/search"))
193 .context("Invalid Datadog base URL")?;
194 url.query_pairs_mut()
195 .append_pair("query", query)
196 .append_pair("page", &page.to_string())
197 .append_pair("per_page", &per_page.to_string());
198 Ok(url)
199}
200
201#[cfg(test)]
202#[allow(clippy::unwrap_used, clippy::expect_used)]
203mod tests {
204 use super::*;
205
206 #[test]
209 fn effective_cap_zero_means_hard_cap() {
210 assert_eq!(effective_cap(0), HARD_CAP);
211 }
212
213 #[test]
214 fn effective_cap_clamps_to_hard_cap() {
215 assert_eq!(effective_cap(HARD_CAP + 5), HARD_CAP);
216 }
217
218 #[test]
219 fn effective_cap_passes_through_small_limits() {
220 assert_eq!(effective_cap(42), 42);
221 }
222
223 #[test]
226 fn build_list_url_appends_only_provided_filters() {
227 let filter = MonitorListFilter {
228 name: Some("cpu".into()),
229 tags: None,
230 monitor_tags: None,
231 };
232 let url = build_list_url("https://api.datadoghq.com", &filter, 0, 100).unwrap();
233 let qs = url.query().unwrap();
234 assert!(qs.contains("name=cpu"));
235 assert!(qs.contains("page=0"));
236 assert!(qs.contains("page_size=100"));
237 assert!(!qs.contains("tags="));
238 assert!(!qs.contains("monitor_tags="));
239 }
240
241 #[test]
242 fn build_list_url_encodes_tags_and_monitor_tags() {
243 let filter = MonitorListFilter {
244 name: None,
245 tags: Some("team:sre,env:prod".into()),
246 monitor_tags: Some("severity:high".into()),
247 };
248 let url = build_list_url("https://api.datadoghq.com", &filter, 2, 50).unwrap();
249 let qs = url.query().unwrap();
250 assert!(qs.contains("tags=team%3Asre%2Cenv%3Aprod"));
252 assert!(qs.contains("monitor_tags=severity%3Ahigh"));
253 assert!(qs.contains("page=2"));
254 assert!(qs.contains("page_size=50"));
255 }
256
257 #[test]
258 fn build_list_url_rejects_invalid_base() {
259 let err = build_list_url("not a url", &MonitorListFilter::default(), 0, 100).unwrap_err();
260 assert!(err.to_string().contains("Invalid Datadog base URL"));
261 }
262
263 #[test]
264 fn build_get_url_includes_id_path_segment() {
265 let url = build_get_url("https://api.datadoghq.com", 12345).unwrap();
266 assert_eq!(url.path(), "/api/v1/monitor/12345");
267 }
268
269 #[test]
270 fn build_get_url_rejects_invalid_base() {
271 let err = build_get_url("not a url", 1).unwrap_err();
272 assert!(err.to_string().contains("Invalid Datadog base URL"));
273 }
274
275 #[test]
276 fn build_search_url_encodes_query() {
277 let url = build_search_url(
278 "https://api.datadoghq.com",
279 "status:alert AND env:prod",
280 0,
281 30,
282 )
283 .unwrap();
284 let qs = url.query().unwrap();
285 assert!(qs.contains("query=status%3Aalert+AND+env%3Aprod"));
286 assert!(qs.contains("page=0"));
287 assert!(qs.contains("per_page=30"));
288 }
289
290 #[test]
291 fn build_search_url_rejects_invalid_base() {
292 let err = build_search_url("not a url", "q", 0, 30).unwrap_err();
293 assert!(err.to_string().contains("Invalid Datadog base URL"));
294 }
295
296 fn monitor_json(id: i64, name: &str) -> serde_json::Value {
299 serde_json::json!({
300 "id": id,
301 "name": name,
302 "type": "metric alert",
303 "query": "avg(last_5m):avg:system.cpu.user{*} > 90",
304 "tags": ["team:sre"],
305 "overall_state": "OK"
306 })
307 }
308
309 fn search_page(items: usize, page: i64, page_count: i64, total: i64) -> serde_json::Value {
310 let monitors: Vec<serde_json::Value> = (0..items)
311 .map(|i| {
312 serde_json::json!({
313 "id": (page * 100) + i as i64,
314 "name": format!("Monitor {i}"),
315 "status": "ALERT",
316 "tags": ["env:prod"]
317 })
318 })
319 .collect();
320 serde_json::json!({
321 "monitors": monitors,
322 "counts": {},
323 "metadata": {
324 "page": page,
325 "per_page": items as i64,
326 "page_count": page_count,
327 "total_count": total
328 }
329 })
330 }
331
332 #[tokio::test]
335 async fn list_single_page_returns_parsed_monitors() {
336 let server = wiremock::MockServer::start().await;
337 wiremock::Mock::given(wiremock::matchers::method("GET"))
338 .and(wiremock::matchers::path("/api/v1/monitor"))
339 .and(wiremock::matchers::query_param("name", "cpu"))
340 .and(wiremock::matchers::query_param("page", "0"))
341 .and(wiremock::matchers::query_param("page_size", "5"))
342 .and(wiremock::matchers::header("DD-API-KEY", "api"))
343 .and(wiremock::matchers::header("DD-APPLICATION-KEY", "app"))
344 .respond_with(
345 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!([
346 monitor_json(1, "Disk full"),
347 monitor_json(2, "CPU high")
348 ])),
349 )
350 .expect(1)
351 .mount(&server)
352 .await;
353
354 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
355 let filter = MonitorListFilter {
356 name: Some("cpu".into()),
357 tags: None,
358 monitor_tags: None,
359 };
360 let monitors = MonitorsApi::new(&client).list(&filter, 5).await.unwrap();
361 assert_eq!(monitors.len(), 2);
362 assert_eq!(monitors[0].id, 1);
363 assert_eq!(monitors[1].name, "CPU high");
364 }
365
366 #[tokio::test]
367 async fn list_auto_paginates_until_short_page() {
368 let server = wiremock::MockServer::start().await;
370 for page in 0..2 {
371 let body: Vec<serde_json::Value> = (0..LIST_PAGE_SIZE as i64)
372 .map(|i| monitor_json(page * 100 + i, "m"))
373 .collect();
374 wiremock::Mock::given(wiremock::matchers::method("GET"))
375 .and(wiremock::matchers::path("/api/v1/monitor"))
376 .and(wiremock::matchers::query_param("page", page.to_string()))
377 .and(wiremock::matchers::query_param(
378 "page_size",
379 LIST_PAGE_SIZE.to_string(),
380 ))
381 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(body))
382 .expect(1)
383 .mount(&server)
384 .await;
385 }
386 let last_page: Vec<serde_json::Value> =
387 (0..37_i64).map(|i| monitor_json(200 + i, "m")).collect();
388 wiremock::Mock::given(wiremock::matchers::method("GET"))
389 .and(wiremock::matchers::path("/api/v1/monitor"))
390 .and(wiremock::matchers::query_param("page", "2"))
391 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(last_page))
392 .expect(1)
393 .mount(&server)
394 .await;
395
396 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
397 let monitors = MonitorsApi::new(&client)
398 .list(&MonitorListFilter::default(), 0)
399 .await
400 .unwrap();
401 assert_eq!(monitors.len(), LIST_PAGE_SIZE * 2 + 37);
402 assert_eq!(monitors[0].id, 0);
404 assert_eq!(monitors.last().unwrap().id, 236);
405 }
406
407 #[tokio::test]
408 async fn list_caps_explicit_limit_at_hard_cap() {
409 let server = wiremock::MockServer::start().await;
413 let body: Vec<serde_json::Value> = (0..LIST_PAGE_SIZE as i64)
414 .map(|i| monitor_json(i, "m"))
415 .collect();
416 wiremock::Mock::given(wiremock::matchers::method("GET"))
419 .and(wiremock::matchers::path("/api/v1/monitor"))
420 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(body))
421 .mount(&server)
422 .await;
423
424 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
425 let monitors = MonitorsApi::new(&client)
426 .list(&MonitorListFilter::default(), HARD_CAP + 50)
427 .await
428 .unwrap();
429 assert_eq!(monitors.len(), HARD_CAP);
430 }
431
432 #[tokio::test]
433 async fn list_stops_when_explicit_limit_reached_within_first_page() {
434 let server = wiremock::MockServer::start().await;
435 let body: Vec<serde_json::Value> = (0..3_i64).map(|i| monitor_json(i, "m")).collect();
438 wiremock::Mock::given(wiremock::matchers::method("GET"))
439 .and(wiremock::matchers::path("/api/v1/monitor"))
440 .and(wiremock::matchers::query_param("page", "0"))
441 .and(wiremock::matchers::query_param("page_size", "3"))
442 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(body))
443 .expect(1)
444 .mount(&server)
445 .await;
446
447 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
448 let monitors = MonitorsApi::new(&client)
449 .list(&MonitorListFilter::default(), 3)
450 .await
451 .unwrap();
452 assert_eq!(monitors.len(), 3);
453 }
454
455 #[tokio::test]
456 async fn list_propagates_api_errors() {
457 let server = wiremock::MockServer::start().await;
458 wiremock::Mock::given(wiremock::matchers::method("GET"))
459 .and(wiremock::matchers::path("/api/v1/monitor"))
460 .respond_with(
461 wiremock::ResponseTemplate::new(403).set_body_string(r#"{"errors":["nope"]}"#),
462 )
463 .mount(&server)
464 .await;
465
466 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
467 let err = MonitorsApi::new(&client)
468 .list(&MonitorListFilter::default(), 5)
469 .await
470 .unwrap_err();
471 let msg = err.to_string();
472 assert!(msg.contains("403"));
473 assert!(msg.contains("nope"));
474 }
475
476 #[tokio::test]
477 async fn list_propagates_invalid_base_url_error() {
478 let client = DatadogClient::new("not a url", "api", "app").unwrap();
479 let err = MonitorsApi::new(&client)
480 .list(&MonitorListFilter::default(), 5)
481 .await
482 .unwrap_err();
483 assert!(err.to_string().contains("Invalid Datadog base URL"));
484 }
485
486 #[tokio::test]
487 async fn list_propagates_network_errors() {
488 let client = DatadogClient::new("http://127.0.0.1:1", "api", "app").unwrap();
491 let err = MonitorsApi::new(&client)
492 .list(&MonitorListFilter::default(), 5)
493 .await
494 .unwrap_err();
495 assert!(err.to_string().contains("Failed to send"));
496 }
497
498 #[tokio::test]
499 async fn list_errors_on_malformed_response() {
500 let server = wiremock::MockServer::start().await;
501 wiremock::Mock::given(wiremock::matchers::method("GET"))
502 .and(wiremock::matchers::path("/api/v1/monitor"))
503 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
504 .mount(&server)
505 .await;
506
507 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
508 let err = MonitorsApi::new(&client)
509 .list(&MonitorListFilter::default(), 5)
510 .await
511 .unwrap_err();
512 assert!(err.to_string().contains("Failed to parse"));
513 }
514
515 #[tokio::test]
518 async fn get_returns_parsed_monitor() {
519 let server = wiremock::MockServer::start().await;
520 wiremock::Mock::given(wiremock::matchers::method("GET"))
521 .and(wiremock::matchers::path("/api/v1/monitor/12345"))
522 .and(wiremock::matchers::header("DD-API-KEY", "api"))
523 .respond_with(
524 wiremock::ResponseTemplate::new(200).set_body_json(monitor_json(12345, "CPU high")),
525 )
526 .expect(1)
527 .mount(&server)
528 .await;
529
530 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
531 let m = MonitorsApi::new(&client).get(12345).await.unwrap();
532 assert_eq!(m.id, 12345);
533 assert_eq!(m.name, "CPU high");
534 }
535
536 #[tokio::test]
537 async fn get_propagates_404() {
538 let server = wiremock::MockServer::start().await;
539 wiremock::Mock::given(wiremock::matchers::method("GET"))
540 .and(wiremock::matchers::path("/api/v1/monitor/9"))
541 .respond_with(
542 wiremock::ResponseTemplate::new(404).set_body_string(r#"{"errors":["Not found"]}"#),
543 )
544 .mount(&server)
545 .await;
546
547 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
548 let err = MonitorsApi::new(&client).get(9).await.unwrap_err();
549 assert!(err.to_string().contains("404"));
550 assert!(err.to_string().contains("Not found"));
551 }
552
553 #[tokio::test]
554 async fn get_propagates_invalid_base_url_error() {
555 let client = DatadogClient::new("not a url", "api", "app").unwrap();
556 let err = MonitorsApi::new(&client).get(1).await.unwrap_err();
557 assert!(err.to_string().contains("Invalid Datadog base URL"));
558 }
559
560 #[tokio::test]
561 async fn get_propagates_network_errors() {
562 let client = DatadogClient::new("http://127.0.0.1:1", "api", "app").unwrap();
563 let err = MonitorsApi::new(&client).get(1).await.unwrap_err();
564 assert!(err.to_string().contains("Failed to send"));
565 }
566
567 #[tokio::test]
568 async fn get_errors_on_malformed_response() {
569 let server = wiremock::MockServer::start().await;
570 wiremock::Mock::given(wiremock::matchers::method("GET"))
571 .and(wiremock::matchers::path("/api/v1/monitor/1"))
572 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
573 .mount(&server)
574 .await;
575
576 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
577 let err = MonitorsApi::new(&client).get(1).await.unwrap_err();
578 assert!(err.to_string().contains("Failed to parse"));
579 }
580
581 #[tokio::test]
584 async fn search_single_page_returns_envelope() {
585 let server = wiremock::MockServer::start().await;
586 wiremock::Mock::given(wiremock::matchers::method("GET"))
587 .and(wiremock::matchers::path("/api/v1/monitor/search"))
588 .and(wiremock::matchers::query_param("query", "status:alert"))
589 .and(wiremock::matchers::query_param("page", "0"))
590 .and(wiremock::matchers::query_param("per_page", "30"))
591 .respond_with(
592 wiremock::ResponseTemplate::new(200).set_body_json(search_page(2, 0, 1, 2)),
593 )
594 .expect(1)
595 .mount(&server)
596 .await;
597
598 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
599 let result = MonitorsApi::new(&client)
600 .search("status:alert", 30)
601 .await
602 .unwrap();
603 assert_eq!(result.monitors.len(), 2);
604 assert_eq!(result.metadata.unwrap().total_count, Some(2));
605 }
606
607 #[tokio::test]
608 async fn search_auto_paginates_with_unbounded_limit() {
609 let server = wiremock::MockServer::start().await;
612 for page in 0..2_i64 {
613 wiremock::Mock::given(wiremock::matchers::method("GET"))
614 .and(wiremock::matchers::path("/api/v1/monitor/search"))
615 .and(wiremock::matchers::query_param("page", page.to_string()))
616 .respond_with(
617 wiremock::ResponseTemplate::new(200).set_body_json(search_page(
618 SEARCH_PAGE_SIZE,
619 page,
620 2,
621 60,
622 )),
623 )
624 .expect(1)
625 .mount(&server)
626 .await;
627 }
628
629 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
630 let result = MonitorsApi::new(&client).search("q", 0).await.unwrap();
631 assert_eq!(result.monitors.len(), SEARCH_PAGE_SIZE * 2);
632 }
633
634 #[tokio::test]
635 async fn search_stops_on_short_page_when_metadata_missing() {
636 let server = wiremock::MockServer::start().await;
638 wiremock::Mock::given(wiremock::matchers::method("GET"))
639 .and(wiremock::matchers::path("/api/v1/monitor/search"))
640 .and(wiremock::matchers::query_param("page", "0"))
641 .respond_with(
642 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
643 "monitors": [
644 { "id": 1_i64, "name": "Only" }
645 ]
646 })),
647 )
648 .expect(1)
649 .mount(&server)
650 .await;
651
652 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
653 let result = MonitorsApi::new(&client).search("q", 0).await.unwrap();
654 assert_eq!(result.monitors.len(), 1);
655 assert!(result.metadata.is_none());
656 }
657
658 #[tokio::test]
659 async fn search_caps_at_explicit_limit_within_full_page() {
660 let server = wiremock::MockServer::start().await;
661 wiremock::Mock::given(wiremock::matchers::method("GET"))
663 .and(wiremock::matchers::path("/api/v1/monitor/search"))
664 .and(wiremock::matchers::query_param("page", "0"))
665 .and(wiremock::matchers::query_param("per_page", "5"))
666 .respond_with(
667 wiremock::ResponseTemplate::new(200).set_body_json(search_page(5, 0, 10, 100)),
668 )
669 .expect(1)
670 .mount(&server)
671 .await;
672
673 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
674 let result = MonitorsApi::new(&client).search("q", 5).await.unwrap();
675 assert_eq!(result.monitors.len(), 5);
676 }
677
678 #[tokio::test]
679 async fn search_propagates_api_errors() {
680 let server = wiremock::MockServer::start().await;
681 wiremock::Mock::given(wiremock::matchers::method("GET"))
682 .and(wiremock::matchers::path("/api/v1/monitor/search"))
683 .respond_with(
684 wiremock::ResponseTemplate::new(400).set_body_string(r#"{"errors":["bad query"]}"#),
685 )
686 .mount(&server)
687 .await;
688
689 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
690 let err = MonitorsApi::new(&client)
691 .search("???", 5)
692 .await
693 .unwrap_err();
694 let msg = err.to_string();
695 assert!(msg.contains("400"));
696 assert!(msg.contains("bad query"));
697 }
698
699 #[tokio::test]
700 async fn search_propagates_invalid_base_url_error() {
701 let client = DatadogClient::new("not a url", "api", "app").unwrap();
702 let err = MonitorsApi::new(&client).search("q", 5).await.unwrap_err();
703 assert!(err.to_string().contains("Invalid Datadog base URL"));
704 }
705
706 #[tokio::test]
707 async fn search_propagates_network_errors() {
708 let client = DatadogClient::new("http://127.0.0.1:1", "api", "app").unwrap();
709 let err = MonitorsApi::new(&client).search("q", 5).await.unwrap_err();
710 assert!(err.to_string().contains("Failed to send"));
711 }
712
713 #[tokio::test]
714 async fn search_errors_on_malformed_response() {
715 let server = wiremock::MockServer::start().await;
716 wiremock::Mock::given(wiremock::matchers::method("GET"))
717 .and(wiremock::matchers::path("/api/v1/monitor/search"))
718 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
719 .mount(&server)
720 .await;
721
722 let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
723 let err = MonitorsApi::new(&client).search("q", 5).await.unwrap_err();
724 assert!(err.to_string().contains("Failed to parse"));
725 }
726}