Skip to main content

roas_arazzo_executor/
testing.rs

1//! A client that answers from a script.
2//!
3//! Running a workflow is the interesting part; talking to a server is
4//! not. [`Fake`] answers each request from a list prepared in advance
5//! and keeps what it was asked, so a test can assert on both what the
6//! engine sent and what it made of the answers — with no network, no
7//! runtime and no timing.
8
9use crate::http::{
10    AsyncHttpClient, ClientError, HttpClient, HttpRequest, HttpResponse, SendFuture, SleepFuture,
11};
12use serde_json::Value;
13use std::time::Duration;
14
15/// An HTTP client that replies from a script.
16#[derive(Clone, Debug, Default)]
17pub struct Fake {
18    replies: Vec<HttpResponse>,
19    /// Every request it was asked to send, in order.
20    sent: Vec<HttpRequest>,
21    /// Every duration it was asked to wait, in order.
22    waited: Vec<Duration>,
23    at: usize,
24}
25
26impl Fake {
27    /// A client with nothing to say yet.
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Answer the next request with this status and JSON body.
34    #[must_use]
35    pub fn reply(mut self, status: u16, body: &Value) -> Self {
36        self.replies.push(HttpResponse::json(status, body));
37        self
38    }
39
40    /// Answer the next request with exactly this response.
41    #[must_use]
42    pub fn reply_with(mut self, response: HttpResponse) -> Self {
43        self.replies.push(response);
44        self
45    }
46
47    /// Every request the run made, in order.
48    #[must_use]
49    pub fn sent(&self) -> &[HttpRequest] {
50        &self.sent
51    }
52
53    /// Every wait the run asked for, in order — the delays a `retry`
54    /// wanted, which a test can assert on without spending them.
55    #[must_use]
56    pub fn waited(&self) -> &[Duration] {
57        &self.waited
58    }
59
60    fn answer(&mut self, request: &HttpRequest) -> Result<HttpResponse, ClientError> {
61        self.sent.push(request.clone());
62        let reply = self.replies.get(self.at).cloned().ok_or_else(|| {
63            ClientError(format!(
64                "the script has {} replies, and this is request {}: {} {}",
65                self.replies.len(),
66                self.at + 1,
67                request.method,
68                request.url,
69            ))
70        })?;
71        self.at += 1;
72        Ok(reply)
73    }
74}
75
76impl HttpClient for Fake {
77    fn send(&mut self, request: &HttpRequest) -> Result<HttpResponse, ClientError> {
78        self.answer(request)
79    }
80}
81
82impl AsyncHttpClient for Fake {
83    fn send<'a>(&'a mut self, request: &'a HttpRequest) -> SendFuture<'a> {
84        let answer = self.answer(request);
85        Box::pin(std::future::ready(answer))
86    }
87
88    /// Returns at once: a test should not spend the delay it asserts on.
89    fn sleep(&self, _duration: Duration) -> SleepFuture<'_> {
90        Box::pin(std::future::ready(()))
91    }
92}
93
94/// A blocking [`HttpClient`] that records the waits asked of it instead
95/// of sleeping through them.
96///
97/// [`crate::execute`] sleeps on the calling thread, so a test that wants
98/// a retry's delay counted rather than spent drives [`crate::Run`]
99/// itself; this is the recorder it uses.
100impl Fake {
101    /// Note that the run asked to wait, without waiting.
102    pub fn note_wait(&mut self, duration: Duration) {
103        self.waited.push(duration);
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use serde_json::json;
111
112    fn request(url: &str) -> HttpRequest {
113        HttpRequest {
114            method: "GET".to_owned(),
115            url: url.to_owned(),
116            ..HttpRequest::default()
117        }
118    }
119
120    #[test]
121    fn replies_come_back_in_the_order_they_were_scripted() {
122        let mut fake = Fake::new()
123            .reply(200, &json!({ "first": true }))
124            .reply_with(HttpResponse {
125                status: 503,
126                headers: Vec::new(),
127                body: Vec::new(),
128            });
129        let first = HttpClient::send(&mut fake, &request("https://example.com/a")).unwrap();
130        assert_eq!(first.body_as_json(), Some(json!({ "first": true })));
131        let second = HttpClient::send(&mut fake, &request("https://example.com/b")).unwrap();
132        assert_eq!(second.status, 503);
133        assert_eq!(fake.sent().len(), 2);
134        assert_eq!(fake.sent()[1].url, "https://example.com/b");
135    }
136
137    #[test]
138    fn running_out_of_script_says_which_request_it_was() {
139        let mut fake = Fake::new();
140        let error = HttpClient::send(&mut fake, &request("https://example.com/a")).unwrap_err();
141        assert_eq!(
142            error.to_string(),
143            "the script has 0 replies, and this is request 1: GET https://example.com/a"
144        );
145    }
146
147    #[test]
148    fn the_async_client_answers_from_the_same_script() {
149        let mut fake = Fake::new().reply(204, &json!(null));
150        let response = futures_lite(AsyncHttpClient::send(
151            &mut fake,
152            &request("https://example.com/a"),
153        ));
154        assert_eq!(response.unwrap().status, 204);
155        futures_lite(AsyncHttpClient::sleep(&fake, Duration::from_secs(60)));
156        assert_eq!(fake.sent().len(), 1);
157    }
158
159    #[test]
160    fn a_wait_can_be_noted_rather_than_spent() {
161        let mut fake = Fake::new();
162        fake.note_wait(Duration::from_millis(1500));
163        assert_eq!(fake.waited(), [Duration::from_millis(1500)]);
164    }
165
166    /// Poll a future that is ready on the first poll — enough for a
167    /// client that never actually waits, and it keeps the tests free of
168    /// a runtime.
169    fn futures_lite<F: std::future::Future>(future: F) -> F::Output {
170        use std::pin::pin;
171        use std::task::{Context, Poll, Waker};
172        let mut future = pin!(future);
173        match future
174            .as_mut()
175            .poll(&mut Context::from_waker(Waker::noop()))
176        {
177            Poll::Ready(output) => output,
178            Poll::Pending => panic!("the fake client is never pending"),
179        }
180    }
181}