1use std::fmt;
54use std::sync::{Arc, Mutex};
55use std::time::Duration;
56
57use axum::http::{Method, StatusCode};
58use serde::Serialize;
59use serde::de::DeserializeOwned;
60
61use crate::{Error, Result};
62
63const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
65
66#[derive(Clone, Default)]
68pub struct Http {
69 fake: Arc<Mutex<Option<Arc<Mutex<FakeState>>>>>,
70}
71
72impl fmt::Debug for Http {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str("Http")
75 }
76}
77
78impl Http {
79 pub fn get(&self, url: impl Into<String>) -> Request {
81 self.request(Method::GET, url)
82 }
83
84 pub fn post(&self, url: impl Into<String>) -> Request {
86 self.request(Method::POST, url)
87 }
88
89 pub fn put(&self, url: impl Into<String>) -> Request {
91 self.request(Method::PUT, url)
92 }
93
94 pub fn patch(&self, url: impl Into<String>) -> Request {
96 self.request(Method::PATCH, url)
97 }
98
99 pub fn delete(&self, url: impl Into<String>) -> Request {
101 self.request(Method::DELETE, url)
102 }
103
104 pub fn request(&self, method: Method, url: impl Into<String>) -> Request {
106 Request {
107 http: self.clone(),
108 method,
109 url: url.into(),
110 query: Vec::new(),
111 headers: Vec::new(),
112 body: None,
113 timeout: DEFAULT_TIMEOUT,
114 retries: 0,
115 retry_delay: Duration::from_millis(100),
116 error: None,
117 }
118 }
119
120 pub fn fake(&self) -> FakeHttp {
123 let mut slot = self.fake.lock().unwrap_or_else(|e| e.into_inner());
124 let state = slot.get_or_insert_with(Arc::default).clone();
125 FakeHttp(state)
126 }
127
128 fn faked(&self) -> Option<Arc<Mutex<FakeState>>> {
129 self.fake.lock().unwrap_or_else(|e| e.into_inner()).clone()
130 }
131}
132
133#[must_use = "a request does nothing until sent"]
135pub struct Request {
136 http: Http,
137 method: Method,
138 url: String,
139 query: Vec<(String, String)>,
140 headers: Vec<(String, String)>,
141 body: Option<(String, Vec<u8>)>,
142 timeout: Duration,
143 retries: u32,
144 retry_delay: Duration,
145 error: Option<Error>,
146}
147
148impl Request {
149 pub fn query(mut self, pairs: &[(&str, &str)]) -> Self {
151 self.query.extend(
152 pairs
153 .iter()
154 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned())),
155 );
156 self
157 }
158
159 pub fn header(mut self, name: &str, value: impl Into<String>) -> Self {
161 self.headers.push((name.to_owned(), value.into()));
162 self
163 }
164
165 pub fn bearer(self, token: &str) -> Self {
167 self.header("authorization", format!("Bearer {token}"))
168 }
169
170 pub fn basic_auth(self, user: &str, password: &str) -> Self {
172 use base64::Engine;
173 let encoded =
174 base64::engine::general_purpose::STANDARD.encode(format!("{user}:{password}"));
175 self.header("authorization", format!("Basic {encoded}"))
176 }
177
178 pub fn json(mut self, body: &impl Serialize) -> Self {
180 match serde_json::to_vec(body) {
181 Ok(bytes) => self.body = Some(("application/json".into(), bytes)),
182 Err(err) => self.error = Some(err.into()),
183 }
184 self
185 }
186
187 pub fn form(mut self, body: &impl Serialize) -> Self {
189 match serde_urlencoded::to_string(body) {
190 Ok(text) => {
191 self.body = Some((
192 "application/x-www-form-urlencoded".into(),
193 text.into_bytes(),
194 ));
195 }
196 Err(err) => self.error = Some(anyhow::Error::new(err).into()),
197 }
198 self
199 }
200
201 pub fn body(mut self, content_type: &str, body: impl Into<Vec<u8>>) -> Self {
203 self.body = Some((content_type.to_owned(), body.into()));
204 self
205 }
206
207 pub fn timeout(mut self, timeout: Duration) -> Self {
209 self.timeout = timeout;
210 self
211 }
212
213 pub fn retry(mut self, times: u32, delay: Duration) -> Self {
216 self.retries = times;
217 self.retry_delay = delay;
218 self
219 }
220
221 fn full_url(&self) -> Result<String> {
223 if self.query.is_empty() {
224 return Ok(self.url.clone());
225 }
226 let query = serde_urlencoded::to_string(&self.query).map_err(anyhow::Error::new)?;
227 let joiner = if self.url.contains('?') { '&' } else { '?' };
228 Ok(format!("{}{joiner}{query}", self.url))
229 }
230
231 pub async fn send(mut self) -> Result<Response> {
234 if let Some(err) = self.error.take() {
235 return Err(err);
236 }
237 let url = self.full_url()?;
238 let mut attempt = 0;
239 loop {
240 attempt += 1;
241 let outcome = match self.http.faked() {
242 Some(fake) => fake_send(&fake, &self, &url),
243 None => self.send_once(&url).await,
244 };
245 let retryable = match &outcome {
246 Ok(response) => {
247 response.status == StatusCode::TOO_MANY_REQUESTS
248 || response.status.is_server_error()
249 }
250 Err(_) => true,
251 };
252 if !retryable || attempt > self.retries {
253 return outcome;
254 }
255 tracing::debug!(url = %url, attempt, "HTTP request failed, retrying");
256 tokio::time::sleep(self.retry_delay * attempt).await;
257 }
258 }
259
260 #[cfg(feature = "http")]
261 async fn send_once(&self, url: &str) -> Result<Response> {
262 let mut request = client()
263 .request(self.method.clone(), url)
264 .timeout(self.timeout);
265 for (name, value) in &self.headers {
266 request = request.header(name, value);
267 }
268 if let Some((content_type, body)) = &self.body {
269 request = request
270 .header("content-type", content_type)
271 .body(body.clone());
272 }
273 let response = request
274 .send()
275 .await
276 .map_err(|err| anyhow::anyhow!("{} {url}: {}", self.method, err_chain(&err)))?;
277 let status = response.status();
278 let headers = response
279 .headers()
280 .iter()
281 .map(|(k, v)| {
282 (
283 k.as_str().to_owned(),
284 v.to_str().unwrap_or_default().to_owned(),
285 )
286 })
287 .collect();
288 let body = response
289 .bytes()
290 .await
291 .map_err(|err| anyhow::anyhow!("{} {url}: {}", self.method, err_chain(&err)))?;
292 Ok(Response {
293 status,
294 headers,
295 body: body.to_vec(),
296 url: url.to_owned(),
297 })
298 }
299
300 #[cfg(not(feature = "http"))]
301 async fn send_once(&self, url: &str) -> Result<Response> {
302 Err(anyhow::anyhow!(
303 "{} {url}: Renox was built without the `http` feature",
304 self.method
305 )
306 .into())
307 }
308}
309
310#[cfg(feature = "http")]
311fn err_chain(err: &dyn std::error::Error) -> String {
312 let mut text = err.to_string();
313 let mut source = err.source();
314 while let Some(cause) = source {
315 text.push_str(": ");
316 text.push_str(&cause.to_string());
317 source = cause.source();
318 }
319 text
320}
321
322#[cfg(feature = "http")]
324pub(crate) fn client() -> &'static reqwest::Client {
325 static CLIENT: std::sync::LazyLock<reqwest::Client> = std::sync::LazyLock::new(|| {
326 let _ = rustls::crypto::ring::default_provider().install_default();
329 reqwest::Client::builder()
330 .user_agent(concat!("renox/", env!("CARGO_PKG_VERSION")))
331 .build()
332 .unwrap_or_default()
333 });
334 &CLIENT
335}
336
337#[derive(Debug, Clone)]
339pub struct Response {
340 status: StatusCode,
341 headers: Vec<(String, String)>,
342 body: Vec<u8>,
343 url: String,
344}
345
346impl Response {
347 pub fn status(&self) -> StatusCode {
349 self.status
350 }
351
352 pub fn ok(&self) -> bool {
354 self.status.is_success()
355 }
356
357 pub fn header(&self, name: &str) -> Option<&str> {
359 self.headers
360 .iter()
361 .find(|(k, _)| k.eq_ignore_ascii_case(name))
362 .map(|(_, v)| v.as_str())
363 }
364
365 pub fn bytes(&self) -> &[u8] {
367 &self.body
368 }
369
370 pub fn text(&self) -> String {
372 String::from_utf8_lossy(&self.body).into_owned()
373 }
374
375 pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
377 serde_json::from_slice(&self.body).map_err(|err| {
378 anyhow::anyhow!(
379 "the response of {} is not the expected JSON: {err}",
380 self.url
381 )
382 .into()
383 })
384 }
385
386 pub fn error_for_status(self) -> Result<Self> {
389 if self.status.is_client_error() || self.status.is_server_error() {
390 let body = self.text();
391 let excerpt: String = body.chars().take(300).collect();
392 return Err(anyhow::anyhow!("{} answered {}: {excerpt}", self.url, self.status).into());
393 }
394 Ok(self)
395 }
396}
397
398#[derive(Debug, Clone)]
400#[non_exhaustive]
401pub struct SentRequest {
402 pub method: String,
404 pub url: String,
406 pub headers: Vec<(String, String)>,
408 pub body: String,
410}
411
412impl SentRequest {
413 pub fn header(&self, name: &str) -> Option<&str> {
415 self.headers
416 .iter()
417 .find(|(k, _)| k.eq_ignore_ascii_case(name))
418 .map(|(_, v)| v.as_str())
419 }
420
421 pub fn json(&self) -> serde_json::Value {
423 serde_json::from_str(&self.body).unwrap_or_default()
424 }
425}
426
427#[derive(Debug, Clone)]
429pub struct FakeResponse {
430 status: StatusCode,
431 headers: Vec<(String, String)>,
432 body: Vec<u8>,
433 fails: bool,
435}
436
437impl FakeResponse {
438 pub fn status(status: u16) -> Self {
440 Self {
441 status: StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
442 headers: Vec::new(),
443 body: Vec::new(),
444 fails: false,
445 }
446 }
447
448 pub fn json(status: u16, body: serde_json::Value) -> Self {
450 Self::status(status)
451 .header("content-type", "application/json")
452 .body(body.to_string())
453 }
454
455 pub fn text(status: u16, body: impl Into<String>) -> Self {
457 Self::status(status).body(body.into())
458 }
459
460 pub fn connection_error() -> Self {
462 Self {
463 fails: true,
464 ..Self::status(500)
465 }
466 }
467
468 pub fn header(mut self, name: &str, value: &str) -> Self {
470 self.headers.push((name.to_owned(), value.to_owned()));
471 self
472 }
473
474 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
476 self.body = body.into();
477 self
478 }
479}
480
481#[derive(Default)]
482struct FakeState {
483 routes: Vec<(String, Vec<FakeResponse>)>,
485 sent: Vec<SentRequest>,
486}
487
488#[derive(Clone)]
491pub struct FakeHttp(Arc<Mutex<FakeState>>);
492
493impl FakeHttp {
494 pub fn on(&self, pattern: &str, response: FakeResponse) -> &Self {
499 let mut state = self.0.lock().unwrap_or_else(|e| e.into_inner());
500 match state.routes.iter_mut().find(|(p, _)| p == pattern) {
501 Some((_, answers)) => answers.push(response),
502 None => state.routes.push((pattern.to_owned(), vec![response])),
503 }
504 self
505 }
506
507 pub fn sent(&self) -> Vec<SentRequest> {
509 self.0
510 .lock()
511 .unwrap_or_else(|e| e.into_inner())
512 .sent
513 .clone()
514 }
515
516 #[track_caller]
518 pub fn assert_sent(&self, check: impl Fn(&SentRequest) -> bool) {
519 let sent = self.sent();
520 assert!(
521 sent.iter().any(check),
522 "no matching HTTP request was sent; sent: {:#?}",
523 sent.iter()
524 .map(|r| format!("{} {}", r.method, r.url))
525 .collect::<Vec<_>>()
526 );
527 }
528
529 #[track_caller]
531 pub fn assert_not_sent(&self, check: impl Fn(&SentRequest) -> bool) {
532 assert!(
533 !self.sent().iter().any(check),
534 "a matching HTTP request was sent"
535 );
536 }
537
538 #[track_caller]
540 pub fn assert_sent_count(&self, expected: usize) {
541 assert_eq!(self.sent().len(), expected, "HTTP requests sent");
542 }
543}
544
545fn fake_send(fake: &Mutex<FakeState>, request: &Request, url: &str) -> Result<Response> {
546 let mut state = fake.lock().unwrap_or_else(|e| e.into_inner());
547 let mut headers = request.headers.clone();
548 if let Some((content_type, _)) = &request.body {
549 headers.push(("content-type".into(), content_type.clone()));
550 }
551 state.sent.push(SentRequest {
552 method: request.method.to_string(),
553 url: url.to_owned(),
554 headers,
555 body: request
556 .body
557 .as_ref()
558 .map(|(_, body)| String::from_utf8_lossy(body).into_owned())
559 .unwrap_or_default(),
560 });
561 let method = request.method.as_str();
562 let answers = state
563 .routes
564 .iter_mut()
565 .find(|(pattern, _)| matches(pattern, method, url))
566 .map(|(_, answers)| answers)
567 .ok_or_else(|| {
568 anyhow::anyhow!(
569 "no fake for {method} {url}: add one with `fake_http().on(\"{url}\", …)`"
570 )
571 })?;
572 let answer = if answers.len() > 1 {
573 answers.remove(0)
574 } else {
575 answers[0].clone()
576 };
577 if answer.fails {
578 return Err(anyhow::anyhow!("{method} {url}: connection refused (fake)").into());
579 }
580 Ok(Response {
581 status: answer.status,
582 headers: answer.headers,
583 body: answer.body,
584 url: url.to_owned(),
585 })
586}
587
588fn matches(pattern: &str, method: &str, url: &str) -> bool {
590 let (want_method, pattern) = match pattern.split_once(' ') {
591 Some((m, rest)) if m.chars().all(|c| c.is_ascii_uppercase()) => (Some(m), rest),
592 _ => (None, pattern),
593 };
594 if want_method.is_some_and(|m| m != method) {
595 return false;
596 }
597 glob(pattern.as_bytes(), url.as_bytes())
598}
599
600fn glob(pattern: &[u8], text: &[u8]) -> bool {
601 match pattern.split_first() {
602 None => text.is_empty(),
603 Some((b'*', rest)) => (0..=text.len()).any(|i| glob(rest, &text[i..])),
604 Some((c, rest)) => text.first() == Some(c) && glob(rest, &text[1..]),
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 #[cfg(feature = "http")]
613 #[test]
614 fn the_client_builds_with_the_ring_provider() {
615 let _ = client();
617 }
618
619 #[test]
620 fn patterns() {
621 assert!(matches("https://a.test/*", "GET", "https://a.test/x?y=1"));
622 assert!(matches("POST https://a.test/o", "POST", "https://a.test/o"));
623 assert!(!matches("POST https://a.test/o", "GET", "https://a.test/o"));
624 assert!(!matches("https://a.test/o", "GET", "https://a.test/o/1"));
625 assert!(matches("*", "DELETE", "https://b.test"));
626 assert!(matches("https://*.test/*/x", "GET", "https://b.test/1/2/x"));
627 }
628
629 #[tokio::test]
630 async fn fakes_answer_in_turn_and_retries_follow() {
631 let http = Http::default();
632 let fake = http.fake();
633 fake.on("POST https://a.test/pay", FakeResponse::connection_error())
634 .on("POST https://a.test/pay", FakeResponse::status(503))
635 .on(
636 "POST https://a.test/pay",
637 FakeResponse::json(201, serde_json::json!({"id": 7})),
638 );
639 let response = http
640 .post("https://a.test/pay")
641 .json(&serde_json::json!({"total": 5}))
642 .retry(2, Duration::ZERO)
643 .send()
644 .await
645 .unwrap();
646 assert_eq!(response.status(), StatusCode::CREATED);
647 assert_eq!(response.json::<serde_json::Value>().unwrap()["id"], 7);
648 fake.assert_sent_count(3);
649 assert_eq!(fake.sent()[0].json()["total"], 5);
650 assert_eq!(
651 fake.sent()[0].header("Content-Type"),
652 Some("application/json")
653 );
654
655 fake.on("https://a.test/flaky", FakeResponse::status(500));
657 let res = http.get("https://a.test/flaky").send().await.unwrap();
658 assert!(res.clone().error_for_status().is_err());
659 assert!(!res.ok());
660 assert!(
661 http.get("https://a.test/none").send().await.is_err(),
662 "no fake"
663 );
664 }
665
666 #[tokio::test]
667 async fn builds_urls_and_headers() {
668 let http = Http::default();
669 let fake = http.fake();
670 fake.on("*", FakeResponse::text(200, "ok"));
671 http.get("https://a.test/r?x=1")
672 .query(&[("q", "iced coffee"), ("n", "2")])
673 .bearer("t0k")
674 .send()
675 .await
676 .unwrap();
677 http.post("https://a.test/f")
678 .basic_auth("u", "p")
679 .form(&[("a", "1 2")])
680 .send()
681 .await
682 .unwrap();
683 let sent = fake.sent();
684 assert_eq!(sent[0].url, "https://a.test/r?x=1&q=iced+coffee&n=2");
685 assert_eq!(sent[0].header("authorization"), Some("Bearer t0k"));
686 assert_eq!(sent[1].header("authorization"), Some("Basic dTpw"));
687 assert_eq!(sent[1].body, "a=1+2");
688 fake.assert_sent(|r| r.method == "POST");
689 fake.assert_not_sent(|r| r.method == "DELETE");
690 }
691
692 #[tokio::test]
695 async fn put_patch_raw_bodies_and_response_helpers() {
696 let http = Http::default();
697 let fake = http.fake();
698 fake.on(
699 "PUT https://api.test/items/1",
700 FakeResponse::text(200, "not json").header("x-request-id", "r1"),
701 );
702 fake.on("PATCH https://api.test/items/1", FakeResponse::status(204));
703 let res = http
704 .put("https://api.test/items/1")
705 .body("text/csv", "a,b\n1,2")
706 .send()
707 .await
708 .unwrap();
709 assert_eq!(res.header("X-Request-Id"), Some("r1"));
710 assert_eq!(res.bytes(), b"not json");
711 let err = res.json::<serde_json::Value>().unwrap_err();
712 assert!(
713 format!("{err:?}")
714 .contains("the response of https://api.test/items/1 is not the expected JSON"),
715 "{err:?}"
716 );
717 http.patch("https://api.test/items/1").send().await.unwrap();
718 let sent = fake.sent();
719 assert_eq!(sent[0].method, "PUT");
720 assert_eq!(sent[0].body, "a,b\n1,2");
721 assert!(
722 sent[0]
723 .headers
724 .iter()
725 .any(|(k, v)| k == "content-type" && v == "text/csv")
726 );
727 assert_eq!(sent[1].method, "PATCH");
728 assert_eq!(format!("{http:?}"), "Http");
730 }
731
732 #[tokio::test]
733 async fn bodies_that_cant_be_encoded_fail_at_send() {
734 let http = Http::default();
735 let fake = http.fake();
736 let mut odd = std::collections::HashMap::new();
737 odd.insert((1, 2), 3);
738 assert!(
739 http.post("https://api.test/a")
740 .json(&odd)
741 .send()
742 .await
743 .is_err()
744 );
745 let nested = [("a", vec![1, 2])];
746 assert!(
747 http.post("https://api.test/b")
748 .form(&nested)
749 .send()
750 .await
751 .is_err()
752 );
753 assert!(fake.sent().is_empty(), "nothing was sent");
754 }
755
756 #[tokio::test]
757 #[should_panic(
758 expected = "no matching HTTP request was sent; sent: [\n \"GET https://api.test/x\",\n]"
759 )]
760 async fn assert_sent_lists_what_was_sent() {
761 let http = Http::default();
762 let fake = http.fake();
763 fake.on("https://api.test/*", FakeResponse::status(200));
764 http.get("https://api.test/x").send().await.unwrap();
765 fake.assert_sent(|r| r.method == "POST");
766 }
767}