1use std::{
4 collections::VecDeque,
5 future::{self, Future},
6 sync::{Arc, Mutex, MutexGuard},
7};
8
9use bytes::Bytes;
10
11use crate::{
12 http_client::{
13 self, HttpClientExt, LazyBody, MultipartForm, Request, Response, StreamingResponse,
14 },
15 wasm_compat::WasmCompatSend,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CapturedHttpRequest {
21 pub uri: String,
23 pub headers: http::HeaderMap,
25 pub body: Bytes,
27}
28
29#[derive(Clone, Debug)]
31pub enum MockHttpResponse {
32 Success(Bytes),
34 Error(http::StatusCode, String),
36 ErrorResponse(http::StatusCode, Bytes),
39}
40
41impl MockHttpResponse {
42 pub fn success(body: impl Into<Bytes>) -> Self {
44 Self::Success(body.into())
45 }
46
47 pub fn error(status: http::StatusCode, message: impl Into<String>) -> Self {
49 Self::Error(status, message.into())
50 }
51}
52
53impl Default for MockHttpResponse {
54 fn default() -> Self {
55 Self::Success(Bytes::new())
56 }
57}
58
59#[derive(Clone, Debug, Default)]
62pub struct RecordingHttpClient {
63 requests: Arc<Mutex<Vec<CapturedHttpRequest>>>,
64 response: Arc<Mutex<MockHttpResponse>>,
65}
66
67impl RecordingHttpClient {
68 pub fn new(response_body: impl Into<Bytes>) -> Self {
70 Self {
71 requests: Arc::new(Mutex::new(Vec::new())),
72 response: Arc::new(Mutex::new(MockHttpResponse::success(response_body))),
73 }
74 }
75
76 pub fn with_error(status: http::StatusCode, message: impl Into<String>) -> Self {
78 Self {
79 requests: Arc::new(Mutex::new(Vec::new())),
80 response: Arc::new(Mutex::new(MockHttpResponse::error(status, message))),
81 }
82 }
83
84 pub fn with_error_response(status: http::StatusCode, body: impl Into<Bytes>) -> Self {
87 Self {
88 requests: Arc::new(Mutex::new(Vec::new())),
89 response: Arc::new(Mutex::new(MockHttpResponse::ErrorResponse(
90 status,
91 body.into(),
92 ))),
93 }
94 }
95
96 pub fn requests(&self) -> Vec<CapturedHttpRequest> {
98 self.requests_guard().clone()
99 }
100
101 pub fn set_response(&self, response: MockHttpResponse) {
103 *self.response_guard() = response;
104 }
105
106 fn requests_guard(&self) -> MutexGuard<'_, Vec<CapturedHttpRequest>> {
107 match self.requests.lock() {
108 Ok(guard) => guard,
109 Err(poisoned) => poisoned.into_inner(),
110 }
111 }
112
113 fn response_guard(&self) -> MutexGuard<'_, MockHttpResponse> {
114 match self.response.lock() {
115 Ok(guard) => guard,
116 Err(poisoned) => poisoned.into_inner(),
117 }
118 }
119
120 fn record_request(&self, uri: String, headers: http::HeaderMap, body: Bytes) {
121 self.requests_guard()
122 .push(CapturedHttpRequest { uri, headers, body });
123 }
124
125 fn build_unary_response<U>(
126 response: MockHttpResponse,
127 ) -> http_client::Result<Response<LazyBody<U>>>
128 where
129 U: From<Bytes> + WasmCompatSend + 'static,
130 {
131 let (status, response_body) = match response {
132 MockHttpResponse::Success(response_body) => (http::StatusCode::OK, response_body),
133 MockHttpResponse::Error(status, message) => {
134 return Err(http_client::Error::InvalidStatusCodeWithMessage(
135 status, message,
136 ));
137 }
138 MockHttpResponse::ErrorResponse(status, response_body) => (status, response_body),
139 };
140 let body: LazyBody<U> = Box::pin(async move { Ok(U::from(response_body)) });
141 Response::builder()
142 .status(status)
143 .body(body)
144 .map_err(http_client::Error::Protocol)
145 }
146}
147
148impl HttpClientExt for RecordingHttpClient {
149 fn send<T, U>(
150 &self,
151 req: Request<T>,
152 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
153 where
154 T: Into<Bytes> + WasmCompatSend,
155 U: From<Bytes> + WasmCompatSend + 'static,
156 {
157 let response = self.response_guard().clone();
158 let (parts, body) = req.into_parts();
159 self.record_request(parts.uri.to_string(), parts.headers, body.into());
160
161 async move { Self::build_unary_response(response) }
162 }
163
164 fn send_multipart<U>(
165 &self,
166 req: Request<MultipartForm>,
167 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
168 where
169 U: From<Bytes> + WasmCompatSend + 'static,
170 {
171 let response = self.response_guard().clone();
172 let (parts, _body) = req.into_parts();
173 self.record_request(parts.uri.to_string(), parts.headers, Bytes::new());
174
175 async move { Self::build_unary_response(response) }
176 }
177
178 fn send_streaming<T>(
179 &self,
180 _req: Request<T>,
181 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
182 where
183 T: Into<Bytes> + WasmCompatSend,
184 {
185 future::ready(Err(http_client::Error::InvalidStatusCode(
186 http::StatusCode::NOT_IMPLEMENTED,
187 )))
188 }
189}
190
191#[derive(Clone, Debug, Default)]
197pub struct SequencedHttpClient {
198 requests: Arc<Mutex<Vec<CapturedHttpRequest>>>,
199 responses: Arc<Mutex<VecDeque<MockHttpResponse>>>,
200}
201
202impl SequencedHttpClient {
203 pub fn new(responses: impl IntoIterator<Item = MockHttpResponse>) -> Self {
205 Self {
206 requests: Arc::new(Mutex::new(Vec::new())),
207 responses: Arc::new(Mutex::new(responses.into_iter().collect())),
208 }
209 }
210
211 pub fn requests(&self) -> Vec<CapturedHttpRequest> {
213 match self.requests.lock() {
214 Ok(guard) => guard.clone(),
215 Err(poisoned) => poisoned.into_inner().clone(),
216 }
217 }
218
219 pub fn remaining_responses(&self) -> usize {
221 match self.responses.lock() {
222 Ok(guard) => guard.len(),
223 Err(poisoned) => poisoned.into_inner().len(),
224 }
225 }
226
227 fn record_request(&self, uri: String, headers: http::HeaderMap, body: Bytes) {
228 let request = CapturedHttpRequest { uri, headers, body };
229 match self.requests.lock() {
230 Ok(mut guard) => guard.push(request),
231 Err(poisoned) => poisoned.into_inner().push(request),
232 }
233 }
234
235 fn next_response(&self) -> Option<MockHttpResponse> {
236 match self.responses.lock() {
237 Ok(mut guard) => guard.pop_front(),
238 Err(poisoned) => poisoned.into_inner().pop_front(),
239 }
240 }
241}
242
243impl HttpClientExt for SequencedHttpClient {
244 fn send<T, U>(
245 &self,
246 req: Request<T>,
247 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
248 where
249 T: Into<Bytes> + WasmCompatSend,
250 U: From<Bytes> + WasmCompatSend + 'static,
251 {
252 let response = self.next_response();
253 let (parts, body) = req.into_parts();
254 self.record_request(parts.uri.to_string(), parts.headers, body.into());
255
256 async move {
257 match response {
258 Some(response) => RecordingHttpClient::build_unary_response(response),
259 None => Err(http_client::Error::InvalidStatusCode(
260 http::StatusCode::NOT_IMPLEMENTED,
261 )),
262 }
263 }
264 }
265
266 fn send_multipart<U>(
267 &self,
268 req: Request<MultipartForm>,
269 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
270 where
271 U: From<Bytes> + WasmCompatSend + 'static,
272 {
273 let response = self.next_response();
274 let (parts, _body) = req.into_parts();
275 self.record_request(parts.uri.to_string(), parts.headers, Bytes::new());
276
277 async move {
278 match response {
279 Some(response) => RecordingHttpClient::build_unary_response(response),
280 None => Err(http_client::Error::InvalidStatusCode(
281 http::StatusCode::NOT_IMPLEMENTED,
282 )),
283 }
284 }
285 }
286
287 fn send_streaming<T>(
288 &self,
289 _req: Request<T>,
290 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
291 where
292 T: Into<Bytes> + WasmCompatSend,
293 {
294 future::ready(Err(http_client::Error::InvalidStatusCode(
295 http::StatusCode::NOT_IMPLEMENTED,
296 )))
297 }
298}
299
300#[derive(Clone, Debug, Default)]
304pub struct MockStreamingClient {
305 pub sse_bytes: Bytes,
307}
308
309impl HttpClientExt for MockStreamingClient {
310 fn send<T, U>(
311 &self,
312 _req: Request<T>,
313 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
314 where
315 T: Into<Bytes> + WasmCompatSend,
316 U: From<Bytes> + WasmCompatSend + 'static,
317 {
318 future::ready(Err(http_client::Error::InvalidStatusCode(
319 http::StatusCode::NOT_IMPLEMENTED,
320 )))
321 }
322
323 fn send_multipart<U>(
324 &self,
325 _req: Request<MultipartForm>,
326 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
327 where
328 U: From<Bytes> + WasmCompatSend + 'static,
329 {
330 future::ready(Err(http_client::Error::InvalidStatusCode(
331 http::StatusCode::NOT_IMPLEMENTED,
332 )))
333 }
334
335 fn send_streaming<T>(
336 &self,
337 _req: Request<T>,
338 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
339 where
340 T: Into<Bytes> + WasmCompatSend,
341 {
342 let sse_bytes = self.sse_bytes.clone();
343 async move {
344 let byte_stream =
345 futures::stream::iter(vec![Ok::<Bytes, http_client::Error>(sse_bytes)]);
346 let boxed_stream: http_client::sse::BoxedStream = Box::pin(byte_stream);
347
348 Response::builder()
349 .status(http::StatusCode::OK)
350 .header(http::header::CONTENT_TYPE, "text/event-stream")
351 .body(boxed_stream)
352 .map_err(http_client::Error::Protocol)
353 }
354 }
355}
356
357#[derive(Debug, Clone)]
360pub struct HttpErrorStreamingClient {
361 pub status: http::StatusCode,
362 pub body: String,
363}
364
365impl HttpErrorStreamingClient {
366 pub fn new(status: http::StatusCode, body: impl Into<String>) -> Self {
368 Self {
369 status,
370 body: body.into(),
371 }
372 }
373}
374
375impl Default for HttpErrorStreamingClient {
376 fn default() -> Self {
379 Self::new(http::StatusCode::INTERNAL_SERVER_ERROR, String::new())
380 }
381}
382
383impl HttpClientExt for HttpErrorStreamingClient {
384 fn send<T, U>(
385 &self,
386 _req: Request<T>,
387 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
388 where
389 T: Into<Bytes> + WasmCompatSend,
390 U: From<Bytes> + WasmCompatSend + 'static,
391 {
392 future::ready(Err(http_client::Error::InvalidStatusCode(
393 http::StatusCode::NOT_IMPLEMENTED,
394 )))
395 }
396
397 fn send_multipart<U>(
398 &self,
399 _req: Request<MultipartForm>,
400 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
401 where
402 U: From<Bytes> + WasmCompatSend + 'static,
403 {
404 future::ready(Err(http_client::Error::InvalidStatusCode(
405 http::StatusCode::NOT_IMPLEMENTED,
406 )))
407 }
408
409 fn send_streaming<T>(
410 &self,
411 _req: Request<T>,
412 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
413 where
414 T: Into<Bytes> + WasmCompatSend,
415 {
416 let status = self.status;
417 let body = self.body.clone();
418 async move {
419 Err(http_client::Error::InvalidStatusCodeWithMessage(
420 status, body,
421 ))
422 }
423 }
424}
425
426#[derive(Debug, Clone, Default)]
429pub struct SequencedStreamingHttpClient {
430 chunks: Arc<Mutex<Option<Vec<http_client::Result<Bytes>>>>>,
431}
432
433impl SequencedStreamingHttpClient {
434 pub fn new(chunks: Vec<http_client::Result<Bytes>>) -> Self {
436 Self {
437 chunks: Arc::new(Mutex::new(Some(chunks))),
438 }
439 }
440}
441
442impl HttpClientExt for SequencedStreamingHttpClient {
443 fn send<T, U>(
444 &self,
445 _req: Request<T>,
446 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
447 where
448 T: Into<Bytes> + WasmCompatSend,
449 U: From<Bytes> + WasmCompatSend + 'static,
450 {
451 future::ready(Err(http_client::Error::InvalidStatusCode(
452 http::StatusCode::NOT_IMPLEMENTED,
453 )))
454 }
455
456 fn send_multipart<U>(
457 &self,
458 _req: Request<MultipartForm>,
459 ) -> impl Future<Output = http_client::Result<Response<LazyBody<U>>>> + WasmCompatSend + 'static
460 where
461 U: From<Bytes> + WasmCompatSend + 'static,
462 {
463 future::ready(Err(http_client::Error::InvalidStatusCode(
464 http::StatusCode::NOT_IMPLEMENTED,
465 )))
466 }
467
468 fn send_streaming<T>(
469 &self,
470 _req: Request<T>,
471 ) -> impl Future<Output = http_client::Result<StreamingResponse>> + WasmCompatSend
472 where
473 T: Into<Bytes> + WasmCompatSend,
474 {
475 let chunks = match self.chunks.lock() {
476 Ok(mut guard) => guard.take(),
477 Err(poisoned) => poisoned.into_inner().take(),
478 };
479
480 async move {
481 let Some(chunks) = chunks else {
482 return Err(http_client::Error::InvalidStatusCodeWithMessage(
483 http::StatusCode::INTERNAL_SERVER_ERROR,
484 "streaming chunks should only be consumed once".to_string(),
485 ));
486 };
487
488 let byte_stream = futures::stream::iter(chunks);
489 let boxed_stream: http_client::sse::BoxedStream = Box::pin(byte_stream);
490
491 Response::builder()
492 .status(http::StatusCode::OK)
493 .header(http::header::CONTENT_TYPE, "text/event-stream")
494 .body(boxed_stream)
495 .map_err(http_client::Error::Protocol)
496 }
497 }
498}