use crate::container::core::MurServiceContainer;
use crate::mur_http::request::MurRequestContext;
use crate::router::MurRouter;
use crate::traits::MurMiddleware;
use crate::types::MurRes;
use http::{Method, StatusCode};
use hyper::body::Bytes;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
pub struct MurTestClient {
router: Arc<MurRouter>,
container: Arc<MurServiceContainer>,
middleware: Vec<Arc<dyn MurMiddleware>>,
default_headers: HashMap<String, String>,
}
impl MurTestClient {
pub fn new(router: Arc<MurRouter>, container: Arc<MurServiceContainer>) -> Self {
Self {
router,
container,
middleware: Vec::new(),
default_headers: HashMap::new(),
}
}
pub fn with_middleware<M: MurMiddleware>(mut self, middleware: M) -> Self {
self.middleware.push(Arc::new(middleware));
self
}
pub fn with_default_header(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.default_headers.insert(name.into(), value.into());
self
}
pub fn with_bearer_token(self, token: impl Into<String>) -> Self {
self.with_default_header("Authorization", format!("Bearer {}", token.into()))
}
pub fn get(&self, path: &str) -> MurTestRequestBuilder {
self.request(Method::GET, path)
}
pub fn post(&self, path: &str) -> MurTestRequestBuilder {
self.request(Method::POST, path)
}
pub fn put(&self, path: &str) -> MurTestRequestBuilder {
self.request(Method::PUT, path)
}
pub fn patch(&self, path: &str) -> MurTestRequestBuilder {
self.request(Method::PATCH, path)
}
pub fn delete(&self, path: &str) -> MurTestRequestBuilder {
self.request(Method::DELETE, path)
}
pub fn head(&self, path: &str) -> MurTestRequestBuilder {
self.request(Method::HEAD, path)
}
pub fn options(&self, path: &str) -> MurTestRequestBuilder {
self.request(Method::OPTIONS, path)
}
pub fn request(&self, method: Method, path: &str) -> MurTestRequestBuilder {
let headers = self.default_headers.clone();
MurTestRequestBuilder {
client: self.clone(),
method,
path: path.to_string(),
headers,
body: None,
query_params: HashMap::new(),
}
}
}
pub struct MurTestRequestBuilder {
client: MurTestClient,
method: Method,
path: String,
headers: HashMap<String, String>,
body: Option<Bytes>,
query_params: HashMap<String, String>,
}
impl MurTestRequestBuilder {
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn headers(mut self, headers: impl IntoIterator<Item = (String, String)>) -> Self {
for (name, value) in headers {
self.headers.insert(name, value);
}
self
}
pub fn json<T: Serialize>(mut self, body: &T) -> Self {
match serde_json::to_vec(body) {
Ok(bytes) => {
self.body = Some(Bytes::from(bytes));
self.headers
.insert("Content-Type".to_string(), "application/json".to_string());
}
Err(e) => {
eprintln!("Failed to serialize JSON body: {}", e);
}
}
self
}
pub fn body(mut self, body: impl Into<Bytes>) -> Self {
self.body = Some(body.into());
self
}
pub fn text(mut self, body: impl Into<String>) -> Self {
self.body = Some(Bytes::from(body.into()));
self.headers
.insert("Content-Type".to_string(), "text/plain".to_string());
self
}
pub fn form<T: Serialize>(mut self, body: &T) -> Self {
match serde_urlencoded::to_string(body) {
Ok(encoded) => {
self.body = Some(Bytes::from(encoded));
self.headers.insert(
"Content-Type".to_string(),
"application/x-www-form-urlencoded".to_string(),
);
}
Err(e) => {
eprintln!("Failed to encode form body: {}", e);
}
}
self
}
pub fn query(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.query_params.insert(name.into(), value.into());
self
}
pub fn query_params(mut self, params: impl IntoIterator<Item = (String, String)>) -> Self {
for (name, value) in params {
self.query_params.insert(name, value);
}
self
}
pub fn bearer_token(self, token: impl Into<String>) -> Self {
self.header("Authorization", format!("Bearer {}", token.into()))
}
pub fn basic_auth(self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
let credentials = format!("{}:{}", username.as_ref(), password.as_ref());
let encoded = base64_encode(credentials.as_bytes());
self.header("Authorization", format!("Basic {}", encoded))
}
fn build_path(&self) -> String {
if self.query_params.is_empty() {
self.path.clone()
} else {
let query_string: Vec<String> = self
.query_params
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect();
format!("{}?{}", self.path, query_string.join("&"))
}
}
pub async fn send(mut self) -> MurTestResponse {
let path = self.build_path();
let mut builder = http::Request::builder()
.method(self.method.clone())
.uri(&path);
for (name, value) in &self.headers {
builder = builder.header(name, value);
}
let body = self.body.take().unwrap_or_default();
let request = match builder.body(body.clone()) {
Ok(req) => req,
Err(e) => {
return MurTestResponse::error(format!("Failed to build request: {}", e));
}
};
let (parts, _) = request.into_parts();
let ctx = MurRequestContext::new(
parts,
Some(body),
HashMap::new(),
self.client.container.clone(),
);
let response = self.execute_request(ctx).await;
MurTestResponse::from_response(response)
}
async fn execute_request(self, mut ctx: MurRequestContext) -> MurRes {
let path = ctx.path().to_string();
let method = ctx.method().to_string();
if let Some(params) = self.client.router.find_route_params(&method, &path) {
ctx.path_params = params;
}
match self
.client
.router
.execute_matched_route(&method, &path, ctx)
.await
{
Some(result) => result,
None => {
crate::types::MurHttpResponse::not_found().json(serde_json::json!({
"error": "Not Found",
"message": format!("No route found for {} {}", method, path)
}))
}
}
}
}
pub struct MurTestRequest {
method: Method,
path: String,
headers: HashMap<String, String>,
body: Option<Bytes>,
}
impl MurTestRequest {
pub fn method(&self) -> &Method {
&self.method
}
pub fn path(&self) -> &str {
&self.path
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub fn body(&self) -> Option<&Bytes> {
self.body.as_ref()
}
}
pub struct MurTestResponse {
status: StatusCode,
headers: HashMap<String, String>,
body: Bytes,
error: Option<String>,
}
impl MurTestResponse {
pub(crate) fn from_response(result: MurRes) -> Self {
match result {
Ok(response) => {
let status = response.status();
let headers: HashMap<String, String> = response
.headers()
.iter()
.map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
.collect();
let body = Bytes::new();
Self {
status,
headers,
body,
error: None,
}
}
Err(e) => Self::error(format!("Request failed: {}", e)),
}
}
pub(crate) fn error(message: String) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
headers: HashMap::new(),
body: Bytes::from(message.clone()),
error: Some(message),
}
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn is_success(&self) -> bool {
self.status.is_success()
}
pub fn is_client_error(&self) -> bool {
self.status.is_client_error()
}
pub fn is_server_error(&self) -> bool {
self.status.is_server_error()
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).map(|s| s.as_str())
}
pub fn body(&self) -> &Bytes {
&self.body
}
pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.body.to_vec())
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_slice(&self.body)
}
pub fn error_message(&self) -> Option<&str> {
self.error.as_deref()
}
pub fn is_error(&self) -> bool {
self.error.is_some()
}
}
fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
let chunks = data.chunks(3);
for chunk in chunks {
let mut buffer = [0u8; 3];
buffer[..chunk.len()].copy_from_slice(chunk);
let b0 = buffer[0];
let b1 = buffer[1];
let b2 = buffer[2];
result.push(ALPHABET[(b0 >> 2) as usize] as char);
result.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
if chunk.len() > 1 {
result.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(ALPHABET[(b2 & 0x3f) as usize] as char);
} else {
result.push('=');
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base64_encode() {
assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
assert_eq!(base64_encode(b"world"), "d29ybGQ=");
assert_eq!(base64_encode(b"user:pass"), "dXNlcjpwYXNz");
}
#[test]
fn test_test_response_status_checks() {
let success = MurTestResponse {
status: StatusCode::OK,
headers: HashMap::new(),
body: Bytes::new(),
error: None,
};
assert!(success.is_success());
assert!(!success.is_client_error());
assert!(!success.is_server_error());
let not_found = MurTestResponse {
status: StatusCode::NOT_FOUND,
headers: HashMap::new(),
body: Bytes::new(),
error: None,
};
assert!(!not_found.is_success());
assert!(not_found.is_client_error());
assert!(!not_found.is_server_error());
let server_error = MurTestResponse {
status: StatusCode::INTERNAL_SERVER_ERROR,
headers: HashMap::new(),
body: Bytes::new(),
error: None,
};
assert!(!server_error.is_success());
assert!(!server_error.is_client_error());
assert!(server_error.is_server_error());
}
#[test]
fn test_query_params_building() {
let container = Arc::new(MurServiceContainer::new());
let builder = MurTestRequestBuilder {
client: MurTestClient {
router: Arc::new(MurRouter::new(Arc::clone(&container))),
container,
middleware: Vec::new(),
default_headers: HashMap::new(),
},
method: Method::GET,
path: "/api/users".to_string(),
headers: HashMap::new(),
body: None,
query_params: HashMap::new(),
};
assert_eq!(builder.build_path(), "/api/users");
let builder = builder.query("page", "1").query("limit", "10");
let path = builder.build_path();
assert!(path.starts_with("/api/users?"));
assert!(path.contains("page=1"));
assert!(path.contains("limit=10"));
}
}