1mod dom;
6
7pub use dom::{FilterOption, Row, RowActions, field_error, filter_options, row_actions, rows};
8use http::header::{CONTENT_TYPE, COOKIE};
9use http_body_util::BodyExt;
10use topcoat::router::{Body, Router};
11
12#[derive(Clone)]
14pub struct TestClient<'a> {
15 router: &'a Router,
16 cookies: Vec<(String, String)>,
17 tenant: Option<uuid::Uuid>,
18}
19
20impl<'a> TestClient<'a> {
21 pub fn new(router: &'a Router) -> Self {
22 Self {
23 router,
24 cookies: Vec::new(),
25 tenant: None,
26 }
27 }
28
29 pub fn cookie(&self, name: &str, value: &str) -> Self {
32 let mut client = self.clone();
33 match client.cookies.iter_mut().find(|(kept, _)| kept == name) {
34 Some(existing) => existing.1 = value.to_string(),
35 None => client.cookies.push((name.to_string(), value.to_string())),
36 }
37 client
38 }
39
40 pub fn cookies(&self, cookies: &[(String, String)]) -> Self {
43 let mut client = self.clone();
44 for (name, value) in cookies {
45 match client.cookies.iter_mut().find(|(kept, _)| kept == name) {
46 Some(existing) => existing.1 = value.clone(),
47 None => client.cookies.push((name.clone(), value.clone())),
48 }
49 }
50 client
51 }
52
53 pub fn csrf(&self, token: &str) -> Self {
55 self.cookie(tablo_core::csrf::COOKIE_NAME, token)
56 }
57
58 pub fn tenant(&self, tenant: uuid::Uuid) -> Self {
61 let mut client = self.clone();
62 client.tenant = Some(tenant);
63 client
64 }
65
66 pub async fn get(&self, uri: &str) -> http::Response<Body> {
67 self.router
68 .handle(self.request(http::Method::GET, uri))
69 .await
70 }
71
72 pub async fn post_form(&self, uri: &str, body: String) -> http::Response<Body> {
73 let mut request = self.request(http::Method::POST, uri);
74 request.headers_mut().insert(
75 CONTENT_TYPE,
76 http::HeaderValue::from_static("application/x-www-form-urlencoded"),
77 );
78 *request.body_mut() = Body::from(body);
79 self.router.handle(request).await
80 }
81
82 pub async fn post_multipart(
83 &self,
84 uri: &str,
85 boundary: &str,
86 body: String,
87 ) -> http::Response<Body> {
88 let mut request = self.request(http::Method::POST, uri);
89 request.headers_mut().insert(
90 CONTENT_TYPE,
91 format!("multipart/form-data; boundary={boundary}")
92 .parse()
93 .expect("multipart content type"),
94 );
95 *request.body_mut() = Body::from(body);
96 self.router.handle(request).await
97 }
98
99 pub async fn post_json(&self, uri: &str, body: String, identity: &str) -> http::Response<Body> {
101 let mut request = self.request(http::Method::POST, uri);
102 request.headers_mut().insert(
103 CONTENT_TYPE,
104 http::HeaderValue::from_static("application/json"),
105 );
106 request.headers_mut().insert(
107 topcoat::router::request::IDENTITY_HEADER,
108 http::HeaderValue::from_str(identity).expect("identity header"),
109 );
110 *request.body_mut() = Body::from(body);
111 self.router.handle(request).await
112 }
113
114 fn request(&self, method: http::Method, uri: &str) -> http::Request<Body> {
115 let mut builder = http::Request::builder().method(method).uri(uri);
116 if let Some(jar) = cookie_header(
117 self.cookies
118 .iter()
119 .map(|(name, value)| (name.as_str(), value.as_str())),
120 ) {
121 builder = builder.header(COOKIE, jar);
122 }
123 let (mut parts, body) = builder.body(Body::empty()).unwrap().into_parts();
124 if let Some(tenant) = self.tenant {
125 parts.extensions.insert(tablo_core::Tenant(tenant));
126 }
127 http::Request::from_parts(parts, body)
128 }
129}
130
131pub fn cookie_header<'a>(cookies: impl IntoIterator<Item = (&'a str, &'a str)>) -> Option<String> {
133 let mut jar = String::new();
134 for (name, value) in cookies {
135 if !jar.is_empty() {
136 jar.push_str("; ");
137 }
138 jar.push_str(name);
139 jar.push('=');
140 jar.push_str(value);
141 }
142 (!jar.is_empty()).then_some(jar)
143}
144
145pub async fn body_bytes(response: http::Response<Body>) -> Vec<u8> {
147 response
148 .into_body()
149 .collect()
150 .await
151 .expect("collect body")
152 .to_bytes()
153 .to_vec()
154}
155
156pub async fn body_string(response: http::Response<Body>) -> String {
158 String::from_utf8_lossy(&body_bytes(response).await).into_owned()
159}
160
161pub fn form_body(pairs: &[(&str, &str)]) -> String {
163 let mut serializer = form_urlencoded::Serializer::new(String::new());
164 for (key, value) in pairs {
165 serializer.append_pair(key, value);
166 }
167 serializer.finish()
168}
169
170pub fn multipart_body(boundary: &str, parts: &[(&str, Option<&str>, &str)]) -> String {
173 let mut body = String::new();
174 for (name, filename, content) in parts {
175 body.push_str(&format!(
176 "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\""
177 ));
178 if let Some(filename) = filename {
179 body.push_str(&format!("; filename=\"{filename}\""));
180 }
181 body.push_str("\r\n\r\n");
182 body.push_str(content);
183 body.push_str("\r\n");
184 }
185 body.push_str(&format!("--{boundary}--\r\n"));
186 body
187}
188
189pub fn input_value(html: &str, name: &str) -> Option<String> {
192 let double = format!("name=\"{name}\"");
193 let single = format!("name='{name}'");
194 for tag in html.split('<').skip(1) {
195 if !tag.contains(&double) && !tag.contains(&single) {
196 continue;
197 }
198 let attrs = &tag[..tag.find('>')?];
199 for (prefix, term) in [("value=\"", '"'), ("value='", '\'')] {
200 if let Some(start) = attrs.find(prefix) {
201 let rest = &attrs[start + prefix.len()..];
202 if let Some(end) = rest.find(term) {
203 let raw = &rest[..end];
205 return Some(raw.replace(""", "\"").replace("'", "'"));
206 }
207 }
208 }
209 }
210 None
211}
212
213pub const SESSION_COOKIE: &str = "__Host-session";
215
216pub fn response_cookies(response: &http::Response<Body>) -> Vec<(String, String)> {
218 response
219 .headers()
220 .get_all(http::header::SET_COOKIE)
221 .iter()
222 .filter_map(|value| value.to_str().ok())
223 .filter_map(|value| value.split(';').next())
224 .filter_map(|pair| pair.split_once('='))
225 .map(|(name, value)| (name.trim().to_string(), value.trim().to_string()))
226 .collect()
227}
228
229pub fn set_cookie_header(response: &http::Response<Body>, name: &str) -> Option<String> {
231 response
232 .headers()
233 .get_all(http::header::SET_COOKIE)
234 .iter()
235 .filter_map(|value| value.to_str().ok())
236 .find(|value| value.starts_with(&format!("{name}=")))
237 .map(str::to_string)
238}
239
240pub fn session_cookie_value(response: &http::Response<Body>) -> Option<String> {
242 response_cookies(response)
243 .into_iter()
244 .find(|(name, _)| name == SESSION_COOKIE)
245 .map(|(_, value)| value)
246}