mod protocol;
use http::header::{CONTENT_TYPE, COOKIE};
pub use protocol::{
EmptyTable, ErrorKind, FilterOption, Row, RowActions, SESSION_COOKIE, body_bytes, body_string,
confirms_first, cookie_header, empty_table, field_error, field_error_message, filter_options,
find_href_with, form_body, input_value, multipart_body, notification, response_cookies,
row_actions, row_keys, rows, session_cookie_value, set_cookie_header,
};
use tablo_core::{DeclarationError, MountError, Panel, PanelUser, RouterBuilderPanelExt};
use topcoat::router::{Body, Router, RouterBuilderDiscoverExt};
pub fn mount(db: toasty::Db, panel: Panel) -> topcoat::Result<Router> {
Ok(Router::builder()
.discover()
.app_context(db)
.panel(panel)?
.build())
}
pub fn refusal<T>(mounted: topcoat::Result<T>) -> Vec<DeclarationError> {
let Err(error) = mounted else {
panic!("the panel must not mount");
};
error
.downcast_ref::<MountError>()
.unwrap_or_else(|| panic!("a declaration mistake refuses the panel, got {error}"))
.errors()
.to_vec()
}
#[derive(Clone)]
pub struct TestClient<'a> {
router: &'a Router,
cookies: Vec<(String, String)>,
tenant: Option<uuid::Uuid>,
}
impl<'a> TestClient<'a> {
pub fn new(router: &'a Router) -> Self {
Self {
router,
cookies: Vec::new(),
tenant: None,
}
}
pub fn cookie(&self, name: &str, value: &str) -> Self {
self.with_cookies(&[(name.to_string(), value.to_string())])
}
pub fn cookies(&self, cookies: &[(String, String)]) -> Self {
self.with_cookies(cookies)
}
fn with_cookies(&self, cookies: &[(String, String)]) -> Self {
let mut client = self.clone();
for (name, value) in cookies {
match client.cookies.iter_mut().find(|(kept, _)| kept == name) {
Some(existing) => existing.1 = value.clone(),
None => client.cookies.push((name.clone(), value.clone())),
}
}
client
}
pub fn csrf(&self, token: &str) -> Self {
self.cookie(tablo_core::csrf::COOKIE_NAME, token)
}
pub async fn sign_in(&self, db: &toasty::Db, panel: &str, user: &impl PanelUser) -> Self {
let token = tablo_core::auth::mint_session(&mut db.clone(), panel, user)
.await
.expect("record the test session");
self.cookie(SESSION_COOKIE, &token)
}
pub async fn log_in(
&self,
panel: &str,
email: &str,
password: &str,
next: &str,
) -> (Self, http::Response<Body>) {
let url = format!("/{panel}/login");
let page = self.get(&url).await;
let cookies = response_cookies(&page);
let html = body_string(page).await;
let csrf = input_value(&html, "csrf_token")
.unwrap_or_else(|| panic!("the login page embeds a csrf_token input: {html}"));
let body = form_body(&[
("email", email),
("password", password),
("next", next),
("csrf_token", &csrf),
]);
let client = self.cookies(&cookies);
let response = client.post_form(&url, body).await;
let session = response_cookies(&response);
(client.cookies(&session), response)
}
pub fn tenant(&self, tenant: uuid::Uuid) -> Self {
let mut client = self.clone();
client.tenant = Some(tenant);
client
}
pub async fn get(&self, uri: &str) -> http::Response<Body> {
self.router
.handle(self.request(http::Method::GET, uri))
.await
}
pub async fn post_form(&self, uri: &str, body: String) -> http::Response<Body> {
self.post(
uri,
http::HeaderValue::from_static("application/x-www-form-urlencoded"),
body,
)
.await
}
pub async fn submit(&self, uri: &str, body: &str) -> http::Response<Body> {
let token = Self::fresh_csrf_token();
let field = form_body(&[("csrf_token", &token)]);
let body = if body.is_empty() {
field
} else {
format!("{body}&{field}")
};
self.csrf(&token).post_form(uri, body).await
}
pub async fn submit_multipart(
&self,
uri: &str,
parts: &[(&str, Option<&str>, &str)],
) -> http::Response<Body> {
const BOUNDARY: &str = "tablo-test-boundary";
let token = Self::fresh_csrf_token();
let mut parts = parts.to_vec();
parts.push(("csrf_token", None, &token));
let body = multipart_body(BOUNDARY, &parts);
self.csrf(&token).post_multipart(uri, BOUNDARY, body).await
}
pub async fn post_multipart(
&self,
uri: &str,
boundary: &str,
body: String,
) -> http::Response<Body> {
self.post(
uri,
format!("multipart/form-data; boundary={boundary}")
.parse()
.expect("multipart content type"),
body,
)
.await
}
async fn post(
&self,
uri: &str,
content_type: http::HeaderValue,
body: String,
) -> http::Response<Body> {
let mut request = self.request(http::Method::POST, uri);
request.headers_mut().insert(CONTENT_TYPE, content_type);
*request.body_mut() = Body::from(body);
self.router.handle(request).await
}
fn fresh_csrf_token() -> String {
uuid::Uuid::new_v4().to_string()
}
fn request(&self, method: http::Method, uri: &str) -> http::Request<Body> {
let mut builder = http::Request::builder().method(method).uri(uri);
if let Some(jar) = cookie_header(
self.cookies
.iter()
.map(|(name, value)| (name.as_str(), value.as_str())),
) {
builder = builder.header(COOKIE, jar);
}
let (mut parts, body) = builder.body(Body::empty()).unwrap().into_parts();
if let Some(tenant) = self.tenant {
parts.extensions.insert(tablo_core::Tenant(tenant));
}
http::Request::from_parts(parts, body)
}
}