Skip to main content

switchy_http/
simulator.rs

1//! Simulator HTTP client backend implementation.
2//!
3//! This module provides a no-op HTTP client backend that returns empty responses without making
4//! any network requests. It is useful for testing and development environments where you want to
5//! avoid real network calls.
6//!
7//! All requests succeed immediately and return empty/default responses:
8//!
9//! * Status: 200 OK
10//! * Headers: Empty
11//! * Body: Empty
12//!
13//! This module is only available when the `simulator` feature is enabled.
14//!
15//! # Usage
16//!
17//! Use the exported types from the parent crate (`switchy_http::Client`, etc.) rather than
18//! accessing this module directly. The parent crate automatically selects the appropriate
19//! backend based on enabled features.
20
21use std::{collections::BTreeMap, marker::PhantomData};
22
23use async_trait::async_trait;
24use bytes::Bytes;
25
26use crate::{
27    Error, GenericClient, GenericClientBuilder, GenericRequestBuilder, GenericResponse, Method,
28    StatusCode,
29};
30
31/// Simulator HTTP client.
32///
33/// This client provides a no-op implementation that doesn't make any real network requests.
34/// All requests succeed immediately and return empty responses with a 200 OK status.
35#[derive(Default)]
36pub struct Client;
37
38impl Client {
39    /// Create a new simulator HTTP client.
40    #[must_use]
41    pub const fn new() -> Self {
42        Self
43    }
44}
45
46impl GenericClient<crate::SimulatorRequestBuilder> for Client {
47    fn request(&self, _method: Method, _url: &str) -> crate::SimulatorRequestBuilder {
48        crate::RequestBuilderWrapper(RequestBuilder, PhantomData)
49    }
50}
51
52/// Builder for constructing a simulator HTTP client.
53///
54/// This builder always succeeds when building a client since the simulator
55/// requires no configuration or initialization.
56pub struct ClientBuilder;
57
58impl crate::SimulatorClientBuilder {
59    /// Create a new client builder for the simulator HTTP client.
60    #[must_use]
61    pub const fn new() -> Self {
62        Self(ClientBuilder, PhantomData, PhantomData)
63    }
64}
65
66impl GenericClientBuilder<crate::SimulatorRequestBuilder, crate::SimulatorClient>
67    for ClientBuilder
68{
69    fn build(self) -> Result<crate::SimulatorClient, Error> {
70        Ok(crate::ClientWrapper(Client, PhantomData))
71    }
72}
73
74/// Request builder for simulator HTTP client.
75///
76/// This builder ignores all configuration (headers, query parameters, body) and
77/// always returns an empty successful response when sent.
78pub struct RequestBuilder;
79
80#[async_trait]
81impl GenericRequestBuilder<crate::SimulatorResponse> for RequestBuilder {
82    fn header(&mut self, _name: &str, _value: &str) {}
83
84    fn query_param(&mut self, _name: &str, _value: &str) {}
85
86    fn query_param_opt(&mut self, _name: &str, _value: Option<&str>) {}
87
88    fn query_params(&mut self, _params: &[(&str, &str)]) {}
89
90    fn body(&mut self, _body: Bytes) {}
91
92    #[cfg(feature = "json")]
93    fn form(&mut self, _form: &serde_json::Value) {}
94
95    async fn send(&mut self) -> Result<crate::SimulatorResponse, Error> {
96        Ok(crate::ResponseWrapper(Response::default()))
97    }
98}
99
100/// HTTP response from simulator client.
101///
102/// This response always returns a 200 OK status with empty headers and body.
103#[derive(Default)]
104pub struct Response {
105    headers: BTreeMap<String, String>,
106}
107
108#[async_trait]
109impl GenericResponse for Response {
110    fn status(&self) -> StatusCode {
111        StatusCode::Ok
112    }
113
114    fn headers(&mut self) -> &BTreeMap<String, String> {
115        &self.headers
116    }
117
118    async fn text(&mut self) -> Result<String, Error> {
119        Ok(String::new())
120    }
121
122    async fn bytes(&mut self) -> Result<Bytes, Error> {
123        Ok(Bytes::new())
124    }
125
126    #[cfg(feature = "stream")]
127    fn bytes_stream(
128        &mut self,
129    ) -> std::pin::Pin<Box<dyn futures_core::Stream<Item = Result<Bytes, Error>> + Send>> {
130        Box::pin(futures_util::stream::empty())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test_log::test]
139    fn test_simulator_client_builder_succeeds() {
140        let builder = ClientBuilder;
141        let result =
142            GenericClientBuilder::<crate::SimulatorRequestBuilder, crate::SimulatorClient>::build(
143                builder,
144            );
145        assert!(result.is_ok());
146    }
147
148    #[test_log::test]
149    fn test_simulator_response_returns_ok_status() {
150        let response = Response::default();
151        assert_eq!(response.status(), StatusCode::Ok);
152    }
153
154    #[test_log::test]
155    fn test_simulator_response_returns_empty_headers() {
156        let mut response = Response::default();
157        let headers = response.headers();
158        assert!(headers.is_empty());
159    }
160
161    #[test_log::test]
162    fn test_simulator_client_creates_request_builder() {
163        let client = Client::new();
164        let _builder = client.request(Method::Get, "http://example.com");
165        // If we get here without panic, the test passes
166    }
167
168    /// Test the complete request/response flow through the macro-generated HTTP client.
169    /// This exercises the full integration: client creation, request builder configuration,
170    /// sending requests, and consuming responses.
171    #[test_log::test(switchy_async::test)]
172    async fn test_simulator_full_request_response_flow() {
173        let client = crate::SimulatorClient::new();
174
175        // Build and send a request with multiple configurations
176        let response = client
177            .get("http://example.com/test")
178            .header("Authorization", "Bearer token")
179            .query_param("key", "value")
180            .query_param_opt("optional", Some("present"))
181            .query_param_opt("missing", None)
182            .query_params(&[("page", "1"), ("limit", "10")])
183            .send()
184            .await
185            .unwrap();
186
187        // Verify response properties
188        assert_eq!(response.status(), StatusCode::Ok);
189
190        // Verify response body consumption works
191        let text = response.text().await.unwrap();
192        assert!(text.is_empty());
193    }
194
195    /// Test that all HTTP method convenience methods are correctly wired through the macro.
196    #[test_log::test(switchy_async::test)]
197    async fn test_simulator_all_http_methods() {
198        let client = crate::SimulatorClient::new();
199
200        // Test all HTTP method convenience methods work through the macro-generated client
201        let get = client.get("http://example.com").send().await.unwrap();
202        assert_eq!(get.status(), StatusCode::Ok);
203
204        let post = client.post("http://example.com").send().await.unwrap();
205        assert_eq!(post.status(), StatusCode::Ok);
206
207        let put = client.put("http://example.com").send().await.unwrap();
208        assert_eq!(put.status(), StatusCode::Ok);
209
210        let patch = client.patch("http://example.com").send().await.unwrap();
211        assert_eq!(patch.status(), StatusCode::Ok);
212
213        let delete = client.delete("http://example.com").send().await.unwrap();
214        assert_eq!(delete.status(), StatusCode::Ok);
215
216        let head = client.head("http://example.com").send().await.unwrap();
217        assert_eq!(head.status(), StatusCode::Ok);
218
219        let options = client.options("http://example.com").send().await.unwrap();
220        assert_eq!(options.status(), StatusCode::Ok);
221    }
222
223    /// Test that the JSON serialization path works correctly through the macro-generated client.
224    /// This exercises the `json()` method which serializes via `serde_json`.
225    #[cfg(feature = "json")]
226    #[test_log::test(switchy_async::test)]
227    async fn test_simulator_json_body_serialization() {
228        {
229            #[derive(serde::Serialize)]
230            struct TestPayload {
231                name: String,
232                value: i32,
233            }
234
235            let client = crate::SimulatorClient::new();
236
237            let payload = TestPayload {
238                name: "test".to_string(),
239                value: 42,
240            };
241
242            // Verify the JSON serialization path doesn't panic
243            let response = client
244                .post("http://example.com/api")
245                .json(&payload)
246                .send()
247                .await
248                .unwrap();
249
250            assert_eq!(response.status(), StatusCode::Ok);
251        }
252    }
253
254    /// Test that the form serialization path works correctly through the macro-generated client.
255    /// This exercises the `form()` method which serializes via `serde_json::to_value`.
256    #[cfg(feature = "json")]
257    #[test_log::test(switchy_async::test)]
258    async fn test_simulator_form_body_serialization() {
259        {
260            #[derive(serde::Serialize)]
261            struct FormData {
262                username: String,
263                password: String,
264            }
265
266            let client = crate::SimulatorClient::new();
267
268            let form = FormData {
269                username: "user".to_string(),
270                password: "pass".to_string(),
271            };
272
273            // Verify the form serialization path doesn't panic
274            let response = client
275                .post("http://example.com/login")
276                .form(&form)
277                .send()
278                .await
279                .unwrap();
280
281            assert_eq!(response.status(), StatusCode::Ok);
282        }
283    }
284
285    /// Test the `bytes_stream` response method works correctly through the macro-generated response.
286    #[cfg(feature = "stream")]
287    #[test_log::test(switchy_async::test)]
288    async fn test_simulator_bytes_stream_consumption() {
289        {
290            use futures_util::StreamExt;
291
292            let client = crate::SimulatorClient::new();
293            let response = client.get("http://example.com").send().await.unwrap();
294
295            // Test consuming the response as a stream
296            let mut stream = response.bytes_stream();
297            let chunks: Vec<_> = stream.by_ref().collect().await;
298            assert!(chunks.is_empty());
299        }
300    }
301
302    /// Test that `Response::bytes()` returns empty bytes for simulator.
303    /// This verifies the `bytes()` code path in `GenericResponse` impl is working.
304    #[test_log::test(switchy_async::test)]
305    async fn test_simulator_response_bytes() {
306        {
307            use crate::GenericResponse;
308
309            let mut response = Response::default();
310            let bytes = response.bytes().await.unwrap();
311            assert!(bytes.is_empty());
312        }
313    }
314
315    /// Test that `Response::text()` returns empty string for simulator.
316    /// This verifies the `text()` code path in `GenericResponse` impl is working.
317    #[test_log::test(switchy_async::test)]
318    async fn test_simulator_response_text() {
319        {
320            use crate::GenericResponse;
321
322            let mut response = Response::default();
323            let text = response.text().await.unwrap();
324            assert!(text.is_empty());
325        }
326    }
327
328    /// Test that the underlying `RequestBuilder::body()` method accepts raw bytes.
329    /// This exercises the body path separately from the JSON serialization path.
330    #[test_log::test(switchy_async::test)]
331    async fn test_simulator_request_raw_body() {
332        let client = crate::SimulatorClient::new();
333
334        let body_bytes = Bytes::from_static(b"raw request body content");
335
336        let response = client
337            .post("http://example.com/upload")
338            .body(body_bytes)
339            .send()
340            .await
341            .unwrap();
342
343        assert_eq!(response.status(), StatusCode::Ok);
344    }
345
346    /// Test that `SimulatorClient::default()` produces a working client.
347    /// This exercises the Default impl generated by the macro.
348    #[test_log::test(switchy_async::test)]
349    async fn test_simulator_client_default() {
350        let client = crate::SimulatorClient::default();
351
352        let response = client.get("http://example.com").send().await.unwrap();
353        assert_eq!(response.status(), StatusCode::Ok);
354    }
355
356    /// Test that headers can be retrieved through the macro-generated wrapper method.
357    #[test_log::test(switchy_async::test)]
358    async fn test_simulator_response_headers_through_wrapper() {
359        let client = crate::SimulatorClient::new();
360
361        let mut response = client.get("http://example.com").send().await.unwrap();
362
363        // Access headers through the wrapper's headers() method
364        let headers = response.headers();
365        assert!(headers.is_empty());
366    }
367
368    /// Test the `bytes_stream` on the `GenericResponse` trait impl directly.
369    #[cfg(feature = "stream")]
370    #[test_log::test(switchy_async::test)]
371    async fn test_simulator_response_bytes_stream_trait() {
372        {
373            use crate::GenericResponse;
374            use futures_util::StreamExt;
375
376            let mut response = Response::default();
377
378            let mut stream = response.bytes_stream();
379            let chunks: Vec<_> = stream.by_ref().collect().await;
380            assert!(chunks.is_empty());
381        }
382    }
383
384    /// Test the `Client::request()` method with various HTTP methods.
385    /// This verifies the method parameter is correctly passed through.
386    #[test_log::test(switchy_async::test)]
387    async fn test_simulator_client_request_with_different_methods() {
388        let client = Client::new();
389
390        // Test that request() works with different method types
391        let methods = [
392            Method::Get,
393            Method::Post,
394            Method::Put,
395            Method::Patch,
396            Method::Delete,
397            Method::Head,
398            Method::Options,
399        ];
400
401        for method in methods {
402            let _builder = client.request(method, "http://example.com");
403            // If we reach here without panic, the test passes
404        }
405    }
406
407    /// Test JSON deserialization of response body.
408    /// This exercises the `json()` method on the Response wrapper.
409    #[cfg(feature = "json")]
410    #[test_log::test(switchy_async::test)]
411    async fn test_simulator_response_json_deserialization_empty() {
412        let client = crate::SimulatorClient::new();
413
414        let response = client.get("http://example.com/api").send().await.unwrap();
415
416        // The simulator returns empty bytes, so deserializing any non-empty type will fail.
417        // But deserializing to an empty structure or Option should work with appropriate JSON.
418        // Since simulator returns empty bytes (not valid JSON), this should fail.
419        let result: Result<serde_json::Value, _> = response.json().await;
420        assert!(result.is_err());
421    }
422}