use axum::body::Body;
use axum::http::{Method, Request, Response, StatusCode};
use serde_json::{Value, json};
use spikard_http::{Handler, HandlerResult, RequestData};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
pub struct HandlerBuilder {
status: StatusCode,
body: Value,
delay: Option<Duration>,
should_panic: bool,
}
impl HandlerBuilder {
pub fn new() -> Self {
Self {
status: StatusCode::OK,
body: json!({}),
delay: None,
should_panic: false,
}
}
pub fn status(mut self, code: u16) -> Self {
self.status = StatusCode::from_u16(code).unwrap_or(StatusCode::OK);
self
}
pub fn json_body(mut self, body: Value) -> Self {
self.body = body;
self
}
pub const fn delay(mut self, duration: Duration) -> Self {
self.delay = Some(duration);
self
}
pub const fn panics(mut self) -> Self {
self.should_panic = true;
self
}
pub fn build(self) -> Arc<dyn Handler> {
Arc::new(ConfiguredHandler {
status: self.status,
body: self.body,
delay: self.delay,
should_panic: self.should_panic,
})
}
}
impl Default for HandlerBuilder {
fn default() -> Self {
Self::new()
}
}
struct ConfiguredHandler {
status: StatusCode,
body: Value,
delay: Option<Duration>,
should_panic: bool,
}
impl Handler for ConfiguredHandler {
fn call(
&self,
_request: Request<Body>,
_request_data: RequestData,
) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
let status = self.status;
let body = self.body.clone();
let delay = self.delay;
let should_panic = self.should_panic;
Box::pin(async move {
assert!(!should_panic, "Handler configured to panic");
if let Some(duration) = delay {
sleep(duration).await;
}
let response = Response::builder()
.status(status)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();
Ok(response)
})
}
}
pub struct RequestBuilder {
method: Method,
path: String,
headers: HashMap<String, String>,
cookies: HashMap<String, String>,
body: Value,
query_params: HashMap<String, Vec<String>>,
}
impl RequestBuilder {
pub fn new() -> Self {
Self {
method: Method::GET,
path: "/".to_string(),
headers: HashMap::new(),
cookies: HashMap::new(),
body: json!(null),
query_params: HashMap::new(),
}
}
pub fn method(mut self, method: Method) -> Self {
self.method = method;
self
}
pub fn path(mut self, path: &str) -> Self {
self.path = path.to_string();
self
}
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers = headers;
self
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn cookies(mut self, cookies: HashMap<String, String>) -> Self {
self.cookies = cookies;
self
}
pub fn cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.cookies.insert(name.into(), value.into());
self
}
pub fn json_body(mut self, body: Value) -> Self {
self.body = body;
self
}
pub fn query_params(mut self, params: HashMap<String, Vec<String>>) -> Self {
self.query_params = params;
self
}
pub fn query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.query_params.entry(name.into()).or_default().push(value.into());
self
}
pub fn build(self) -> (Request<Body>, RequestData) {
let body = if self.body.is_null() {
Body::empty()
} else {
Body::from(self.body.to_string())
};
let mut request_builder = Request::builder().method(self.method.clone()).uri(&self.path);
for (name, value) in &self.headers {
request_builder = request_builder.header(name, value);
}
let request = request_builder.body(body).unwrap();
let request_data = RequestData {
path_params: Arc::new(HashMap::new()),
query_params: Arc::new(build_query_json(&self.query_params)),
validated_params: None,
raw_query_params: Arc::new(self.query_params),
body: Arc::new(self.body),
raw_body: None,
headers: Arc::new(self.headers),
cookies: Arc::new(self.cookies),
method: self.method.to_string(),
path: self.path,
#[cfg(feature = "di")]
dependencies: None,
};
(request, request_data)
}
}
impl Default for RequestBuilder {
fn default() -> Self {
Self::new()
}
}
fn build_query_json(raw_params: &HashMap<String, Vec<String>>) -> Value {
let mut map = serde_json::Map::new();
for (key, values) in raw_params {
if values.is_empty() {
map.insert(key.clone(), json!(null));
} else if values.len() == 1 {
map.insert(key.clone(), json!(values[0].clone()));
} else {
map.insert(key.clone(), json!(values.clone()));
}
}
Value::Object(map)
}
pub fn load_fixture(relative_path: &str) -> Result<Value, Box<dyn std::error::Error>> {
use std::path::PathBuf;
let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
while root.pop() {
if root.join("Cargo.toml").exists() {
break;
}
}
let path = root.join(relative_path);
let content = std::fs::read_to_string(&path)?;
let value = serde_json::from_str(&content)?;
Ok(value)
}
pub fn assert_status(response: &Response<Body>, expected: StatusCode) {
assert_eq!(
response.status(),
expected,
"Expected status {} but got {}",
expected,
response.status()
);
}
pub async fn parse_json_body(response: &mut Response<Body>) -> Result<Value, Box<dyn std::error::Error>> {
use axum::body::to_bytes;
use std::mem;
let body = mem::take(response.body_mut());
let bytes = to_bytes(body, usize::MAX).await?;
let value = serde_json::from_slice(&bytes)?;
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_handler_builder_default() {
let handler = HandlerBuilder::new().build();
let request = Request::builder().body(Body::empty()).unwrap();
let request_data = RequestData {
path_params: Arc::new(HashMap::new()),
query_params: Arc::new(json!({})),
validated_params: None,
raw_query_params: Arc::new(HashMap::new()),
body: Arc::new(json!(null)),
raw_body: None,
headers: Arc::new(HashMap::new()),
cookies: Arc::new(HashMap::new()),
method: "GET".to_string(),
path: "/".to_string(),
#[cfg(feature = "di")]
dependencies: None,
};
let result = handler.call(request, request_data).await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_handler_builder_custom_status() {
let handler = HandlerBuilder::new().status(201).build();
let request = Request::builder().body(Body::empty()).unwrap();
let request_data = RequestData {
path_params: Arc::new(HashMap::new()),
query_params: Arc::new(json!({})),
validated_params: None,
raw_query_params: Arc::new(HashMap::new()),
body: Arc::new(json!(null)),
raw_body: None,
headers: Arc::new(HashMap::new()),
cookies: Arc::new(HashMap::new()),
method: "POST".to_string(),
path: "/".to_string(),
#[cfg(feature = "di")]
dependencies: None,
};
let result = handler.call(request, request_data).await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
#[tokio::test]
async fn test_handler_builder_with_body() {
let body = json!({"message": "success", "code": 42});
let handler = HandlerBuilder::new().json_body(body.clone()).build();
let request = Request::builder().body(Body::empty()).unwrap();
let request_data = RequestData {
path_params: Arc::new(HashMap::new()),
query_params: Arc::new(json!({})),
validated_params: None,
raw_query_params: Arc::new(HashMap::new()),
body: Arc::new(json!(null)),
raw_body: None,
headers: Arc::new(HashMap::new()),
cookies: Arc::new(HashMap::new()),
method: "GET".to_string(),
path: "/".to_string(),
#[cfg(feature = "di")]
dependencies: None,
};
let result = handler.call(request, request_data).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handler_builder_with_delay() {
let start = std::time::Instant::now();
let handler = HandlerBuilder::new().delay(Duration::from_millis(10)).build();
let request = Request::builder().body(Body::empty()).unwrap();
let request_data = RequestData {
path_params: Arc::new(HashMap::new()),
query_params: Arc::new(json!({})),
validated_params: None,
raw_query_params: Arc::new(HashMap::new()),
body: Arc::new(json!(null)),
raw_body: None,
headers: Arc::new(HashMap::new()),
cookies: Arc::new(HashMap::new()),
method: "GET".to_string(),
path: "/".to_string(),
#[cfg(feature = "di")]
dependencies: None,
};
let _result = handler.call(request, request_data).await;
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(10));
}
#[test]
fn test_request_builder_default() {
let (request, request_data) = RequestBuilder::new().build();
assert_eq!(request.method(), &Method::GET);
assert_eq!(request_data.path, "/");
assert_eq!(request_data.method, "GET");
}
#[test]
fn test_request_builder_post_with_body() {
let body = json!({"name": "Alice", "age": 30});
let (request, request_data) = RequestBuilder::new()
.method(Method::POST)
.path("/users")
.json_body(body.clone())
.build();
assert_eq!(request.method(), &Method::POST);
assert_eq!(request_data.path, "/users");
assert_eq!(*request_data.body, body);
}
#[test]
fn test_request_builder_with_headers() {
let mut headers = HashMap::new();
headers.insert("authorization".to_string(), "Bearer token".to_string());
headers.insert("x-custom".to_string(), "value".to_string());
let (_request, request_data) = RequestBuilder::new().headers(headers.clone()).build();
assert_eq!(
request_data.headers.get("authorization"),
Some(&"Bearer token".to_string())
);
assert_eq!(request_data.headers.get("x-custom"), Some(&"value".to_string()));
}
#[test]
fn test_request_builder_with_single_header() {
let (_request, request_data) = RequestBuilder::new()
.header("x-api-key", "secret123")
.header("accept", "application/json")
.build();
assert_eq!(request_data.headers.get("x-api-key"), Some(&"secret123".to_string()));
assert_eq!(
request_data.headers.get("accept"),
Some(&"application/json".to_string())
);
}
#[test]
fn test_request_builder_with_cookies() {
let mut cookies = HashMap::new();
cookies.insert("session".to_string(), "abc123".to_string());
cookies.insert("preferences".to_string(), "dark_mode".to_string());
let (_request, request_data) = RequestBuilder::new().cookies(cookies).build();
assert_eq!(request_data.cookies.get("session"), Some(&"abc123".to_string()));
assert_eq!(request_data.cookies.get("preferences"), Some(&"dark_mode".to_string()));
}
#[test]
fn test_request_builder_with_query_params() {
let mut params = HashMap::new();
params.insert("page".to_string(), vec!["1".to_string()]);
params.insert("sort".to_string(), vec!["name".to_string()]);
params.insert("filter".to_string(), vec!["active".to_string(), "verified".to_string()]);
let (_request, request_data) = RequestBuilder::new().query_params(params).build();
assert_eq!(request_data.query_params["page"], "1");
assert_eq!(request_data.query_params["sort"], "name");
}
#[test]
fn test_request_builder_single_query_param() {
let (_request, request_data) = RequestBuilder::new()
.query_param("limit", "10")
.query_param("offset", "5")
.build();
assert_eq!(request_data.query_params["limit"], "10");
assert_eq!(request_data.query_params["offset"], "5");
}
#[test]
fn test_request_builder_fluent_api() {
let body = json!({"name": "Bob"});
let (_request, request_data) = RequestBuilder::new()
.method(Method::PUT)
.path("/users/42")
.header("authorization", "Bearer abc123")
.cookie("session", "xyz789")
.json_body(body.clone())
.query_param("notify", "true")
.build();
assert_eq!(request_data.method, "PUT");
assert_eq!(request_data.path, "/users/42");
assert_eq!(*request_data.body, body);
assert_eq!(
request_data.headers.get("authorization"),
Some(&"Bearer abc123".to_string())
);
assert_eq!(request_data.cookies.get("session"), Some(&"xyz789".to_string()));
}
#[test]
fn test_query_params_conversion() {
let mut params = HashMap::new();
params.insert("single".to_string(), vec!["value".to_string()]);
let query_json = build_query_json(¶ms);
assert_eq!(query_json["single"], "value");
}
}