1use alux_http::{HttpMethod, HttpStatus};
7use alux_http_parts::{DirectRequest, DirectResponse};
8use core::future::Future;
9
10pub trait AnswerAlg {
15 fn answer(&self, request: DirectRequest) -> impl Future<Output = DirectResponse> + Send;
17}
18
19#[derive(Debug, Clone)]
21pub struct Exchange {
22 pub what: &'static str,
24 pub request: DirectRequest,
26 pub status: HttpStatus,
28 pub body: Option<&'static str>,
30 pub header: Option<(&'static str, &'static str)>,
32}
33
34impl Exchange {
35 fn asked(what: &'static str, method: HttpMethod, path: &str, status: HttpStatus) -> Self {
37 Self { what, request: DirectRequest::new(method, path), status, body: None, header: None }
38 }
39
40 #[must_use]
42 fn sending(mut self, name: &str, value: &str) -> Self {
43 self.request = self.request.with_header(name, value);
44 self
45 }
46
47 fn sent(
49 what: &'static str,
50 method: HttpMethod,
51 path: &str,
52 content_type: &str,
53 body: &'static str,
54 status: HttpStatus,
55 ) -> Self {
56 let request = DirectRequest::new(method, path).with_header("content-type", content_type).with_body(body);
57
58 Self { what, request, status, body: None, header: None }
59 }
60
61 #[must_use]
63 fn answering(mut self, body: &'static str) -> Self {
64 self.body = Some(body);
65 self
66 }
67
68 #[must_use]
70 fn carrying(mut self, name: &'static str, value: &'static str) -> Self {
71 self.header = Some((name, value));
72 self
73 }
74
75 fn disagreements(&self, answer: &DirectResponse) -> Vec<String> {
77 let mut found = Vec::new();
78 let what = self.what;
79 if answer.status() != self.status {
80 found.push(format!("{what}: answered {} where {} was stated", answer.status().code(), self.status.code()));
81 }
82 if let Some(body) = self.body
83 && answer.text() != body
84 {
85 found.push(format!("{what}: answered `{}` where `{body}` was stated", answer.text()));
86 }
87 if let Some((name, value)) = self.header
88 && answer.header(name) != Some(value)
89 {
90 found.push(format!(
91 "{what}: answered `{name}: {}` where `{name}: {value}` was stated",
92 answer.header(name).unwrap_or("nothing")
93 ));
94 }
95
96 found
97 }
98}
99
100pub fn exchanges() -> Vec<Exchange> {
105 vec![
106 Exchange::asked("a reading as it stands", HttpMethod::Get, "/items", HttpStatus::OK).answering("[7]"),
107 Exchange::asked("a path binding one segment", HttpMethod::Get, "/item/1", HttpStatus::OK).answering("7"),
108 Exchange::asked("what a failure means", HttpMethod::Get, "/item/9", HttpStatus::NOT_FOUND)
109 .answering("no such reading"),
110 Exchange::sent(
111 "a body read as a document, under a declared status",
112 HttpMethod::Post,
113 "/items",
114 "application/json",
115 "7",
116 HttpStatus::new(201),
117 )
118 .answering("7"),
119 Exchange::sent(
120 "a body read as a form",
121 HttpMethod::Put,
122 "/items",
123 "application/x-www-form-urlencoded",
124 "value=7",
125 HttpStatus::OK,
126 )
127 .answering("7"),
128 Exchange::sent(
129 "a body taken as it arrived",
130 HttpMethod::Patch,
131 "/items",
132 "text/plain",
133 "seven",
134 HttpStatus::OK,
135 )
136 .answering("noted seven"),
137 Exchange::asked("an answer with no body", HttpMethod::Delete, "/items", HttpStatus::NO_CONTENT).answering(""),
138 Exchange::asked("a redirect", HttpMethod::Get, "/home", HttpStatus::SEE_OTHER).carrying("location", "/items"),
139 Exchange::asked("a page", HttpMethod::Get, "/page", HttpStatus::OK)
140 .answering("<p>7</p>")
141 .carrying("content-type", "text/html; charset=utf-8"),
142 Exchange::asked("the readings as stored", HttpMethod::Get, "/stored", HttpStatus::OK).answering("seven"),
143 Exchange::asked("what the cookies a caller sent state", HttpMethod::Get, "/session", HttpStatus::OK)
144 .sending("cookie", "session=abc; theme=dark")
145 .answering("known as abc"),
146 Exchange::asked("a header an answer carries", HttpMethod::Get, "/cached", HttpStatus::OK)
148 .answering("[7]")
149 .carrying("cache-control", "max-age=60"),
150 Exchange::asked("what the headers a caller sent state", HttpMethod::Get, "/agent", HttpStatus::OK)
152 .sending("user-agent", "probe")
153 .answering("sent by probe"),
154 ]
155}
156
157pub async fn expect<Answers>(answers: &Answers) -> Result<(), Vec<String>>
166where
167 Answers: AnswerAlg,
168{
169 let mut found = Vec::new();
170 for exchange in exchanges() {
171 let answer = answers.answer(exchange.request.clone()).await.collected().await;
174 found.extend(exchange.disagreements(&answer));
175 }
176
177 if found.is_empty() { Ok(()) } else { Err(found) }
178}
179
180pub fn stream_exchanges() -> Vec<Exchange> {
182 vec![
183 Exchange::asked("a body produced a piece at a time", HttpMethod::Get, "/ticks", HttpStatus::OK)
184 .answering("onetwothree"),
185 ]
186}
187
188pub async fn expect_streaming<Answers>(answers: &Answers) -> Result<(), Vec<String>>
194where
195 Answers: AnswerAlg,
196{
197 let mut found = Vec::new();
198 for exchange in stream_exchanges() {
199 let answer = answers.answer(exchange.request.clone()).await.collected().await;
200 found.extend(exchange.disagreements(&answer));
201 }
202
203 if found.is_empty() { Ok(()) } else { Err(found) }
204}
205
206const BOUNDARY: &str = "alux";
208const PARTS: &str = "--alux\r\nContent-Disposition: form-data; name=\"one\"\r\n\r\n1\r\n\
209--alux\r\nContent-Disposition: form-data; name=\"two\"\r\n\r\n2\r\n--alux--\r\n";
210
211pub fn multipart_exchanges() -> Vec<Exchange> {
213 vec![
214 Exchange::sent(
215 "a body arriving as parts",
216 HttpMethod::Post,
217 "/upload",
218 &format!("multipart/form-data; boundary={BOUNDARY}"),
219 PARTS,
220 HttpStatus::OK,
221 )
222 .answering("one=1,two=2"),
223 ]
224}
225
226pub async fn expect_multipart<Answers>(answers: &Answers) -> Result<(), Vec<String>>
232where
233 Answers: AnswerAlg,
234{
235 let mut found = Vec::new();
236 for exchange in multipart_exchanges() {
237 let answer = answers.answer(exchange.request.clone()).await.collected().await;
238 found.extend(exchange.disagreements(&answer));
239 }
240
241 if found.is_empty() { Ok(()) } else { Err(found) }
242}