1mod protocol;
5
6use http::header::{CONTENT_TYPE, COOKIE};
7pub use protocol::{
8 EmptyTable, ErrorKind, FilterOption, Row, RowActions, SESSION_COOKIE, body_bytes, body_string,
9 confirms_first, cookie_header, empty_table, field_error, field_error_message, filter_options,
10 find_href_with, form_body, input_value, multipart_body, notification, response_cookies,
11 row_actions, row_keys, rows, session_cookie_value, set_cookie_header,
12};
13use tablo_core::{DeclarationError, MountError, Panel, PanelUser, RouterBuilderPanelExt};
14use topcoat::router::{Body, Router, RouterBuilderDiscoverExt};
15
16pub fn mount(db: toasty::Db, panel: Panel) -> topcoat::Result<Router> {
23 Ok(Router::builder()
24 .discover()
25 .app_context(db)
26 .panel(panel)?
27 .build())
28}
29
30pub fn refusal<T>(mounted: topcoat::Result<T>) -> Vec<DeclarationError> {
49 let Err(error) = mounted else {
50 panic!("the panel must not mount");
51 };
52 error
53 .downcast_ref::<MountError>()
54 .unwrap_or_else(|| panic!("a declaration mistake refuses the panel, got {error}"))
55 .errors()
56 .to_vec()
57}
58
59#[derive(Clone)]
61pub struct TestClient<'a> {
62 router: &'a Router,
63 cookies: Vec<(String, String)>,
64 tenant: Option<uuid::Uuid>,
65}
66
67impl<'a> TestClient<'a> {
68 pub fn new(router: &'a Router) -> Self {
69 Self {
70 router,
71 cookies: Vec::new(),
72 tenant: None,
73 }
74 }
75
76 pub fn cookie(&self, name: &str, value: &str) -> Self {
79 self.with_cookies(&[(name.to_string(), value.to_string())])
80 }
81
82 pub fn cookies(&self, cookies: &[(String, String)]) -> Self {
85 self.with_cookies(cookies)
86 }
87
88 fn with_cookies(&self, cookies: &[(String, String)]) -> Self {
89 let mut client = self.clone();
90 for (name, value) in cookies {
91 match client.cookies.iter_mut().find(|(kept, _)| kept == name) {
92 Some(existing) => existing.1 = value.clone(),
93 None => client.cookies.push((name.clone(), value.clone())),
94 }
95 }
96 client
97 }
98
99 pub fn csrf(&self, token: &str) -> Self {
101 self.cookie(tablo_core::csrf::COOKIE_NAME, token)
102 }
103
104 pub async fn sign_in(&self, db: &toasty::Db, panel: &str, user: &impl PanelUser) -> Self {
129 let token = tablo_core::auth::mint_session(&mut db.clone(), panel, user)
130 .await
131 .expect("record the test session");
132 self.cookie(SESSION_COOKIE, &token)
133 }
134
135 pub async fn log_in(
147 &self,
148 panel: &str,
149 email: &str,
150 password: &str,
151 next: &str,
152 ) -> (Self, http::Response<Body>) {
153 let url = format!("/{panel}/login");
154 let page = self.get(&url).await;
155 let cookies = response_cookies(&page);
156 let html = body_string(page).await;
157 let csrf = input_value(&html, "csrf_token")
158 .unwrap_or_else(|| panic!("the login page embeds a csrf_token input: {html}"));
159 let body = form_body(&[
160 ("email", email),
161 ("password", password),
162 ("next", next),
163 ("csrf_token", &csrf),
164 ]);
165 let client = self.cookies(&cookies);
166 let response = client.post_form(&url, body).await;
167 let session = response_cookies(&response);
168 (client.cookies(&session), response)
169 }
170
171 pub fn tenant(&self, tenant: uuid::Uuid) -> Self {
174 let mut client = self.clone();
175 client.tenant = Some(tenant);
176 client
177 }
178
179 pub async fn get(&self, uri: &str) -> http::Response<Body> {
180 self.router
181 .handle(self.request(http::Method::GET, uri))
182 .await
183 }
184
185 pub async fn post_form(&self, uri: &str, body: String) -> http::Response<Body> {
186 self.post(
187 uri,
188 http::HeaderValue::from_static("application/x-www-form-urlencoded"),
189 body,
190 )
191 .await
192 }
193
194 pub async fn submit(&self, uri: &str, body: &str) -> http::Response<Body> {
197 let token = Self::fresh_csrf_token();
198 let field = form_body(&[("csrf_token", &token)]);
199 let body = if body.is_empty() {
200 field
201 } else {
202 format!("{body}&{field}")
203 };
204 self.csrf(&token).post_form(uri, body).await
205 }
206
207 pub async fn submit_multipart(
210 &self,
211 uri: &str,
212 parts: &[(&str, Option<&str>, &str)],
213 ) -> http::Response<Body> {
214 const BOUNDARY: &str = "tablo-test-boundary";
215 let token = Self::fresh_csrf_token();
216 let mut parts = parts.to_vec();
217 parts.push(("csrf_token", None, &token));
218 let body = multipart_body(BOUNDARY, &parts);
219 self.csrf(&token).post_multipart(uri, BOUNDARY, body).await
220 }
221
222 pub async fn post_multipart(
223 &self,
224 uri: &str,
225 boundary: &str,
226 body: String,
227 ) -> http::Response<Body> {
228 self.post(
229 uri,
230 format!("multipart/form-data; boundary={boundary}")
231 .parse()
232 .expect("multipart content type"),
233 body,
234 )
235 .await
236 }
237
238 async fn post(
239 &self,
240 uri: &str,
241 content_type: http::HeaderValue,
242 body: String,
243 ) -> http::Response<Body> {
244 let mut request = self.request(http::Method::POST, uri);
245 request.headers_mut().insert(CONTENT_TYPE, content_type);
246 *request.body_mut() = Body::from(body);
247 self.router.handle(request).await
248 }
249
250 fn fresh_csrf_token() -> String {
251 uuid::Uuid::new_v4().to_string()
252 }
253
254 fn request(&self, method: http::Method, uri: &str) -> http::Request<Body> {
255 let mut builder = http::Request::builder().method(method).uri(uri);
256 if let Some(jar) = cookie_header(
257 self.cookies
258 .iter()
259 .map(|(name, value)| (name.as_str(), value.as_str())),
260 ) {
261 builder = builder.header(COOKIE, jar);
262 }
263 let (mut parts, body) = builder.body(Body::empty()).unwrap().into_parts();
264 if let Some(tenant) = self.tenant {
265 parts.extensions.insert(tablo_core::Tenant(tenant));
266 }
267 http::Request::from_parts(parts, body)
268 }
269}