typed_openapi/transport.rs
1//! The one seam between a decided request and the network.
2//!
3//! The vocabulary is `http::Request<Vec<u8>>` in, `http::Response<Vec<u8>>`
4//! out, which is what every client-agnostic Rust crate converges on (oauth2,
5//! rustify, atrium-xrpc, kube-core). There is no de-facto *trait*, so this
6//! crate defines its own two — a sync one and an async one, because a single
7//! trait cannot be both and `maybe-async` makes the flavour a global switch.
8//!
9//! This crate ships no adapter and depends on no HTTP client, in any feature
10//! combination: an adopter's choice of client is theirs, and a crate that
11//! pinned one would make it everyone's. `examples/toy/cli/src/client.rs` has
12//! both adapters in full — a ureq 3 agent in nine lines, a `reqwest::Client`
13//! in twelve — written to be copied rather than depended on.
14//!
15//! An adapter in an adopter's crate wraps the client in a newtype, because
16//! [`SyncClient`] and the client are both foreign there. A crate that owns
17//! either one writes `impl SyncClient for ureq::Agent` directly.
18//!
19//! Both `send` methods take `&self`, so a client is shared rather than
20//! borrowed exclusively — which is what `ureq::Agent` and `reqwest::Client`
21//! are built for, and what lets one client serve a whole command tree. An
22//! adapter over a client that needs `&mut` to send holds it behind a `Mutex`,
23//! and [`Recorder`] holds its script the same way; that is why a recorder is
24//! scripted, sent through and read back without ever being `mut`.
25//!
26//! No adapter may turn a status code into an error: the status belongs to the
27//! layer above, which needs the body that came with it. ureq does this by
28//! default and must be built with `http_status_as_error(false)`.
29//!
30//! # Writing a script
31//!
32//! [`Recorder`] is the client the tests of this crate and of its example
33//! adoption run against. It sends nothing, answers from a script, and keeps
34//! every request it was given.
35//!
36//! An answer is queued either for one **route** — the method and the path of
37//! the request as it goes out, with the query string left out of it — or for
38//! **anything**. A request takes the next answer queued for its own route;
39//! when that queue is empty it takes the next answer queued for anything; when
40//! that is empty too the answer is `200 {}`.
41//!
42//! ```
43//! use http::{Method, StatusCode};
44//! use serde_json::json;
45//! use typed_openapi::{Recorder, SyncClient};
46//!
47//! let client = Recorder::new()
48//! .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!([{"id": 5}]))
49//! .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!([]))
50//! .failing_route(Method::POST, "/vouchers", "the request never left")
51//! .answering(StatusCode::OK, &json!({}));
52//!
53//! let get = |uri: &str| http::Request::get(uri).body(Vec::new()).unwrap();
54//! assert_eq!(client.send(get("/vouchers?page=1")).unwrap().body(), br#"[{"id":5}]"#);
55//! assert_eq!(client.send(get("/vouchers?page=2")).unwrap().body(), b"[]");
56//!
57//! // One answer queued for anything is left, and nothing asked for it.
58//! assert_eq!(client.unused(), 2);
59//! assert_eq!(client.take().len(), 2);
60//! ```
61//!
62//! Several answers queued for one route come back in the order they were
63//! queued, which is what a pager needs — page, next page, empty page — and
64//! what a retry policy needs for "a failure, then an answer". Queueing per
65//! route is what keeps a scenario that crosses several endpoints from being
66//! pinned to the order the code under test happens to send in.
67//!
68//! A scripted failure reaches the caller as [`RecorderError`] through whichever
69//! `send` was called, so everything a caller does with a transport failure — a
70//! retry policy, a ledger line for an attempt whose outcome never arrived — is
71//! reachable from a test. The request that got it is recorded like any other.
72//!
73//! [`Recorder::take`] hands back what was sent, oldest first, and
74//! [`Recorder::unused`] how many answers were never reached: a run that left
75//! answers behind did not do what the test set it up to do, and that is worth
76//! asserting on rather than inferring.
77//!
78//! # An answer that carries headers
79//!
80//! `answering` and `answering_route` are the short spelling of the common
81//! case, and they build a JSON response. A caller that branches on a *header* —
82//! a pager following `Link`, a backoff reading `Retry-After`, a create reading
83//! `Location` — queues a whole [`HttpResponse`] instead, through
84//! [`Recorder::answering_with`] or [`Recorder::answering_route_with`].
85//! [`json_response`] builds the one the short spelling builds, so "the same
86//! answer, plus a header" is two lines:
87//!
88//! ```
89//! use http::{Method, StatusCode, header};
90//! use serde_json::json;
91//! use typed_openapi::{Recorder, SyncClient, json_response};
92//!
93//! let mut page = json_response(StatusCode::OK, &json!([{"id": 5}]));
94//! page.headers_mut().insert(
95//! header::LINK,
96//! header::HeaderValue::from_static(r#"</vouchers?page=2>; rel="next""#),
97//! );
98//!
99//! let client = Recorder::new().answering_route_with(Method::GET, "/vouchers", page);
100//! let answer = client.send(http::Request::get("/vouchers").body(Vec::new()).unwrap()).unwrap();
101//!
102//! assert_eq!(answer.headers()[header::LINK], r#"</vouchers?page=2>; rel="next""#);
103//! ```
104//!
105//! # Refusing what nobody queued
106//!
107//! An empty script answers `200 {}`, which is what a test that only cares what
108//! went *out* wants: it queues nothing and asserts on [`Recorder::take`].
109//!
110//! A test that does care what came back wants the opposite, and asks for it
111//! with [`Recorder::strict`]. A strict recorder **panics** on a request no
112//! queue has an answer for, naming the route it was asked for and what it is
113//! still holding. That is a bug in the test rather than in the code under test,
114//! and a panic puts it on the line that caused it — where a plausible, empty,
115//! successful answer would surface as an assertion going red three layers away.
116//! It is a panic and not a [`RecorderError`] for the same reason: a failure the
117//! script asked for and a request the script forgot are different mistakes, and
118//! a test scripted to expect the first must not pass on the second.
119//!
120//! Strictness says nothing about what a queued answer is. A strict recorder
121//! answers, fails and records exactly as a lenient one does; only the empty
122//! case differs. Both `send` methods do their work when they are called, so the
123//! panic lands on the `send` line even on the async path, before anything is
124//! awaited.
125
126use std::collections::{HashMap, VecDeque};
127use std::fmt;
128use std::sync::{Mutex, MutexGuard, PoisonError};
129
130use http::{Method, Request, Response, StatusCode, header};
131use thiserror::Error;
132
133/// What every adapter takes.
134pub type HttpRequest = Request<Vec<u8>>;
135/// What every adapter returns.
136pub type HttpResponse = Response<Vec<u8>>;
137
138/// Something that can send a request and wait for the answer.
139pub trait SyncClient {
140 type Error: std::error::Error + Send + Sync + 'static;
141
142 fn send(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error>;
143}
144
145/// A reference to a client is a client, which is what the `&self` receiver on
146/// [`SyncClient::send`] already promises.
147///
148/// Without this, a caller holding `&dyn SyncClient<Error = E>` — the natural
149/// way to hold one of two clients, a production one and a [`Recorder`] — cannot
150/// hand it to anything in this crate, because every `send` here is generic over
151/// `C: SyncClient` and the trait object is not one. The delegating newtype that
152/// closes the gap carries no decision, so the crate closes it instead.
153///
154/// `?Sized` is the load-bearing half: bounded to `C: Sized` the impl covers
155/// `&ConcreteClient` and still not `&dyn SyncClient<Error = E>`, which is the
156/// case that wanted the newtype.
157///
158/// The cost, which is a decision rather than a side effect: `&C` is spoken for,
159/// so a downstream crate cannot write `impl SyncClient for &Theirs`. It writes
160/// the impl on `Theirs` and takes the reference from here.
161impl<C: SyncClient + ?Sized> SyncClient for &C {
162 type Error = C::Error;
163
164 fn send(&self, request: HttpRequest) -> Result<HttpResponse, C::Error> {
165 (**self).send(request)
166 }
167}
168
169/// The same, for a caller inside a runtime. `Send` on the future so that a
170/// call can be `tokio::spawn`ed.
171pub trait AsyncClient {
172 type Error: std::error::Error + Send + Sync + 'static;
173
174 fn send(
175 &self,
176 request: HttpRequest,
177 ) -> impl Future<Output = Result<HttpResponse, Self::Error>> + Send;
178}
179
180/// The same for the async flavour, for the same reason and at the same cost.
181///
182/// Two things differ. The future handed back is `C`'s own and the trait already
183/// declares that one `Send`, so nothing here asks for `C: Sync`: what crosses a
184/// `tokio::spawn` is the inner future, not the reference, and a client whose
185/// answer is ready before the future is built is free to be `!Sync`. And the
186/// trait returns `impl Future`, which makes it dyn-incompatible, so there is no
187/// `&dyn AsyncClient` for `?Sized` to reach here — it is written this way so the
188/// two impls are one shape rather than two rules to remember.
189impl<C: AsyncClient + ?Sized> AsyncClient for &C {
190 type Error = C::Error;
191
192 fn send(
193 &self,
194 request: HttpRequest,
195 ) -> impl Future<Output = Result<HttpResponse, C::Error>> + Send {
196 (**self).send(request)
197 }
198}
199
200/// The failure a script asked for, carrying the message it was queued with.
201///
202/// A message is the whole of it, deliberately. The two states a retry rule
203/// tells apart — the request never left, and the request left and nothing came
204/// back — are a reading of what a failure *means*, and this crate has no far
205/// side to read: it sends nothing. A test that needs the distinction queues
206/// two different messages and asserts on the one it got, and the reading stays
207/// where the rule is.
208#[derive(Debug, Error)]
209#[error("{message}")]
210pub struct RecorderError {
211 message: String,
212}
213
214impl RecorderError {
215 /// The message this failure was queued with.
216 ///
217 /// The same text [`Display`](std::fmt::Display) renders, handed back
218 /// unwrapped so that a test compares against the constant it scripted
219 /// rather than against a rendering.
220 #[must_use]
221 pub fn message(&self) -> &str {
222 &self.message
223 }
224}
225
226/// A client that sends nothing, answers from a script, and keeps every request
227/// it was given.
228///
229/// It is both a [`SyncClient`] and an [`AsyncClient`] over one script, so one
230/// fixture tests both call paths, and because it *answers* it can stand in for
231/// the server through a multi-step chain — no socket, no runtime, no fixture
232/// server. The module documentation describes how a script is written.
233#[derive(Debug, Default)]
234pub struct Recorder {
235 held: Mutex<Held>,
236 /// What a request no queue has an answer for gets: `200 {}` when this is
237 /// false, a panic when [`Recorder::strict`] has set it.
238 strict: bool,
239}
240
241/// What a recorder is holding: what is left to answer, and what has been sent.
242///
243/// One lock over both, so that what was recorded and what was answered cannot
244/// disagree about the order they happened in.
245#[derive(Debug, Default)]
246struct Held {
247 per_route: HashMap<Route, VecDeque<Answer>>,
248 anything: VecDeque<Answer>,
249 sent: Vec<HttpRequest>,
250}
251
252/// What an answer is queued against: a method and a path.
253///
254/// The query string is no part of it, so a pager walking one path with a
255/// different `page` each time draws from one queue.
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257struct Route {
258 method: Method,
259 path: String,
260}
261
262impl Route {
263 fn new(method: Method, path: &str) -> Self {
264 Self {
265 method,
266 path: path.to_owned(),
267 }
268 }
269
270 fn of(request: &HttpRequest) -> Self {
271 Self::new(request.method().clone(), request.uri().path())
272 }
273}
274
275/// How a route reads in the refusal a strict recorder panics with, and the
276/// spelling the two constructors that queue for one take it in.
277impl fmt::Display for Route {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(f, "{} {}", self.method, self.path)
280 }
281}
282
283/// One thing a recorder does when a request reaches it.
284#[derive(Debug)]
285enum Answer {
286 Response(HttpResponse),
287 Failure(String),
288}
289
290impl Recorder {
291 /// A recorder with an empty script: every request is answered `200 {}`.
292 #[must_use]
293 pub fn new() -> Self {
294 Self::default()
295 }
296
297 /// Queue a JSON response for the next request on `method path`.
298 #[must_use]
299 pub fn answering_route(
300 self,
301 method: Method,
302 path: &str,
303 status: StatusCode,
304 body: &serde_json::Value,
305 ) -> Self {
306 self.queue_for(
307 Route::new(method, path),
308 Answer::Response(json_response(status, body)),
309 )
310 }
311
312 /// Queue `response` — headers and all — for the next request on
313 /// `method path`.
314 ///
315 /// The long spelling of [`Self::answering_route`], for a caller that
316 /// branches on something a status and a JSON body cannot carry.
317 /// [`json_response`] builds what the short spelling builds, to add a header
318 /// to.
319 #[must_use]
320 pub fn answering_route_with(self, method: Method, path: &str, response: HttpResponse) -> Self {
321 self.queue_for(Route::new(method, path), Answer::Response(response))
322 }
323
324 /// Queue a failure for the next request on `method path`.
325 #[must_use]
326 pub fn failing_route(self, method: Method, path: &str, message: &str) -> Self {
327 self.queue_for(
328 Route::new(method, path),
329 Answer::Failure(message.to_owned()),
330 )
331 }
332
333 /// Queue a JSON response for the next request whose own route has nothing
334 /// left.
335 ///
336 /// A scenario that does not care which endpoint answers what queues here
337 /// and stays one line per answer.
338 #[must_use]
339 pub fn answering(self, status: StatusCode, body: &serde_json::Value) -> Self {
340 self.queue_for_anything(Answer::Response(json_response(status, body)))
341 }
342
343 /// Queue `response` — headers and all — for the next request whose own
344 /// route has nothing left.
345 ///
346 /// The long spelling of [`Self::answering`], for a caller that branches on
347 /// something a status and a JSON body cannot carry.
348 #[must_use]
349 pub fn answering_with(self, response: HttpResponse) -> Self {
350 self.queue_for_anything(Answer::Response(response))
351 }
352
353 /// Queue a failure for the next request whose own route has nothing left.
354 #[must_use]
355 pub fn failing(self, message: &str) -> Self {
356 self.queue_for_anything(Answer::Failure(message.to_owned()))
357 }
358
359 /// Refuse a request no queue has an answer for, instead of answering
360 /// `200 {}`.
361 ///
362 /// The refusal is a panic naming the route and what the queues still hold,
363 /// because the mistake is in the test rather than in the code under test
364 /// and a panic reports it on the line that made it. Takes effect whenever
365 /// it is called: a script is the same script either way.
366 #[must_use]
367 pub fn strict(mut self) -> Self {
368 self.strict = true;
369 self
370 }
371
372 /// Every request sent so far, oldest first, and the recorder is left empty.
373 ///
374 /// It drains because `http::Request` is not `Clone`, so there is nothing to
375 /// hand back a copy of; a test that wants to look twice binds the vector.
376 pub fn take(&self) -> Vec<HttpRequest> {
377 std::mem::take(&mut self.held().sent)
378 }
379
380 /// How many scripted answers are still queued, over every route and the
381 /// anything queue together.
382 #[must_use]
383 pub fn unused(&self) -> usize {
384 let held = self.held();
385 held.anything.len() + held.per_route.values().map(VecDeque::len).sum::<usize>()
386 }
387
388 fn queue_for(mut self, route: Route, answer: Answer) -> Self {
389 self.script()
390 .per_route
391 .entry(route)
392 .or_default()
393 .push_back(answer);
394 self
395 }
396
397 fn queue_for_anything(mut self, answer: Answer) -> Self {
398 self.script().anything.push_back(answer);
399 self
400 }
401
402 /// The script, while the recorder is still being built and owned.
403 fn script(&mut self) -> &mut Held {
404 self.held.get_mut().unwrap_or_else(PoisonError::into_inner)
405 }
406
407 /// What the recorder is holding.
408 ///
409 /// A poisoned lock is taken anyway. The only way to poison it is for a test
410 /// to panic while holding it, and a second failure reported as a poisoned
411 /// mutex would bury the first one, which is the one that says what went
412 /// wrong.
413 fn held(&self) -> MutexGuard<'_, Held> {
414 self.held.lock().unwrap_or_else(PoisonError::into_inner)
415 }
416
417 fn answer(&self, request: HttpRequest) -> Result<HttpResponse, RecorderError> {
418 let mut held = self.held();
419 let route = Route::of(&request);
420 held.sent.push(request);
421
422 let for_the_route = held.per_route.get_mut(&route).and_then(VecDeque::pop_front);
423 let queued = match for_the_route {
424 Some(answer) => Some(answer),
425 None => held.anything.pop_front(),
426 };
427
428 match queued {
429 Some(Answer::Response(response)) => Ok(response),
430 Some(Answer::Failure(message)) => Err(RecorderError { message }),
431 None if self.strict => unscripted(&route, &held),
432 None => Ok(json_response(StatusCode::OK, &serde_json::json!({}))),
433 }
434 }
435}
436
437/// What a strict recorder says about a request it was never given an answer
438/// for.
439///
440/// The routes are sorted because they come out of a `HashMap`, and a message
441/// that reads differently on every run is a message a reader stops trusting.
442fn unscripted(route: &Route, held: &Held) -> ! {
443 let mut queued: Vec<String> = held
444 .per_route
445 .iter()
446 .filter(|(_, answers)| !answers.is_empty())
447 .map(|(route, answers)| format!("{route} ({} left)", answers.len()))
448 .collect();
449 queued.sort();
450 panic!(
451 "the recorder was asked for `{route}` and has no answer for it. It is holding {queued:?} \
452 and {} queued for anything. Queue one with `answering_route` or `answering`, or drop \
453 `strict()` to let it answer 200 {{}}.",
454 held.anything.len(),
455 );
456}
457
458impl SyncClient for Recorder {
459 type Error = RecorderError;
460
461 fn send(&self, request: HttpRequest) -> Result<HttpResponse, RecorderError> {
462 self.answer(request)
463 }
464}
465
466impl AsyncClient for Recorder {
467 type Error = RecorderError;
468
469 fn send(
470 &self,
471 request: HttpRequest,
472 ) -> impl Future<Output = Result<HttpResponse, RecorderError>> + Send {
473 std::future::ready(self.answer(request))
474 }
475}
476
477/// A JSON response, which is what the short spelling of a scripted answer
478/// builds: `body` rendered into the body and `content-type: application/json`.
479///
480/// Public so that "the same answer, plus a header" does not start by rebuilding
481/// this — take one, insert the header, and queue it with
482/// [`Recorder::answering_with`] or [`Recorder::answering_route_with`].
483#[must_use]
484pub fn json_response(status: StatusCode, body: &serde_json::Value) -> HttpResponse {
485 let mut response = Response::new(body.to_string().into_bytes());
486 *response.status_mut() = status;
487 response.headers_mut().insert(
488 header::CONTENT_TYPE,
489 header::HeaderValue::from_static("application/json"),
490 );
491 response
492}
493
494#[cfg(test)]
495mod tests {
496 #![expect(
497 clippy::unwrap_used,
498 clippy::expect_used,
499 clippy::indexing_slicing,
500 reason = "a failed unwrap or a panicking index is a failing test"
501 )]
502
503 use std::pin::pin;
504 use std::task::{Context, Poll, Waker};
505
506 use serde_json::json;
507
508 use super::{
509 AsyncClient, HttpRequest, HttpResponse, Method, Recorder, RecorderError, Request,
510 StatusCode, SyncClient, header, json_response,
511 };
512
513 fn request(method: Method, uri: &str) -> HttpRequest {
514 Request::builder()
515 .method(method)
516 .uri(uri)
517 .body(Vec::new())
518 .unwrap()
519 }
520
521 fn sent(client: &Recorder, method: Method, uri: &str) -> HttpResponse {
522 SyncClient::send(client, request(method, uri)).unwrap()
523 }
524
525 fn body(response: &HttpResponse) -> serde_json::Value {
526 serde_json::from_slice(response.body()).unwrap()
527 }
528
529 /// A runtime in six lines. The recorder never yields, so the first poll is
530 /// ready — enough to drive the async path without taking tokio as a
531 /// dependency of this crate.
532 fn block_on<F: Future>(future: F) -> F::Output {
533 let mut future = pin!(future);
534 let mut cx = Context::from_waker(Waker::noop());
535 loop {
536 if let Poll::Ready(value) = future.as_mut().poll(&mut cx) {
537 return value;
538 }
539 }
540 }
541
542 #[test]
543 fn answers_queued_for_one_route_come_back_in_the_order_they_were_queued() {
544 let client = Recorder::new()
545 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!(["first"]))
546 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!(["second"]))
547 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!([]));
548
549 // The query is no part of the route, so a pager's three calls to one
550 // path draw from the one queue in the order it was filled.
551 let pages: Vec<serde_json::Value> = ["?page=1", "?page=2", "?page=3"]
552 .into_iter()
553 .map(|query| body(&sent(&client, Method::GET, &format!("/vouchers{query}"))))
554 .collect();
555
556 assert_eq!(pages, [json!(["first"]), json!(["second"]), json!([])]);
557 }
558
559 #[test]
560 fn a_route_is_answered_from_its_own_queue_and_not_from_another_routes() {
561 // One path, two methods: the method is half the route, so a read and a
562 // write of the same collection do not share a queue.
563 let client = Recorder::new()
564 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!("the list"))
565 .answering_route(
566 Method::POST,
567 "/vouchers",
568 StatusCode::CREATED,
569 &json!("the new one"),
570 );
571
572 assert_eq!(
573 body(&sent(&client, Method::POST, "/vouchers")),
574 json!("the new one")
575 );
576 assert_eq!(
577 body(&sent(&client, Method::GET, "/vouchers")),
578 json!("the list")
579 );
580 }
581
582 #[test]
583 fn a_route_with_nothing_queued_falls_back_to_anything_and_then_to_the_default() {
584 let client = Recorder::new().answering(StatusCode::ACCEPTED, &json!("whatever you asked"));
585
586 let from_anything = sent(&client, Method::GET, "/vouchers");
587 assert_eq!(from_anything.status(), StatusCode::ACCEPTED);
588 assert_eq!(body(&from_anything), json!("whatever you asked"));
589
590 let from_nothing = sent(&client, Method::GET, "/vouchers");
591 assert_eq!(from_nothing.status(), StatusCode::OK);
592 assert_eq!(body(&from_nothing), json!({}));
593 }
594
595 #[test]
596 fn a_route_with_something_queued_leaves_the_anything_queue_alone() {
597 let client = Recorder::new()
598 .answering_route(
599 Method::GET,
600 "/vouchers",
601 StatusCode::OK,
602 &json!("for the route"),
603 )
604 .answering(StatusCode::OK, &json!("for anything"));
605
606 assert_eq!(
607 body(&sent(&client, Method::GET, "/vouchers")),
608 json!("for the route")
609 );
610 assert_eq!(client.unused(), 1);
611 }
612
613 #[test]
614 fn a_scripted_failure_reaches_the_caller_and_the_request_is_recorded_anyway() {
615 let client =
616 Recorder::new().failing_route(Method::POST, "/vouchers", "the request never left");
617
618 let failed =
619 SyncClient::send(&client, request(Method::POST, "/vouchers")).expect_err("scripted");
620
621 assert_eq!(failed.message(), "the request never left");
622 assert_eq!(failed.to_string(), "the request never left");
623 let sent = client.take();
624 assert_eq!(sent.len(), 1, "a refused request went out like any other");
625 assert_eq!(sent[0].uri().path(), "/vouchers");
626 }
627
628 #[test]
629 fn two_failures_are_told_apart_by_the_messages_they_were_queued_with() {
630 let client = Recorder::new()
631 .failing_route(Method::POST, "/vouchers", "the request never left")
632 .failing("the request left and nothing came back");
633
634 let never = SyncClient::send(&client, request(Method::POST, "/vouchers"))
635 .expect_err("the route's own failure");
636 let silent = SyncClient::send(&client, request(Method::GET, "/vouchers"))
637 .expect_err("the failure queued for anything");
638
639 assert_eq!(never.message(), "the request never left");
640 assert_eq!(silent.message(), "the request left and nothing came back");
641 }
642
643 #[test]
644 fn what_is_left_unused_is_something_a_test_can_ask_about() {
645 let client = Recorder::new()
646 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!("first"))
647 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!("second"))
648 .answering(StatusCode::OK, &json!("third"));
649
650 assert_eq!(client.unused(), 3);
651 let _ = sent(&client, Method::GET, "/vouchers");
652 assert_eq!(client.unused(), 2);
653 let _ = sent(&client, Method::GET, "/elsewhere");
654 assert_eq!(client.unused(), 1);
655 }
656
657 #[test]
658 fn one_script_drives_the_sync_and_the_async_path_identically() {
659 let script = || {
660 Recorder::new()
661 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!("the list"))
662 .failing_route(Method::POST, "/vouchers", "the request never left")
663 };
664 let synchronous = script();
665 let asynchronous = script();
666
667 let read = body(&sent(&synchronous, Method::GET, "/vouchers"));
668 let wrote = SyncClient::send(&synchronous, request(Method::POST, "/vouchers"))
669 .expect_err("scripted");
670
671 let read_async = block_on(AsyncClient::send(
672 &asynchronous,
673 request(Method::GET, "/vouchers"),
674 ))
675 .unwrap();
676 let wrote_async = block_on(AsyncClient::send(
677 &asynchronous,
678 request(Method::POST, "/vouchers"),
679 ))
680 .expect_err("scripted");
681
682 assert_eq!(read, body(&read_async));
683 assert_eq!(wrote.message(), wrote_async.message());
684
685 let routes = |client: &Recorder| -> Vec<String> {
686 client
687 .take()
688 .iter()
689 .map(|request| format!("{} {}", request.method(), request.uri()))
690 .collect()
691 };
692 assert_eq!(routes(&synchronous), routes(&asynchronous));
693 }
694
695 #[test]
696 fn a_response_queued_whole_keeps_the_headers_it_was_built_with() {
697 let mut page = json_response(StatusCode::OK, &json!([{"id": 5}]));
698 page.headers_mut().insert(
699 header::LINK,
700 header::HeaderValue::from_static(r#"</vouchers?page=2>; rel="next""#),
701 );
702 let mut created = json_response(StatusCode::CREATED, &json!(null));
703 created.headers_mut().insert(
704 header::LOCATION,
705 header::HeaderValue::from_static("/vouchers/6"),
706 );
707
708 let client = Recorder::new()
709 .answering_route_with(Method::GET, "/vouchers", page)
710 .answering_with(created);
711
712 let listed = sent(&client, Method::GET, "/vouchers");
713 assert_eq!(
714 listed.headers()[header::LINK],
715 r#"</vouchers?page=2>; rel="next""#
716 );
717 assert_eq!(body(&listed), json!([{"id": 5}]));
718
719 // The anything queue carries a whole response too.
720 let made = sent(&client, Method::POST, "/vouchers");
721 assert_eq!(made.status(), StatusCode::CREATED);
722 assert_eq!(made.headers()[header::LOCATION], "/vouchers/6");
723 }
724
725 #[test]
726 fn the_long_spelling_of_an_answer_and_the_short_one_build_the_same_response() {
727 let short = Recorder::new().answering(StatusCode::OK, &json!({"id": 5}));
728 let long = Recorder::new().answering_with(json_response(StatusCode::OK, &json!({"id": 5})));
729
730 let from_short = sent(&short, Method::GET, "/vouchers");
731 let from_long = sent(&long, Method::GET, "/vouchers");
732
733 assert_eq!(from_short.status(), from_long.status());
734 assert_eq!(from_short.headers(), from_long.headers());
735 assert_eq!(from_short.body(), from_long.body());
736 }
737
738 /// Two routes are held, so the sort in the refusal is load-bearing: a
739 /// `HashMap` hands them over in a different order on every run, and this
740 /// test flakes without it.
741 #[test]
742 #[should_panic(
743 expected = "asked for `GET /vouchers` and has no answer for it. It is holding \
744 [\"POST /vouchers (2 left)\", \"PUT /vouchers/5 (1 left)\"] and 0 queued \
745 for anything"
746 )]
747 fn a_strict_recorder_refuses_what_nobody_queued_and_names_what_it_holds() {
748 let client = Recorder::new()
749 .strict()
750 .answering_route(Method::POST, "/vouchers", StatusCode::CREATED, &json!(null))
751 .answering_route(Method::POST, "/vouchers", StatusCode::CREATED, &json!(null))
752 .answering_route(Method::PUT, "/vouchers/5", StatusCode::OK, &json!(null));
753
754 let _refused = SyncClient::send(&client, request(Method::GET, "/vouchers"));
755 }
756
757 #[test]
758 fn a_strict_recorder_answers_and_fails_from_the_script_like_a_lenient_one() {
759 let client = Recorder::new()
760 .strict()
761 .answering_route(Method::GET, "/vouchers", StatusCode::OK, &json!("the list"))
762 .failing_route(Method::POST, "/vouchers", "the request never left");
763
764 assert_eq!(
765 body(&sent(&client, Method::GET, "/vouchers")),
766 json!("the list")
767 );
768 assert_eq!(
769 SyncClient::send(&client, request(Method::POST, "/vouchers"))
770 .expect_err("scripted")
771 .message(),
772 "the request never left"
773 );
774 assert_eq!(client.take().len(), 2, "both went out");
775 }
776
777 #[test]
778 #[should_panic(expected = "asked for `GET /vouchers`")]
779 fn the_async_path_refuses_when_send_is_called_rather_than_when_it_is_awaited() {
780 let client = Recorder::new().strict();
781
782 // Never awaited: the work happens in `send`, so the refusal lands on
783 // this line rather than wherever the future is polled.
784 let _refused = AsyncClient::send(&client, request(Method::GET, "/vouchers"));
785 }
786
787 /// An async client that is not `Sync`, and whose future is `Send` all the
788 /// same because the answer is ready before the future is built.
789 ///
790 /// The `Cell` is the whole of it: an atomic counter here would make `Eager`
791 /// `Sync` and the test below would stop asking its question.
792 struct Eager(std::cell::Cell<usize>);
793
794 impl AsyncClient for Eager {
795 type Error = RecorderError;
796
797 fn send(
798 &self,
799 _request: HttpRequest,
800 ) -> impl Future<Output = Result<HttpResponse, RecorderError>> + Send {
801 self.0.set(self.0.get() + 1);
802 std::future::ready(Ok(json_response(
803 StatusCode::OK,
804 &json!({ "answered": self.0.get() }),
805 )))
806 }
807 }
808
809 /// A reference to a client is a client on the async path, and the client it
810 /// refers to owes no `Sync`.
811 ///
812 /// What the blanket impl hands back is the future `Eager` itself declares
813 /// `Send`, so the reference never has to cross a thread for the promise to
814 /// hold — `is_send` is where that is pinned rather than assumed.
815 #[test]
816 fn a_reference_to_a_client_is_an_async_client_and_the_client_owes_no_sync() {
817 fn is_send<F: Send>(future: F) -> F {
818 future
819 }
820
821 let client = Eager(std::cell::Cell::new(0));
822 let through_reference: &Eager = &client;
823
824 let pending = is_send(AsyncClient::send(
825 &through_reference,
826 request(Method::GET, "/vouchers"),
827 ));
828 let answered = block_on(pending).expect("the eager client answers");
829
830 assert_eq!(body(&answered), json!({ "answered": 1 }));
831 }
832}