1use std::collections::VecDeque;
2use std::fmt;
3use std::sync::{Arc, Mutex};
4
5use bytes::Bytes;
6use futures_util::StreamExt;
7
8use crate::{
9 Body, Headers, HttpRequest, HttpResponse, HttpTransport, SendFuture, TransportError,
10 TransportErrorKind,
11};
12
13#[derive(Clone, Default)]
24pub struct ReplayTransport {
25 state: Arc<Mutex<State>>,
26}
27
28#[derive(Default)]
29struct State {
30 responses: VecDeque<ReplayResponse>,
31 requests: Vec<HttpRequest>,
32}
33
34impl fmt::Debug for ReplayTransport {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 let state = self.lock();
37 f.debug_struct("ReplayTransport")
38 .field("remaining", &state.responses.len())
39 .field("requests", &state.requests.len())
40 .finish()
41 }
42}
43
44impl ReplayTransport {
45 #[must_use]
47 pub fn new(responses: impl IntoIterator<Item = ReplayResponse>) -> Self {
48 Self {
49 state: Arc::new(Mutex::new(State {
50 responses: responses.into_iter().collect(),
51 requests: Vec::new(),
52 })),
53 }
54 }
55
56 pub fn push(&self, response: ReplayResponse) {
58 self.lock().responses.push_back(response);
59 }
60
61 #[must_use]
63 pub fn requests(&self) -> Vec<HttpRequest> {
64 self.lock().requests.clone()
65 }
66
67 #[must_use]
69 pub fn remaining(&self) -> usize {
70 self.lock().responses.len()
71 }
72
73 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
74 self.state.lock().unwrap_or_else(|e| e.into_inner())
75 }
76}
77
78impl HttpTransport for ReplayTransport {
79 fn send(&self, request: HttpRequest) -> SendFuture<'_> {
80 let next = {
81 let mut state = self.lock();
82 state.requests.push(request);
83 state.responses.pop_front()
84 };
85 Box::pin(async move {
86 let response = next.ok_or_else(|| {
87 TransportError::new(TransportErrorKind::Connect, "no replayed response left")
88 })?;
89 if let Some((kind, message)) = response.failure {
90 return Err(TransportError::new(kind, message));
91 }
92 let chunks = futures_util::stream::iter(response.chunks.into_iter().map(Ok));
93 let body: Body = if response.stalls {
94 Box::pin(chunks.chain(futures_util::stream::pending()))
95 } else {
96 Box::pin(chunks)
97 };
98 Ok(HttpResponse::new(response.status, response.headers, body))
99 })
100 }
101}
102
103#[derive(Debug, Clone)]
112pub struct ReplayResponse {
113 status: u16,
114 headers: Headers,
115 chunks: Vec<Bytes>,
116 failure: Option<(TransportErrorKind, String)>,
117 stalls: bool,
118}
119
120impl ReplayResponse {
121 #[must_use]
123 pub fn new(status: u16, body: impl Into<Bytes>) -> Self {
124 Self::chunks(status, [body])
125 }
126
127 #[must_use]
144 pub fn chunks(status: u16, chunks: impl IntoIterator<Item = impl Into<Bytes>>) -> Self {
145 Self {
146 status,
147 headers: Headers::new(),
148 chunks: chunks.into_iter().map(Into::into).collect(),
149 failure: None,
150 stalls: false,
151 }
152 }
153
154 #[must_use]
157 pub fn json(status: u16, body: impl Into<Bytes>) -> Self {
158 Self::new(status, body).header("content-type", "application/json")
159 }
160
161 #[must_use]
167 pub fn json_value(status: u16, value: &impl serde::Serialize) -> Self {
168 let body = serde_json::to_vec(value).expect("serialize replayed JSON response");
169 Self::json(status, body)
170 }
171
172 #[must_use]
175 pub fn sse(body: impl AsRef<str>) -> Self {
176 let chunks = body
177 .as_ref()
178 .split_inclusive("\n\n")
179 .map(|event| Bytes::from(event.to_owned()));
180 Self::chunks(200, chunks).header("content-type", "text/event-stream")
181 }
182
183 #[must_use]
202 pub fn ndjson(body: impl AsRef<str>) -> Self {
203 let chunks = body
204 .as_ref()
205 .split_inclusive('\n')
206 .map(|line| Bytes::from(line.to_owned()));
207 Self::chunks(200, chunks).header("content-type", "application/x-ndjson")
208 }
209
210 #[must_use]
212 pub fn failure(kind: TransportErrorKind, message: impl Into<String>) -> Self {
213 Self {
214 status: 0,
215 headers: Headers::new(),
216 chunks: Vec::new(),
217 failure: Some((kind, message.into())),
218 stalls: false,
219 }
220 }
221
222 #[must_use]
224 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
225 self.headers.insert(name, value);
226 self
227 }
228
229 #[must_use]
248 pub fn split_every(mut self, size: usize) -> Self {
249 assert!(
250 size > 0,
251 "a replayed body cannot be split into empty chunks"
252 );
253 let body: Vec<u8> = self.chunks.concat();
254 self.chunks = body.chunks(size).map(Bytes::copy_from_slice).collect();
255 self
256 }
257
258 #[must_use]
263 pub fn then_stall(mut self) -> Self {
264 self.stalls = true;
265 self
266 }
267}
268
269#[cfg(all(test, not(target_arch = "wasm32")))]
270mod tests {
271 use futures_util::StreamExt;
272
273 use super::*;
274
275 #[tokio::test]
276 async fn responses_are_served_in_order_and_requests_recorded() {
277 let transport = ReplayTransport::new([
278 ReplayResponse::json(200, r#"{"n":1}"#).header("request-id", "req_1"),
279 ReplayResponse::failure(TransportErrorKind::Timeout, "slow"),
280 ]);
281 let first = transport
282 .send(HttpRequest::post("https://x/a", "1"))
283 .await
284 .unwrap();
285 assert_eq!(first.headers.get("Request-Id"), Some("req_1"));
286 assert_eq!(first.text().await.unwrap(), r#"{"n":1}"#);
287 let second = transport
288 .send(HttpRequest::get("https://x/b"))
289 .await
290 .unwrap_err();
291 assert_eq!(second.kind(), TransportErrorKind::Timeout);
292 let exhausted = transport
293 .send(HttpRequest::get("https://x/c"))
294 .await
295 .unwrap_err();
296 assert_eq!(exhausted.kind(), TransportErrorKind::Connect);
297
298 let urls: Vec<_> = transport.requests().into_iter().map(|r| r.url).collect();
299 assert_eq!(urls, ["https://x/a", "https://x/b", "https://x/c"]);
300 assert_eq!(transport.remaining(), 0);
301 }
302
303 #[tokio::test]
304 async fn sse_is_chunked_per_event() {
305 let transport = ReplayTransport::default();
306 transport.push(ReplayResponse::sse("data: a\n\ndata: b\n\n"));
307 let response = transport.send(HttpRequest::get("https://x")).await.unwrap();
308 assert_eq!(
309 response.headers.get("content-type"),
310 Some("text/event-stream")
311 );
312 let chunks: Vec<_> = response.into_body().collect().await;
313 assert_eq!(chunks.len(), 2);
314 }
315
316 #[tokio::test]
317 async fn bodies_arrive_in_the_chunks_they_were_built_with_or_split() {
318 let body = |response: ReplayResponse| async {
319 let transport = ReplayTransport::new([response]);
320 let response = transport.send(HttpRequest::get("https://x")).await.unwrap();
321 let chunks: Vec<Bytes> = response.into_body().map(Result::unwrap).collect().await;
322 chunks
323 };
324 assert_eq!(
325 body(ReplayResponse::chunks(200, ["ab", "", "cde"])).await,
326 ["ab", "", "cde"]
327 );
328 assert_eq!(
329 body(ReplayResponse::chunks(200, ["ab", "", "cde"]).split_every(2)).await,
330 ["ab", "cd", "e"]
331 );
332 assert_eq!(
333 body(ReplayResponse::new(200, "héllo").split_every(1))
334 .await
335 .len(),
336 "héllo".len()
337 );
338 assert!(
339 body(ReplayResponse::new(200, "").split_every(3))
340 .await
341 .is_empty()
342 );
343 }
344
345 #[test]
346 fn json_takes_the_body_as_written_and_json_value_serializes() {
347 let written = ReplayResponse::json(400, "{not json");
348 assert_eq!(written.chunks, ["{not json"]);
349 let value = ReplayResponse::json_value(200, &serde_json::json!({"ok": true}));
350 assert_eq!(value.chunks, [r#"{"ok":true}"#]);
351 assert_eq!(value.headers.get("content-type"), Some("application/json"));
352 }
353
354 #[tokio::test]
355 async fn stalled_bodies_never_end() {
356 let transport = ReplayTransport::new([ReplayResponse::sse("data: a\n\n").then_stall()]);
357 let mut body = transport
358 .send(HttpRequest::get("https://x"))
359 .await
360 .unwrap()
361 .into_body();
362 assert!(body.next().await.is_some());
363 let next = futures_util::future::select(
364 body.next(),
365 futures_timer::Delay::new(std::time::Duration::from_millis(20)),
366 )
367 .await;
368 assert!(matches!(next, futures_util::future::Either::Right(_)));
369 }
370}