1use crate::app::{App, InProcess};
36use crate::request::Request;
37use bytes::Bytes;
38use http_body_util::BodyExt;
39use serde::{de::DeserializeOwned, Serialize};
40use std::collections::BTreeMap;
41use std::net::SocketAddr;
42use std::sync::{Arc, Mutex};
43
44#[derive(Clone)]
46pub struct TestClient {
47 server: Arc<InProcess>,
48 jar: Option<Arc<Mutex<BTreeMap<String, String>>>>,
49}
50
51impl TestClient {
52 pub fn new(app: App) -> Self {
53 Self {
54 server: Arc::new(app.into_in_process()),
55 jar: None,
56 }
57 }
58
59 pub fn with_cookies(mut self) -> Self {
61 self.jar = Some(Arc::default());
62 self
63 }
64
65 pub fn cookie(&self, name: &str) -> Option<String> {
67 self.jar
68 .as_ref()?
69 .lock()
70 .unwrap_or_else(|e| e.into_inner())
71 .get(name)
72 .cloned()
73 }
74
75 pub fn clear_cookies(&self) {
77 if let Some(jar) = &self.jar {
78 jar.lock().unwrap_or_else(|e| e.into_inner()).clear();
79 }
80 }
81
82 pub fn request(&self, method: &str, path: &str) -> TestRequest {
83 TestRequest {
84 client: self.clone(),
85 method: method.to_string(),
86 path: path.to_string(),
87 headers: Vec::new(),
88 body: Bytes::new(),
89 peer: ([127, 0, 0, 1], 50_000).into(),
90 secure: false,
91 }
92 }
93
94 pub fn get(&self, path: &str) -> TestRequest {
95 self.request("GET", path)
96 }
97 pub fn post(&self, path: &str) -> TestRequest {
98 self.request("POST", path)
99 }
100 pub fn put(&self, path: &str) -> TestRequest {
101 self.request("PUT", path)
102 }
103 pub fn patch(&self, path: &str) -> TestRequest {
104 self.request("PATCH", path)
105 }
106 pub fn delete(&self, path: &str) -> TestRequest {
107 self.request("DELETE", path)
108 }
109 pub fn head(&self, path: &str) -> TestRequest {
110 self.request("HEAD", path)
111 }
112 pub fn options(&self, path: &str) -> TestRequest {
113 self.request("OPTIONS", path)
114 }
115
116 fn remember(&self, headers: &http::HeaderMap) {
117 let Some(jar) = &self.jar else { return };
118 let mut jar = jar.lock().unwrap_or_else(|e| e.into_inner());
119 for value in headers.get_all(http::header::SET_COOKIE) {
120 let Ok(text) = value.to_str() else { continue };
121 let mut parts = text.split(';').map(str::trim);
122 let Some((name, val)) = parts.next().and_then(|nv| nv.split_once('=')) else {
123 continue;
124 };
125 let expired = parts.any(|attr| {
126 attr.eq_ignore_ascii_case("max-age=0")
127 || attr.to_ascii_lowercase().starts_with("max-age=-")
128 });
129 if expired || val.is_empty() {
130 jar.remove(name);
131 } else {
132 jar.insert(name.to_string(), val.to_string());
133 }
134 }
135 }
136}
137
138pub struct TestRequest {
140 client: TestClient,
141 method: String,
142 path: String,
143 headers: Vec<(String, String)>,
144 body: Bytes,
145 peer: SocketAddr,
146 secure: bool,
147}
148
149impl TestRequest {
150 pub fn header(mut self, name: &str, value: &str) -> Self {
151 self.headers.push((name.to_string(), value.to_string()));
152 self
153 }
154
155 pub fn bearer(self, token: &str) -> Self {
156 self.header("authorization", &format!("Bearer {token}"))
157 }
158
159 pub fn cookie(self, name: &str, value: &str) -> Self {
161 self.header("cookie", &format!("{name}={value}"))
162 }
163
164 pub fn json<T: Serialize>(mut self, value: &T) -> Self {
165 self.headers
166 .push(("content-type".into(), "application/json".into()));
167 self.body = Bytes::from(serde_json::to_vec(value).unwrap_or_default());
168 self
169 }
170
171 pub fn form<T: Serialize>(mut self, value: &T) -> Self {
173 self.headers.push((
174 "content-type".into(),
175 "application/x-www-form-urlencoded".into(),
176 ));
177 self.body = Bytes::from(serde_urlencoded::to_string(value).unwrap_or_default());
178 self
179 }
180
181 pub fn text(mut self, body: &str) -> Self {
182 self.headers
183 .push(("content-type".into(), "text/plain; charset=utf-8".into()));
184 self.body = Bytes::from(body.to_string());
185 self
186 }
187
188 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
189 self.body = body.into();
190 self
191 }
192
193 pub fn remote_addr(mut self, addr: impl Into<SocketAddr>) -> Self {
195 self.peer = addr.into();
196 self
197 }
198
199 pub fn https(mut self) -> Self {
201 self.secure = true;
202 self
203 }
204
205 pub async fn send(self) -> TestResponse {
206 let uri: http::Uri = self
207 .path
208 .parse()
209 .expect("test request path is not a valid URI");
210 let method = http::Method::from_bytes(self.method.as_bytes()).expect("invalid method");
211 let mut headers = http::HeaderMap::new();
212 headers.insert(
213 http::header::HOST,
214 http::HeaderValue::from_static("localhost"),
215 );
216 for (name, value) in &self.headers {
217 let (Ok(n), Ok(v)) = (
218 http::HeaderName::from_bytes(name.as_bytes()),
219 http::HeaderValue::from_str(value),
220 ) else {
221 panic!("invalid test header {name}: {value}");
222 };
223 headers.append(n, v);
224 }
225 if let Some(jar) = &self.client.jar {
226 let jar = jar.lock().unwrap_or_else(|e| e.into_inner());
227 if !jar.is_empty() {
228 let joined = jar
229 .iter()
230 .map(|(n, v)| format!("{n}={v}"))
231 .collect::<Vec<_>>()
232 .join("; ");
233 headers.append(
234 http::header::COOKIE,
235 http::HeaderValue::from_str(&joined).expect("cookie jar value"),
236 );
237 }
238 }
239 if !self.body.is_empty() {
240 headers.insert(http::header::CONTENT_LENGTH, self.body.len().into());
241 }
242
243 let request = Request::new(method, uri, headers, self.body);
244 let res = self
245 .client
246 .server
247 .call(request, self.peer, self.secure)
248 .await;
249 let (parts, body) = res.into_parts();
250 let body = tokio::time::timeout(std::time::Duration::from_secs(10), body.collect())
252 .await
253 .expect("response body did not finish within 10 seconds")
254 .map(|c| c.to_bytes())
255 .unwrap_or_default();
256 self.client.remember(&parts.headers);
257 TestResponse {
258 status: parts.status.as_u16(),
259 headers: parts.headers,
260 body,
261 }
262 }
263}
264
265#[derive(Debug, Clone)]
267pub struct TestResponse {
268 status: u16,
269 headers: http::HeaderMap,
270 body: Bytes,
271}
272
273impl TestResponse {
274 pub fn status(&self) -> u16 {
275 self.status
276 }
277
278 pub fn headers(&self) -> &http::HeaderMap {
279 &self.headers
280 }
281
282 pub fn header(&self, name: &str) -> &str {
284 self.headers
285 .get(name)
286 .and_then(|v| v.to_str().ok())
287 .unwrap_or("")
288 }
289
290 pub fn bytes(&self) -> &Bytes {
291 &self.body
292 }
293
294 pub fn text(&self) -> String {
295 String::from_utf8_lossy(&self.body).into_owned()
296 }
297
298 pub fn json<T: DeserializeOwned>(&self) -> T {
300 serde_json::from_slice(&self.body)
301 .unwrap_or_else(|e| panic!("response is not the expected JSON ({e}): {}", self.text()))
302 }
303}