1use crate::app::{App, Server};
4use crate::error::Result;
5use crate::request::Request;
6use crate::response::Response;
7use bytes::Bytes;
8use http::{HeaderMap, HeaderName, HeaderValue, Method};
9use std::collections::HashMap;
10use std::future::{Future, IntoFuture};
11use std::pin::Pin;
12use std::sync::{Arc, Mutex};
13
14pub type RequestHook = Arc<dyn Fn(&mut Request) + Send + Sync>;
16
17pub struct TestClient {
19 server: Server,
20 jar: Mutex<HashMap<String, String>>,
21 request_hooks: Mutex<Vec<RequestHook>>,
22}
23
24impl TestClient {
25 pub fn new(app: App) -> Result<Self> {
26 Ok(Self {
27 server: app.build()?,
28 jar: Mutex::new(HashMap::new()),
29 request_hooks: Mutex::new(Vec::new()),
30 })
31 }
32
33 pub fn tracked(app: App) -> Result<Self> {
35 Self::new(app)
36 }
37
38 pub fn server(&self) -> &Server {
39 &self.server
40 }
41
42 pub fn on_request<F>(&self, hook: F)
44 where
45 F: Fn(&mut Request) + Send + Sync + 'static,
46 {
47 self.request_hooks.lock().unwrap().push(Arc::new(hook));
48 }
49
50 pub fn clear_request_hooks(&self) {
52 self.request_hooks.lock().unwrap().clear();
53 }
54
55 pub fn get(&self, path: impl Into<String>) -> ClientRequest<'_> {
56 ClientRequest::new(self, Method::GET, path.into())
57 }
58
59 pub fn post(&self, path: impl Into<String>) -> ClientRequest<'_> {
60 ClientRequest::new(self, Method::POST, path.into())
61 }
62
63 pub fn put(&self, path: impl Into<String>) -> ClientRequest<'_> {
64 ClientRequest::new(self, Method::PUT, path.into())
65 }
66
67 pub fn patch(&self, path: impl Into<String>) -> ClientRequest<'_> {
68 ClientRequest::new(self, Method::PATCH, path.into())
69 }
70
71 pub fn delete(&self, path: impl Into<String>) -> ClientRequest<'_> {
72 ClientRequest::new(self, Method::DELETE, path.into())
73 }
74
75 fn cookie_header(&self) -> Option<String> {
76 let jar = self.jar.lock().unwrap();
77 if jar.is_empty() {
78 return None;
79 }
80 Some(
81 jar.iter()
82 .map(|(k, v)| format!("{k}={v}"))
83 .collect::<Vec<_>>()
84 .join("; "),
85 )
86 }
87
88 fn store_set_cookie(&self, res: &Response) {
89 let mut jar = self.jar.lock().unwrap();
90 for val in res.headers().get_all(http::header::SET_COOKIE) {
91 let Ok(raw) = val.to_str() else { continue };
92 let pair = raw.split(';').next().unwrap_or(raw).trim();
93 if let Some((name, value)) = pair.split_once('=') {
94 jar.insert(name.trim().to_string(), value.trim().to_string());
95 }
96 }
97 }
98
99 fn apply_hooks(&self, req: &mut Request) {
100 let hooks = self.request_hooks.lock().unwrap().clone();
101 for hook in hooks {
102 hook(req);
103 }
104 }
105}
106
107pub struct ClientRequest<'a> {
109 client: &'a TestClient,
110 method: Method,
111 path: String,
112 headers: HeaderMap,
113 body: Bytes,
114}
115
116impl<'a> ClientRequest<'a> {
117 fn new(client: &'a TestClient, method: Method, path: String) -> Self {
118 Self {
119 client,
120 method,
121 path,
122 headers: HeaderMap::new(),
123 body: Bytes::new(),
124 }
125 }
126
127 pub fn header(mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
128 if let (Ok(n), Ok(v)) = (
129 HeaderName::from_bytes(name.as_ref().as_bytes()),
130 HeaderValue::from_str(value.as_ref()),
131 ) {
132 self.headers.insert(n, v);
133 }
134 self
135 }
136
137 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
138 self.body = body.into();
139 self
140 }
141
142 pub fn form(mut self, pairs: &[(&str, &str)]) -> Self {
143 let encoded = serde_urlencoded::to_string(pairs).unwrap_or_default();
144 self.headers.insert(
145 http::header::CONTENT_TYPE,
146 HeaderValue::from_static("application/x-www-form-urlencoded"),
147 );
148 self.body = Bytes::from(encoded);
149 self
150 }
151
152 pub fn json<T: serde::Serialize>(mut self, value: &T) -> Self {
153 let bytes = serde_json::to_vec(value).unwrap_or_default();
154 self.headers.insert(
155 http::header::CONTENT_TYPE,
156 HeaderValue::from_static("application/json"),
157 );
158 self.body = Bytes::from(bytes);
159 self
160 }
161
162 async fn dispatch(self) -> Response {
163 let mut builder = Request::builder()
164 .method(self.method)
165 .path(self.path)
166 .body(self.body);
167 for (k, v) in self.headers.iter() {
168 if let Ok(s) = v.to_str() {
169 builder = builder.header(k.as_str(), s);
170 }
171 }
172 if let Some(cookie) = self.client.cookie_header() {
173 builder = builder.header("cookie", cookie);
174 }
175 let mut req = builder.build();
176 self.client.apply_hooks(&mut req);
177 let res = self.client.server.handle(req).await;
178 self.client.store_set_cookie(&res);
179 res
180 }
181}
182
183impl<'a> IntoFuture for ClientRequest<'a> {
184 type Output = Response;
185 type IntoFuture = Pin<Box<dyn Future<Output = Response> + Send + 'a>>;
186
187 fn into_future(self) -> Self::IntoFuture {
188 Box::pin(self.dispatch())
189 }
190}
191
192pub trait ResponseAssert {
194 fn assert_status(&self, code: u16) -> &Self;
196
197 fn json<T: serde::de::DeserializeOwned>(&self) -> T;
199
200 fn json_value(&self) -> serde_json::Value;
202}
203
204impl ResponseAssert for Response {
205 fn assert_status(&self, code: u16) -> &Self {
206 let got = self.status_code().as_u16();
207 assert_eq!(
208 got,
209 code,
210 "unexpected status {got}, body: {:?}",
211 self.body_bytes().map(|b| String::from_utf8_lossy(b).into_owned())
212 );
213 self
214 }
215
216 fn json<T: serde::de::DeserializeOwned>(&self) -> T {
217 let bytes = self
218 .body_bytes()
219 .unwrap_or_else(|| panic!("response body is not buffered"));
220 serde_json::from_slice(bytes).unwrap_or_else(|e| {
221 panic!(
222 "json decode failed: {e}; body: {}",
223 String::from_utf8_lossy(bytes)
224 )
225 })
226 }
227
228 fn json_value(&self) -> serde_json::Value {
229 self.json()
230 }
231}