Skip to main content

mcp_servers/testing/
web_fetch.rs

1use futures::future::BoxFuture;
2use std::collections::HashMap;
3use std::sync::{Arc, Mutex};
4use std::time::Duration;
5
6use crate::coding::error::WebFetchError;
7use crate::coding::tools::web_fetch::{HttpClient, HttpResponse};
8
9/// In-memory [`HttpClient`] fake that scripts responses per URL.
10///
11/// Requests to URLs without a scripted result fail with
12/// [`WebFetchError::RequestFailed`].
13#[derive(Debug, Clone, Default)]
14pub struct FakeHttpClient {
15    responses: Arc<Mutex<HashMap<String, Result<HttpResponse, WebFetchError>>>>,
16}
17
18impl FakeHttpClient {
19    /// Scripts a full response for `url`, including redirects via `final_url`.
20    pub fn with_response(self, url: &str, response: HttpResponse) -> Self {
21        self.with_result(url, Ok(response))
22    }
23
24    /// Scripts a 200 response whose body is `html` and whose `final_url` is `url`.
25    pub fn with_html(self, url: &str, html: &str) -> Self {
26        self.with_response(
27            url,
28            HttpResponse {
29                final_url: url.to_string(),
30                status_code: 200,
31                body: html.to_string(),
32                content_type: Some("text/html".to_string()),
33            },
34        )
35    }
36
37    /// Scripts a [`WebFetchError::Timeout`] carrying `timeout_ms` for `url`.
38    pub fn with_timeout(self, url: &str, timeout_ms: u64) -> Self {
39        self.with_result(url, Err(WebFetchError::Timeout(timeout_ms)))
40    }
41
42    /// Scripts a [`WebFetchError::RequestFailed`] carrying `message` for `url`.
43    pub fn with_request_failed(self, url: &str, message: &str) -> Self {
44        self.with_result(url, Err(WebFetchError::RequestFailed(message.to_string())))
45    }
46
47    fn with_result(self, url: &str, result: Result<HttpResponse, WebFetchError>) -> Self {
48        self.responses.lock().unwrap().insert(url.to_string(), result);
49        self
50    }
51}
52
53impl HttpClient for FakeHttpClient {
54    fn fetch<'a>(&'a self, url: &'a str, _timeout: Duration) -> BoxFuture<'a, Result<HttpResponse, WebFetchError>> {
55        let response = self.responses.lock().unwrap().get(url).cloned();
56        Box::pin(std::future::ready(response.unwrap_or_else(|| {
57            Err(WebFetchError::RequestFailed(format!("No fake response configured for URL: {url}")))
58        })))
59    }
60}