1use std::{
5 collections::VecDeque,
6 future::{self, Future},
7 sync::{Arc, Mutex, MutexGuard, PoisonError},
8};
9
10use bytes::Bytes;
11
12use crate::{
13 http_client::{
14 self, HttpClientExt, LazyBody, MultipartForm, Request, Response, StreamingResponse,
15 },
16 wasm_compat::WasmCompatSend,
17};
18
19fn not_implemented() -> http_client::Error {
22 http_client::Error::non_success_with_details(
23 http::StatusCode::NOT_IMPLEMENTED,
24 http::HeaderMap::new(),
25 String::new(),
26 )
27}
28
29fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
31 mutex.lock().unwrap_or_else(PoisonError::into_inner)
32}
33
34macro_rules! no_unary_surface {
40 () => {
41 fn send<T, U>(
42 &self,
43 _req: Request<T>,
44 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>>
45 + WasmCompatSend
46 + 'static
47 where
48 T: Into<Bytes> + WasmCompatSend,
49 U: From<Bytes> + WasmCompatSend + 'static,
50 {
51 future::ready(Err(not_implemented()))
52 }
53
54 fn send_multipart<U>(
55 &self,
56 _req: Request<MultipartForm>,
57 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>>
58 + WasmCompatSend
59 + 'static
60 where
61 U: From<Bytes> + WasmCompatSend + 'static,
62 {
63 future::ready(Err(not_implemented()))
64 }
65 };
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct CapturedHttpRequest {
71 pub uri: String,
73 pub headers: http::HeaderMap,
75 pub body: Bytes,
77}
78
79#[derive(Clone, Debug)]
81pub enum MockHttpResponse {
82 Success(Bytes),
84 SuccessWithHeaders(Bytes, http::HeaderMap),
87 ErrorResponse(http::StatusCode, Bytes),
90 ErrorWithHeaders(http::StatusCode, String, http::HeaderMap),
93 ErrorResponseWithHeaders(http::StatusCode, Bytes, http::HeaderMap),
96}
97
98impl MockHttpResponse {
99 pub fn success(body: impl Into<Bytes>) -> Self {
101 Self::Success(body.into())
102 }
103
104 pub fn success_typed(body: impl Into<Bytes>, content_type: &'static str) -> Self {
106 let mut headers = http::HeaderMap::new();
107 headers.insert(
108 http::header::CONTENT_TYPE,
109 http::HeaderValue::from_static(content_type),
110 );
111 Self::SuccessWithHeaders(body.into(), headers)
112 }
113
114 pub fn error(status: http::StatusCode, message: impl Into<String>) -> Self {
118 Self::error_with_headers(status, message, http::HeaderMap::new())
119 }
120
121 pub fn error_with_headers(
124 status: http::StatusCode,
125 message: impl Into<String>,
126 headers: http::HeaderMap,
127 ) -> Self {
128 Self::ErrorWithHeaders(status, message.into(), headers)
129 }
130}
131
132impl MockHttpResponse {
133 fn into_parts(self) -> http_client::Result<(http::StatusCode, Bytes, Option<http::HeaderMap>)> {
136 Ok(match self {
137 Self::Success(body) => (http::StatusCode::OK, body, None),
138 Self::SuccessWithHeaders(body, headers) => (http::StatusCode::OK, body, Some(headers)),
139 Self::ErrorWithHeaders(status, body, headers) => {
140 return Err(http_client::Error::InvalidStatusCodeWithDetails {
141 status,
142 body,
143 headers,
144 });
145 }
146 Self::ErrorResponse(status, body) => (status, body, None),
147 Self::ErrorResponseWithHeaders(status, body, headers) => (status, body, Some(headers)),
148 })
149 }
150}
151
152impl Default for MockHttpResponse {
153 fn default() -> Self {
154 Self::Success(Bytes::new())
155 }
156}
157
158#[derive(Clone, Debug, Default)]
161pub struct RecordingHttpClient {
162 requests: Arc<Mutex<Vec<CapturedHttpRequest>>>,
163 response: Arc<Mutex<MockHttpResponse>>,
164}
165
166impl RecordingHttpClient {
167 fn scripted(response: MockHttpResponse) -> Self {
169 Self {
170 requests: Arc::default(),
171 response: Arc::new(Mutex::new(response)),
172 }
173 }
174
175 pub fn new(response_body: impl Into<Bytes>) -> Self {
177 Self::scripted(MockHttpResponse::success(response_body))
178 }
179
180 pub fn with_error(status: http::StatusCode, message: impl Into<String>) -> Self {
182 Self::scripted(MockHttpResponse::error(status, message))
183 }
184
185 pub fn with_error_response(status: http::StatusCode, body: impl Into<Bytes>) -> Self {
188 Self::scripted(MockHttpResponse::ErrorResponse(status, body.into()))
189 }
190
191 pub fn with_error_headers(
195 status: http::StatusCode,
196 message: impl Into<String>,
197 headers: http::HeaderMap,
198 ) -> Self {
199 Self::scripted(MockHttpResponse::error_with_headers(
200 status, message, headers,
201 ))
202 }
203
204 pub fn with_error_response_headers(
207 status: http::StatusCode,
208 body: impl Into<Bytes>,
209 headers: http::HeaderMap,
210 ) -> Self {
211 Self::scripted(MockHttpResponse::ErrorResponseWithHeaders(
212 status,
213 body.into(),
214 headers,
215 ))
216 }
217
218 pub fn requests(&self) -> Vec<CapturedHttpRequest> {
220 lock(&self.requests).clone()
221 }
222
223 fn record_request(&self, uri: String, headers: http::HeaderMap, body: Bytes) {
224 lock(&self.requests).push(CapturedHttpRequest { uri, headers, body });
225 }
226
227 fn build_unary_response<U>(
228 response: MockHttpResponse,
229 ) -> http_client::Result<Response<LazyBody<U>>>
230 where
231 U: From<Bytes> + WasmCompatSend + 'static,
232 {
233 let (status, response_body, response_headers) = response.into_parts()?;
234 let body: LazyBody<U> = Box::pin(async move { Ok(U::from(response_body)) });
235 let mut builder = Response::builder().status(status);
236 if let Some(headers) = response_headers
237 && let Some(slot) = builder.headers_mut()
238 {
239 *slot = headers;
240 }
241 builder.body(body).map_err(http_client::Error::Protocol)
242 }
243}
244
245impl HttpClientExt for RecordingHttpClient {
246 fn send<T, U>(
247 &self,
248 req: Request<T>,
249 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
250 where
251 T: Into<Bytes> + WasmCompatSend,
252 U: From<Bytes> + WasmCompatSend + 'static,
253 {
254 let response = lock(&self.response).clone();
255 let (parts, body) = req.into_parts();
256 self.record_request(parts.uri.to_string(), parts.headers, body.into());
257
258 async move { Self::build_unary_response(response) }
259 }
260
261 fn send_multipart<U>(
262 &self,
263 req: Request<MultipartForm>,
264 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
265 where
266 U: From<Bytes> + WasmCompatSend + 'static,
267 {
268 let response = lock(&self.response).clone();
269 let (parts, body) = req.into_parts();
270 let (_, body) = body.boundary("recording-http-client").encode();
271 self.record_request(parts.uri.to_string(), parts.headers, body);
272
273 async move { Self::build_unary_response(response) }
274 }
275
276 fn send_streaming<T>(
277 &self,
278 _req: Request<T>,
279 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
280 where
281 T: Into<Bytes> + WasmCompatSend,
282 {
283 future::ready(Err(not_implemented()))
284 }
285}
286
287#[derive(Clone, Debug, Default)]
294pub struct SequencedHttpClient {
295 requests: Arc<Mutex<Vec<CapturedHttpRequest>>>,
296 responses: Arc<Mutex<VecDeque<MockHttpResponse>>>,
297}
298
299impl SequencedHttpClient {
300 pub fn new(responses: impl IntoIterator<Item = MockHttpResponse>) -> Self {
302 Self {
303 requests: Arc::new(Mutex::new(Vec::new())),
304 responses: Arc::new(Mutex::new(responses.into_iter().collect())),
305 }
306 }
307
308 pub fn requests(&self) -> Vec<CapturedHttpRequest> {
310 lock(&self.requests).clone()
311 }
312
313 pub fn remaining_responses(&self) -> usize {
315 lock(&self.responses).len()
316 }
317
318 fn answer(&self, parts: http::request::Parts, body: Bytes) -> Option<MockHttpResponse> {
321 lock(&self.requests).push(CapturedHttpRequest {
322 uri: parts.uri.to_string(),
323 headers: parts.headers,
324 body,
325 });
326 lock(&self.responses).pop_front()
327 }
328}
329
330impl HttpClientExt for SequencedHttpClient {
331 fn send<T, U>(
332 &self,
333 req: Request<T>,
334 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
335 where
336 T: Into<Bytes> + WasmCompatSend,
337 U: From<Bytes> + WasmCompatSend + 'static,
338 {
339 let (parts, body) = req.into_parts();
340 let response = self.answer(parts, body.into());
341 async move { RecordingHttpClient::build_unary_response(response.ok_or_else(not_implemented)?) }
342 }
343
344 fn send_multipart<U>(
345 &self,
346 req: Request<MultipartForm>,
347 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
348 where
349 U: From<Bytes> + WasmCompatSend + 'static,
350 {
351 let (parts, _body) = req.into_parts();
352 let response = self.answer(parts, Bytes::new());
353 async move { RecordingHttpClient::build_unary_response(response.ok_or_else(not_implemented)?) }
354 }
355
356 fn send_streaming<T>(
357 &self,
358 req: Request<T>,
359 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
360 where
361 T: Into<Bytes> + WasmCompatSend,
362 {
363 let (parts, body) = req.into_parts();
364 let response = self.answer(parts, body.into());
365 future::ready(
366 response
367 .ok_or_else(not_implemented)
368 .and_then(streaming_response),
369 )
370 }
371}
372
373fn streaming_response(response: MockHttpResponse) -> http_client::Result<StreamingResponse> {
375 let (status, body, headers) = response.into_parts()?;
376 let chunks: http_client::BoxedStream = Box::pin(futures::stream::iter([Ok::<
377 Bytes,
378 http_client::Error,
379 >(body)]));
380 let mut response = Response::builder()
381 .status(status)
382 .body(chunks)
383 .map_err(http_client::Error::Protocol)?;
384 if let Some(headers) = headers {
385 *response.headers_mut() = headers;
386 }
387 Ok(response)
388}
389
390#[derive(Clone, Debug, Default)]
394pub struct MockStreamingClient {
395 pub sse_bytes: Bytes,
397}
398
399impl HttpClientExt for MockStreamingClient {
400 no_unary_surface!();
401
402 fn send_streaming<T>(
403 &self,
404 _req: Request<T>,
405 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
406 where
407 T: Into<Bytes> + WasmCompatSend,
408 {
409 let sse_bytes = self.sse_bytes.clone();
410 async move {
411 let byte_stream =
412 futures::stream::iter(vec![Ok::<Bytes, http_client::Error>(sse_bytes)]);
413 let boxed_stream: http_client::BoxedStream = Box::pin(byte_stream);
414
415 Response::builder()
416 .status(http::StatusCode::OK)
417 .header(http::header::CONTENT_TYPE, "text/event-stream")
418 .body(boxed_stream)
419 .map_err(http_client::Error::Protocol)
420 }
421 }
422}
423
424#[derive(Clone, Debug, Default)]
427pub struct SilentStreamingClient;
428
429impl HttpClientExt for SilentStreamingClient {
430 no_unary_surface!();
431
432 fn send_streaming<T>(
433 &self,
434 _req: Request<T>,
435 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
436 where
437 T: Into<Bytes> + WasmCompatSend,
438 {
439 std::future::pending()
440 }
441}
442
443#[derive(Clone, Debug, Default)]
446pub struct PacedStreamingClient {
447 pub chunks: Vec<&'static str>,
449 pub gap: std::time::Duration,
451}
452
453impl HttpClientExt for PacedStreamingClient {
454 no_unary_surface!();
455
456 fn send_streaming<T>(
457 &self,
458 _req: Request<T>,
459 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
460 where
461 T: Into<Bytes> + WasmCompatSend,
462 {
463 let (chunks, gap) = (self.chunks.clone(), self.gap);
464 async move {
465 let byte_stream =
466 futures::StreamExt::then(futures::stream::iter(chunks), move |chunk| async move {
467 crate::wasm_compat::sleep(gap).await;
468 Ok::<Bytes, http_client::Error>(Bytes::from_static(chunk.as_bytes()))
469 });
470 let byte_stream = futures::StreamExt::chain(byte_stream, futures::stream::pending());
471 let boxed_stream: http_client::BoxedStream = Box::pin(byte_stream);
472 Response::builder()
473 .status(http::StatusCode::OK)
474 .header(http::header::CONTENT_TYPE, "text/event-stream")
475 .body(boxed_stream)
476 .map_err(http_client::Error::Protocol)
477 }
478 }
479}
480
481#[derive(Debug, Clone)]
484pub struct HttpErrorStreamingClient {
485 pub status: http::StatusCode,
486 pub body: String,
487}
488
489impl HttpErrorStreamingClient {
490 pub fn new(status: http::StatusCode, body: impl Into<String>) -> Self {
492 Self {
493 status,
494 body: body.into(),
495 }
496 }
497}
498
499impl Default for HttpErrorStreamingClient {
500 fn default() -> Self {
503 Self::new(http::StatusCode::INTERNAL_SERVER_ERROR, String::new())
504 }
505}
506
507impl HttpClientExt for HttpErrorStreamingClient {
508 no_unary_surface!();
509
510 fn send_streaming<T>(
511 &self,
512 _req: Request<T>,
513 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
514 where
515 T: Into<Bytes> + WasmCompatSend,
516 {
517 let status = self.status;
518 let body = self.body.clone();
519 async move {
520 Err(http_client::Error::non_success_with_details(
521 status,
522 http::HeaderMap::new(),
523 body,
524 ))
525 }
526 }
527}
528
529#[derive(Debug, Clone)]
533pub struct NonSuccessStreamingClient {
534 pub status: http::StatusCode,
535 pub headers: http::HeaderMap,
536 pub body: Bytes,
537}
538
539impl HttpClientExt for NonSuccessStreamingClient {
540 no_unary_surface!();
541
542 fn send_streaming<T>(
543 &self,
544 _req: Request<T>,
545 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
546 where
547 T: Into<Bytes> + WasmCompatSend,
548 {
549 let status = self.status;
550 let headers = self.headers.clone();
551 let body = self.body.clone();
552 async move {
553 let byte_stream = futures::stream::iter(vec![Ok::<Bytes, http_client::Error>(body)]);
554 let boxed_stream: http_client::BoxedStream = Box::pin(byte_stream);
555 let mut response = Response::builder()
556 .status(status)
557 .body(boxed_stream)
558 .map_err(http_client::Error::Protocol)?;
559 *response.headers_mut() = headers;
560 Ok(response)
561 }
562 }
563}
564
565#[derive(Debug, Clone, Default)]
568pub struct SequencedStreamingHttpClient {
569 chunks: Arc<Mutex<Option<Vec<http_client::Result<Bytes>>>>>,
570}
571
572impl SequencedStreamingHttpClient {
573 pub fn new(chunks: Vec<http_client::Result<Bytes>>) -> Self {
575 Self {
576 chunks: Arc::new(Mutex::new(Some(chunks))),
577 }
578 }
579}
580
581impl HttpClientExt for SequencedStreamingHttpClient {
582 no_unary_surface!();
583
584 fn send_streaming<T>(
585 &self,
586 _req: Request<T>,
587 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
588 where
589 T: Into<Bytes> + WasmCompatSend,
590 {
591 let chunks = lock(&self.chunks).take();
592
593 async move {
594 let Some(chunks) = chunks else {
595 return Err(http_client::Error::non_success_with_details(
596 http::StatusCode::INTERNAL_SERVER_ERROR,
597 http::HeaderMap::new(),
598 "streaming chunks should only be consumed once".to_string(),
599 ));
600 };
601
602 let byte_stream = futures::stream::iter(chunks);
603 let boxed_stream: http_client::BoxedStream = Box::pin(byte_stream);
604
605 Response::builder()
606 .status(http::StatusCode::OK)
607 .header(http::header::CONTENT_TYPE, "text/event-stream")
608 .body(boxed_stream)
609 .map_err(http_client::Error::Protocol)
610 }
611 }
612}