1use std::{
4 future::{self, Future},
5 sync::{Arc, Mutex, MutexGuard},
6};
7
8use bytes::Bytes;
9
10use crate::{
11 http_client::{
12 self, HttpClientExt, LazyBody, MultipartForm, Request, Response, StreamingResponse,
13 },
14 wasm_compat::WasmCompatSend,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct CapturedHttpRequest {
20 pub uri: String,
22 pub headers: http::HeaderMap,
24 pub body: Bytes,
26}
27
28#[derive(Clone, Debug)]
30pub enum MockHttpResponse {
31 Success(Bytes),
33 Error(http::StatusCode, String),
35 ErrorResponse(http::StatusCode, Bytes),
38}
39
40impl MockHttpResponse {
41 pub fn success(body: impl Into<Bytes>) -> Self {
43 Self::Success(body.into())
44 }
45
46 pub fn error(status: http::StatusCode, message: impl Into<String>) -> Self {
48 Self::Error(status, message.into())
49 }
50}
51
52impl Default for MockHttpResponse {
53 fn default() -> Self {
54 Self::Success(Bytes::new())
55 }
56}
57
58#[derive(Clone, Debug, Default)]
61pub struct RecordingHttpClient {
62 requests: Arc<Mutex<Vec<CapturedHttpRequest>>>,
63 response: Arc<Mutex<MockHttpResponse>>,
64}
65
66impl RecordingHttpClient {
67 pub fn new(response_body: impl Into<Bytes>) -> Self {
69 Self {
70 requests: Arc::new(Mutex::new(Vec::new())),
71 response: Arc::new(Mutex::new(MockHttpResponse::success(response_body))),
72 }
73 }
74
75 pub fn with_error(status: http::StatusCode, message: impl Into<String>) -> Self {
77 Self {
78 requests: Arc::new(Mutex::new(Vec::new())),
79 response: Arc::new(Mutex::new(MockHttpResponse::error(status, message))),
80 }
81 }
82
83 pub fn with_error_response(status: http::StatusCode, body: impl Into<Bytes>) -> Self {
86 Self {
87 requests: Arc::new(Mutex::new(Vec::new())),
88 response: Arc::new(Mutex::new(MockHttpResponse::ErrorResponse(
89 status,
90 body.into(),
91 ))),
92 }
93 }
94
95 pub fn requests(&self) -> Vec<CapturedHttpRequest> {
97 self.requests_guard().clone()
98 }
99
100 pub fn set_response(&self, response: MockHttpResponse) {
102 *self.response_guard() = response;
103 }
104
105 fn requests_guard(&self) -> MutexGuard<'_, Vec<CapturedHttpRequest>> {
106 match self.requests.lock() {
107 Ok(guard) => guard,
108 Err(poisoned) => poisoned.into_inner(),
109 }
110 }
111
112 fn response_guard(&self) -> MutexGuard<'_, MockHttpResponse> {
113 match self.response.lock() {
114 Ok(guard) => guard,
115 Err(poisoned) => poisoned.into_inner(),
116 }
117 }
118
119 fn record_request(&self, uri: String, headers: http::HeaderMap, body: Bytes) {
120 self.requests_guard()
121 .push(CapturedHttpRequest { uri, headers, body });
122 }
123
124 fn build_unary_response<U>(
125 response: MockHttpResponse,
126 ) -> http_client::Result<Response<LazyBody<U>>>
127 where
128 U: From<Bytes> + WasmCompatSend + 'static,
129 {
130 let (status, response_body) = match response {
131 MockHttpResponse::Success(response_body) => (http::StatusCode::OK, response_body),
132 MockHttpResponse::Error(status, message) => {
133 return Err(http_client::Error::InvalidStatusCodeWithMessage(
134 status, message,
135 ));
136 }
137 MockHttpResponse::ErrorResponse(status, response_body) => (status, response_body),
138 };
139 let body: LazyBody<U> = Box::pin(async move { Ok(U::from(response_body)) });
140 Response::builder()
141 .status(status)
142 .body(body)
143 .map_err(http_client::Error::Protocol)
144 }
145}
146
147impl HttpClientExt for RecordingHttpClient {
148 fn send<T, U>(
149 &self,
150 req: Request<T>,
151 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
152 where
153 T: Into<Bytes> + WasmCompatSend,
154 U: From<Bytes> + WasmCompatSend + 'static,
155 {
156 let response = self.response_guard().clone();
157 let (parts, body) = req.into_parts();
158 self.record_request(parts.uri.to_string(), parts.headers, body.into());
159
160 async move { Self::build_unary_response(response) }
161 }
162
163 fn send_multipart<U>(
164 &self,
165 req: Request<MultipartForm>,
166 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
167 where
168 U: From<Bytes> + WasmCompatSend + 'static,
169 {
170 let response = self.response_guard().clone();
171 let (parts, _body) = req.into_parts();
172 self.record_request(parts.uri.to_string(), parts.headers, Bytes::new());
173
174 async move { Self::build_unary_response(response) }
175 }
176
177 fn send_streaming<T>(
178 &self,
179 _req: Request<T>,
180 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
181 where
182 T: Into<Bytes> + WasmCompatSend,
183 {
184 future::ready(Err(http_client::Error::InvalidStatusCode(
185 http::StatusCode::NOT_IMPLEMENTED,
186 )))
187 }
188}
189
190#[derive(Clone, Debug, Default)]
194pub struct MockStreamingClient {
195 pub sse_bytes: Bytes,
197}
198
199impl HttpClientExt for MockStreamingClient {
200 fn send<T, U>(
201 &self,
202 _req: Request<T>,
203 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
204 where
205 T: Into<Bytes> + WasmCompatSend,
206 U: From<Bytes> + WasmCompatSend + 'static,
207 {
208 future::ready(Err(http_client::Error::InvalidStatusCode(
209 http::StatusCode::NOT_IMPLEMENTED,
210 )))
211 }
212
213 fn send_multipart<U>(
214 &self,
215 _req: Request<MultipartForm>,
216 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
217 where
218 U: From<Bytes> + WasmCompatSend + 'static,
219 {
220 future::ready(Err(http_client::Error::InvalidStatusCode(
221 http::StatusCode::NOT_IMPLEMENTED,
222 )))
223 }
224
225 fn send_streaming<T>(
226 &self,
227 _req: Request<T>,
228 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
229 where
230 T: Into<Bytes> + WasmCompatSend,
231 {
232 let sse_bytes = self.sse_bytes.clone();
233 async move {
234 let byte_stream =
235 futures::stream::iter(vec![Ok::<Bytes, http_client::Error>(sse_bytes)]);
236 let boxed_stream: http_client::sse::BoxedStream = Box::pin(byte_stream);
237
238 Response::builder()
239 .status(http::StatusCode::OK)
240 .header(http::header::CONTENT_TYPE, "text/event-stream")
241 .body(boxed_stream)
242 .map_err(http_client::Error::Protocol)
243 }
244 }
245}
246
247#[derive(Debug, Clone)]
250pub struct HttpErrorStreamingClient {
251 pub status: http::StatusCode,
252 pub body: String,
253}
254
255impl HttpErrorStreamingClient {
256 pub fn new(status: http::StatusCode, body: impl Into<String>) -> Self {
258 Self {
259 status,
260 body: body.into(),
261 }
262 }
263}
264
265impl Default for HttpErrorStreamingClient {
266 fn default() -> Self {
269 Self::new(http::StatusCode::INTERNAL_SERVER_ERROR, String::new())
270 }
271}
272
273impl HttpClientExt for HttpErrorStreamingClient {
274 fn send<T, U>(
275 &self,
276 _req: Request<T>,
277 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
278 where
279 T: Into<Bytes> + WasmCompatSend,
280 U: From<Bytes> + WasmCompatSend + 'static,
281 {
282 future::ready(Err(http_client::Error::InvalidStatusCode(
283 http::StatusCode::NOT_IMPLEMENTED,
284 )))
285 }
286
287 fn send_multipart<U>(
288 &self,
289 _req: Request<MultipartForm>,
290 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
291 where
292 U: From<Bytes> + WasmCompatSend + 'static,
293 {
294 future::ready(Err(http_client::Error::InvalidStatusCode(
295 http::StatusCode::NOT_IMPLEMENTED,
296 )))
297 }
298
299 fn send_streaming<T>(
300 &self,
301 _req: Request<T>,
302 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
303 where
304 T: Into<Bytes> + WasmCompatSend,
305 {
306 let status = self.status;
307 let body = self.body.clone();
308 async move {
309 Err(http_client::Error::InvalidStatusCodeWithMessage(
310 status, body,
311 ))
312 }
313 }
314}
315
316#[derive(Debug, Clone, Default)]
319pub struct SequencedStreamingHttpClient {
320 chunks: Arc<Mutex<Option<Vec<http_client::Result<Bytes>>>>>,
321}
322
323impl SequencedStreamingHttpClient {
324 pub fn new(chunks: Vec<http_client::Result<Bytes>>) -> Self {
326 Self {
327 chunks: Arc::new(Mutex::new(Some(chunks))),
328 }
329 }
330}
331
332impl HttpClientExt for SequencedStreamingHttpClient {
333 fn send<T, U>(
334 &self,
335 _req: Request<T>,
336 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
337 where
338 T: Into<Bytes> + WasmCompatSend,
339 U: From<Bytes> + WasmCompatSend + 'static,
340 {
341 future::ready(Err(http_client::Error::InvalidStatusCode(
342 http::StatusCode::NOT_IMPLEMENTED,
343 )))
344 }
345
346 fn send_multipart<U>(
347 &self,
348 _req: Request<MultipartForm>,
349 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
350 where
351 U: From<Bytes> + WasmCompatSend + 'static,
352 {
353 future::ready(Err(http_client::Error::InvalidStatusCode(
354 http::StatusCode::NOT_IMPLEMENTED,
355 )))
356 }
357
358 fn send_streaming<T>(
359 &self,
360 _req: Request<T>,
361 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
362 where
363 T: Into<Bytes> + WasmCompatSend,
364 {
365 let chunks = match self.chunks.lock() {
366 Ok(mut guard) => guard.take(),
367 Err(poisoned) => poisoned.into_inner().take(),
368 };
369
370 async move {
371 let Some(chunks) = chunks else {
372 return Err(http_client::Error::InvalidStatusCodeWithMessage(
373 http::StatusCode::INTERNAL_SERVER_ERROR,
374 "streaming chunks should only be consumed once".to_string(),
375 ));
376 };
377
378 let byte_stream = futures::stream::iter(chunks);
379 let boxed_stream: http_client::sse::BoxedStream = Box::pin(byte_stream);
380
381 Response::builder()
382 .status(http::StatusCode::OK)
383 .header(http::header::CONTENT_TYPE, "text/event-stream")
384 .body(boxed_stream)
385 .map_err(http_client::Error::Protocol)
386 }
387 }
388}