1use std::sync::Arc;
20
21use serde::de::DeserializeOwned;
22use serde::Serialize;
23use tower::ServiceExt;
24use wabot_feature_rest_controller::axum::body::Body;
25use wabot_feature_rest_controller::axum::http::{HeaderMap, Request, StatusCode};
26use wabot_feature_rest_controller::axum::Router;
27use wabot_feature_rest_controller::rest_app;
28
29#[derive(Clone)]
40pub struct RestHarness {
41 router: Router,
42 default_headers: Arc<Vec<(String, String)>>,
45}
46
47impl RestHarness {
48 pub fn new(router: Router) -> Self {
49 Self {
50 router,
51 default_headers: Arc::new(Vec::new()),
52 }
53 }
54
55 pub fn with_header(&self, name: impl Into<String>, value: impl Into<String>) -> Self {
62 let mut headers = (*self.default_headers).clone();
63 headers.push((name.into(), value.into()));
64 Self {
65 router: self.router.clone(),
66 default_headers: Arc::new(headers),
67 }
68 }
69
70 pub fn with_bearer(&self, token: impl std::fmt::Display) -> Self {
72 self.with_header("authorization", format!("Bearer {token}"))
73 }
74
75 pub fn with_cookie(&self, name: &str, value: impl std::fmt::Display) -> Self {
78 self.with_header("cookie", format!("{name}={value}"))
79 }
80
81 pub fn get(&self, path: &str) -> RequestBuilder {
82 self.request("GET", path)
83 }
84 pub fn post(&self, path: &str) -> RequestBuilder {
85 self.request("POST", path)
86 }
87 pub fn put(&self, path: &str) -> RequestBuilder {
88 self.request("PUT", path)
89 }
90 pub fn delete(&self, path: &str) -> RequestBuilder {
91 self.request("DELETE", path)
92 }
93
94 pub fn request(&self, method: &str, path: &str) -> RequestBuilder {
95 RequestBuilder {
96 router: self.router.clone(),
97 method: method.to_string(),
98 path: path.to_string(),
99 query: Vec::new(),
100 headers: (*self.default_headers).clone(),
101 body: None,
102 }
103 }
104}
105
106pub struct RequestBuilder {
107 router: Router,
108 method: String,
109 path: String,
110 query: Vec<(String, String)>,
111 headers: Vec<(String, String)>,
112 body: Option<String>,
113}
114
115impl RequestBuilder {
116 pub fn json<T: Serialize>(mut self, body: &T) -> Self {
118 self.body = Some(serde_json::to_string(body).expect("a serializable body"));
119 self
120 }
121
122 pub fn body(mut self, body: impl Into<String>) -> Self {
125 self.body = Some(body.into());
126 self
127 }
128
129 pub fn query(mut self, key: &str, value: impl std::fmt::Display) -> Self {
130 self.query.push((key.into(), value.to_string()));
131 self
132 }
133
134 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
135 self.headers.push((name.into(), value.into()));
136 self
137 }
138
139 pub fn bearer(self, token: impl std::fmt::Display) -> Self {
140 self.header("authorization", format!("Bearer {token}"))
141 }
142
143 pub async fn send(self) -> TestResponse {
151 let mut uri = self.path.clone();
152 if !self.query.is_empty() {
153 let encoded: Vec<String> = self
154 .query
155 .iter()
156 .map(|(k, v)| format!("{}={}", encode(k), encode(v)))
157 .collect();
158 uri = format!("{uri}?{}", encoded.join("&"));
159 }
160
161 let mut request = Request::builder().method(self.method.as_str()).uri(&uri);
162 let has_content_type = self
163 .headers
164 .iter()
165 .any(|(name, _)| name.eq_ignore_ascii_case("content-type"));
166 for (name, value) in &self.headers {
167 request = request.header(name, value);
168 }
169 if self.body.is_some() && !has_content_type {
170 request = request.header("content-type", "application/json");
171 }
172
173 let request = request
174 .body(self.body.map(Body::from).unwrap_or_else(Body::empty))
175 .expect("a valid request");
176
177 let response = rest_app(self.router)
178 .oneshot(request)
179 .await
180 .expect("the service should not fail outright");
181
182 let status = response.status();
183 let headers = response.headers().clone();
184 let bytes =
185 wabot_feature_rest_controller::axum::body::to_bytes(response.into_body(), usize::MAX)
186 .await
187 .expect("a readable body");
188
189 TestResponse {
190 status,
191 headers,
192 body: String::from_utf8_lossy(&bytes).into_owned(),
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct TestResponse {
200 pub status: StatusCode,
201 pub headers: HeaderMap,
202 pub body: String,
204}
205
206impl TestResponse {
207 pub fn json<T: DeserializeOwned>(&self) -> T {
215 serde_json::from_str(&self.body).unwrap_or_else(|error| {
216 panic!(
217 "expected a {} body, got HTTP {} with {:?} ({error})",
218 std::any::type_name::<T>(),
219 self.status,
220 self.body
221 )
222 })
223 }
224
225 pub fn value(&self) -> serde_json::Value {
227 self.json()
228 }
229
230 pub fn header(&self, name: &str) -> Option<&str> {
231 self.headers.get(name).and_then(|v| v.to_str().ok())
232 }
233
234 pub fn is_success(&self) -> bool {
235 self.status.is_success()
236 }
237
238 pub fn assert_status(&self, expected: StatusCode) -> &Self {
241 assert_eq!(
242 self.status, expected,
243 "expected HTTP {expected}, got {} with body {:?}",
244 self.status, self.body
245 );
246 self
247 }
248
249 pub fn assert_ok(&self) -> &Self {
250 self.assert_status(StatusCode::OK)
251 }
252}
253
254fn encode(value: &str) -> String {
257 let mut out = String::with_capacity(value.len());
258 for byte in value.bytes() {
259 match byte {
260 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
261 out.push(byte as char)
262 }
263 b' ' => out.push_str("%20"),
264 other => out.push_str(&format!("%{other:02X}")),
265 }
266 }
267 out
268}