use crate::{Request, Response, Router};
use hyper::{Body, Method, Uri};
use std::collections::HashMap;
pub struct TestClient {
router: Router,
default_headers: HashMap<String, String>,
}
impl TestClient {
pub fn new(router: Router) -> Self {
Self {
router,
default_headers: HashMap::new(),
}
}
pub fn with_auth(mut self, token: &str) -> Self {
self.default_headers.insert("authorization".to_string(), format!("Bearer {}", token));
self
}
pub fn with_session(mut self, session_id: &str) -> Self {
self.default_headers.insert("cookie".to_string(), format!("yaiko_session={}", session_id));
self
}
pub fn with_header(mut self, key: &str, value: &str) -> Self {
self.default_headers.insert(key.to_string(), value.to_string());
self
}
pub async fn get(&self, path: &str) -> TestResponse {
self.request(Method::GET, path, None, HashMap::new()).await
}
pub async fn post(&self, path: &str, body: impl Into<String>) -> TestResponse {
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
self.request(Method::POST, path, Some(body.into()), headers).await
}
pub async fn put(&self, path: &str, body: impl Into<String>) -> TestResponse {
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
self.request(Method::PUT, path, Some(body.into()), headers).await
}
pub async fn patch(&self, path: &str, body: impl Into<String>) -> TestResponse {
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
self.request(Method::PATCH, path, Some(body.into()), headers).await
}
pub async fn delete(&self, path: &str) -> TestResponse {
self.request(Method::DELETE, path, None, HashMap::new()).await
}
pub async fn post_form(&self, path: &str, data: &HashMap<String, String>) -> TestResponse {
let encoded: String = data.iter()
.map(|(k, v)| format!("{}={}",
percent_encoding::utf8_percent_encode(k, percent_encoding::NON_ALPHANUMERIC),
percent_encoding::utf8_percent_encode(v, percent_encoding::NON_ALPHANUMERIC),
))
.collect::<Vec<_>>()
.join("&");
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/x-www-form-urlencoded".to_string());
self.request(Method::POST, path, Some(encoded), headers).await
}
pub async fn request(
&self,
method: Method,
path: &str,
body: Option<String>,
headers: HashMap<String, String>,
) -> TestResponse {
let uri: Uri = path.parse().expect("Invalid URI");
let mut builder = hyper::Request::builder()
.method(method.clone())
.uri(uri);
for (key, value) in &self.default_headers {
builder = builder.header(key.as_str(), value.as_str());
}
for (key, value) in &headers {
builder = builder.header(key.as_str(), value.as_str());
}
let hyper_body = match body {
Some(b) => Body::from(b),
None => Body::empty(),
};
let hyper_req = builder.body(hyper_body).expect("Failed to build request");
let req = match Request::from_hyper(hyper_req).await {
Ok(r) => r,
Err(e) => {
return TestResponse {
status: 500,
headers: HashMap::new(),
body: format!("Failed to create request: {}", e),
};
}
};
match self.router.handle_request(req).await {
Ok(response) => TestResponse::from_response(response).await,
Err(e) => TestResponse {
status: 500,
headers: HashMap::new(),
body: format!("Error: {}", e),
},
}
}
}
#[derive(Debug)]
pub struct TestResponse {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: String,
}
impl TestResponse {
async fn from_response(response: Response) -> Self {
let status = response.status.as_u16();
let headers: HashMap<String, String> = response
.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let body_bytes = hyper::body::to_bytes(response.body)
.await
.unwrap_or_default();
let body = String::from_utf8_lossy(&body_bytes).to_string();
Self { status, headers, body }
}
pub fn is_success(&self) -> bool {
self.status >= 200 && self.status < 300
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_str(&self.body)
}
pub fn assert_status(&self, expected: u16) -> &Self {
assert_eq!(self.status, expected, "Expected status {}, got {}", expected, self.status);
self
}
pub fn assert_body_contains(&self, text: &str) -> &Self {
assert!(self.body.contains(text), "Body does not contain '{}': {}", text, self.body);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_client_works() {
let router = Router::new();
let _client = TestClient::new(router);
}
}