Skip to main content

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}