Skip to main content

tablo_test/
lib.rs

1//! Drives a built `Router` without a socket, carrying cookies, CSRF token, and
2//! tenant. Body and HTML helpers live in `tablo_core::protocol` and are
3//! re-exported here.
4
5use http::header::{CONTENT_TYPE, COOKIE};
6pub use tablo_core::protocol::{
7    FilterOption, Row, RowActions, SESSION_COOKIE, body_bytes, body_string, cookie_header,
8    field_error, filter_options, form_body, input_value, multipart_body, response_cookies,
9    row_actions, rows, session_cookie_value, set_cookie_header,
10};
11use topcoat::router::{Body, Router};
12
13/// Sends requests carrying cookies and tenant.
14#[derive(Clone)]
15pub struct TestClient<'a> {
16    router: &'a Router,
17    cookies: Vec<(String, String)>,
18    tenant: Option<uuid::Uuid>,
19}
20
21impl<'a> TestClient<'a> {
22    pub fn new(router: &'a Router) -> Self {
23        Self {
24            router,
25            cookies: Vec::new(),
26            tenant: None,
27        }
28    }
29
30    /// Attaches a cookie to every request; a later value for the same name
31    /// replaces the earlier one.
32    pub fn cookie(&self, name: &str, value: &str) -> Self {
33        let mut client = self.clone();
34        match client.cookies.iter_mut().find(|(kept, _)| kept == name) {
35            Some(existing) => existing.1 = value.to_string(),
36            None => client.cookies.push((name.to_string(), value.to_string())),
37        }
38        client
39    }
40
41    /// Attaches every `(name, value)` pair, replacing earlier values for the
42    /// same name.
43    pub fn cookies(&self, cookies: &[(String, String)]) -> Self {
44        let mut client = self.clone();
45        for (name, value) in cookies {
46            match client.cookies.iter_mut().find(|(kept, _)| kept == name) {
47                Some(existing) => existing.1 = value.clone(),
48                None => client.cookies.push((name.clone(), value.clone())),
49            }
50        }
51        client
52    }
53
54    /// Attach the CSRF cookie the form's `csrf_token` field must match.
55    pub fn csrf(&self, token: &str) -> Self {
56        self.cookie(tablo_core::csrf::COOKIE_NAME, token)
57    }
58
59    /// Scopes the request to another tenant, taking precedence over the
60    /// logged-in user's tenant.
61    pub fn tenant(&self, tenant: uuid::Uuid) -> Self {
62        let mut client = self.clone();
63        client.tenant = Some(tenant);
64        client
65    }
66
67    pub async fn get(&self, uri: &str) -> http::Response<Body> {
68        self.router
69            .handle(self.request(http::Method::GET, uri))
70            .await
71    }
72
73    pub async fn post_form(&self, uri: &str, body: String) -> http::Response<Body> {
74        let mut request = self.request(http::Method::POST, uri);
75        request.headers_mut().insert(
76            CONTENT_TYPE,
77            http::HeaderValue::from_static("application/x-www-form-urlencoded"),
78        );
79        *request.body_mut() = Body::from(body);
80        self.router.handle(request).await
81    }
82
83    pub async fn post_multipart(
84        &self,
85        uri: &str,
86        boundary: &str,
87        body: String,
88    ) -> http::Response<Body> {
89        let mut request = self.request(http::Method::POST, uri);
90        request.headers_mut().insert(
91            CONTENT_TYPE,
92            format!("multipart/form-data; boundary={boundary}")
93                .parse()
94                .expect("multipart content type"),
95        );
96        *request.body_mut() = Body::from(body);
97        self.router.handle(request).await
98    }
99
100    /// Posts JSON with the page identity header the browser runtime sends.
101    pub async fn post_json(&self, uri: &str, body: String, identity: &str) -> http::Response<Body> {
102        let mut request = self.request(http::Method::POST, uri);
103        request.headers_mut().insert(
104            CONTENT_TYPE,
105            http::HeaderValue::from_static("application/json"),
106        );
107        request.headers_mut().insert(
108            topcoat::router::request::IDENTITY_HEADER,
109            http::HeaderValue::from_str(identity).expect("identity header"),
110        );
111        *request.body_mut() = Body::from(body);
112        self.router.handle(request).await
113    }
114
115    fn request(&self, method: http::Method, uri: &str) -> http::Request<Body> {
116        let mut builder = http::Request::builder().method(method).uri(uri);
117        if let Some(jar) = cookie_header(
118            self.cookies
119                .iter()
120                .map(|(name, value)| (name.as_str(), value.as_str())),
121        ) {
122            builder = builder.header(COOKIE, jar);
123        }
124        let (mut parts, body) = builder.body(Body::empty()).unwrap().into_parts();
125        if let Some(tenant) = self.tenant {
126            parts.extensions.insert(tablo_core::Tenant(tenant));
127        }
128        http::Request::from_parts(parts, body)
129    }
130}