use std::{
collections::BTreeMap,
future::Future,
pin::Pin,
sync::{Arc, RwLock},
};
use bytes::Bytes;
use switchy_http_models::Method;
use switchy_web_server_core::WebServer;
use crate::{PathParams, RouteHandler, StaticFiles, WebServerBuilder};
#[derive(Debug, Clone)]
pub struct SimulationResponse {
pub status: u16,
pub headers: BTreeMap<String, String>,
pub body: Option<Bytes>,
}
impl SimulationResponse {
#[must_use]
pub const fn new(status: u16) -> Self {
Self {
status,
headers: BTreeMap::new(),
body: None,
}
}
#[must_use]
pub const fn ok() -> Self {
Self::new(200)
}
#[must_use]
pub const fn not_found() -> Self {
Self::new(404)
}
#[must_use]
pub const fn internal_server_error() -> Self {
Self::new(500)
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
#[must_use]
pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
self.body = Some(body.into());
self
}
#[must_use]
pub fn body_str(&self) -> Option<&str> {
self.body.as_ref().and_then(|b| std::str::from_utf8(b).ok())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PathSegment {
Literal(String),
Parameter(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PathPattern {
segments: Vec<PathSegment>,
}
impl PathPattern {
#[must_use]
pub const fn new(segments: Vec<PathSegment>) -> Self {
Self { segments }
}
#[must_use]
pub fn segments(&self) -> &[PathSegment] {
&self.segments
}
}
#[must_use]
pub fn parse_path_pattern(path: &str) -> PathPattern {
let path = path.strip_prefix('/').unwrap_or(path);
if path.is_empty() {
return PathPattern::new(Vec::new());
}
let segments = path
.split('/')
.filter(|segment| !segment.is_empty())
.map(|segment| {
if segment.starts_with('{') && segment.ends_with('}') {
let param_name = &segment[1..segment.len() - 1];
PathSegment::Parameter(param_name.to_string())
} else {
PathSegment::Literal(segment.to_string())
}
})
.collect();
PathPattern::new(segments)
}
#[must_use]
pub fn match_path(pattern: &PathPattern, actual_path: &str) -> Option<PathParams> {
let actual_pattern = parse_path_pattern(actual_path);
let actual_segments = actual_pattern.segments();
let pattern_segments = pattern.segments();
if actual_segments.len() != pattern_segments.len() {
return None;
}
let mut params = PathParams::new();
for (pattern_segment, actual_segment) in pattern_segments.iter().zip(actual_segments.iter()) {
match (pattern_segment, actual_segment) {
(PathSegment::Literal(pattern_lit), PathSegment::Literal(actual_lit)) => {
if pattern_lit != actual_lit {
return None;
}
}
(PathSegment::Parameter(param_name), PathSegment::Literal(actual_value)) => {
params.insert(param_name.clone(), actual_value.clone());
}
(PathSegment::Literal(_) | PathSegment::Parameter(_), PathSegment::Parameter(_)) => {
return None;
}
}
}
Some(params)
}
fn convert_http_response_to_simulation_response(
http_response: crate::HttpResponse,
) -> SimulationResponse {
let status = status_code_to_u16(http_response.status_code);
let mut response = SimulationResponse {
status,
headers: http_response.headers, body: None,
};
if let Some(body) = http_response.body {
let crate::HttpResponseBody::Bytes(body_bytes) = body;
response.body = Some(body_bytes);
}
if let Some(location) = http_response.location {
response.headers.insert("Location".to_string(), location);
}
response
}
const fn status_code_to_u16(status_code: switchy_http_models::StatusCode) -> u16 {
match status_code {
switchy_http_models::StatusCode::Ok => 200,
switchy_http_models::StatusCode::Created => 201,
switchy_http_models::StatusCode::Accepted => 202,
switchy_http_models::StatusCode::NoContent => 204,
switchy_http_models::StatusCode::MovedPermanently => 301,
switchy_http_models::StatusCode::Found => 302,
switchy_http_models::StatusCode::SeeOther => 303,
switchy_http_models::StatusCode::NotModified => 304,
switchy_http_models::StatusCode::TemporaryRedirect => 307,
switchy_http_models::StatusCode::PermanentRedirect => 308,
switchy_http_models::StatusCode::BadRequest => 400,
switchy_http_models::StatusCode::Unauthorized => 401,
switchy_http_models::StatusCode::PaymentRequired => 402,
switchy_http_models::StatusCode::Forbidden => 403,
switchy_http_models::StatusCode::NotFound => 404,
switchy_http_models::StatusCode::MethodNotAllowed => 405,
switchy_http_models::StatusCode::NotAcceptable => 406,
switchy_http_models::StatusCode::ProxyAuthenticationRequired => 407,
switchy_http_models::StatusCode::RequestTimeout => 408,
switchy_http_models::StatusCode::Conflict => 409,
switchy_http_models::StatusCode::Gone => 410,
switchy_http_models::StatusCode::LengthRequired => 411,
switchy_http_models::StatusCode::PreconditionFailed => 412,
switchy_http_models::StatusCode::ContentTooLarge => 413,
switchy_http_models::StatusCode::URITooLong => 414,
switchy_http_models::StatusCode::UnsupportedMediaType => 415,
switchy_http_models::StatusCode::RangeNotSatisfiable => 416,
switchy_http_models::StatusCode::ExpectationFailed => 417,
switchy_http_models::StatusCode::ImATeapot => 418,
switchy_http_models::StatusCode::MisdirectedRequest => 421,
switchy_http_models::StatusCode::UncompressableContent => 422,
switchy_http_models::StatusCode::Locked => 423,
switchy_http_models::StatusCode::FailedDependency => 424,
switchy_http_models::StatusCode::UpgradeRequired => 426,
switchy_http_models::StatusCode::PreconditionRequired => 428,
switchy_http_models::StatusCode::TooManyRequests => 429,
switchy_http_models::StatusCode::RequestHeaderFieldsTooLarge => 431,
switchy_http_models::StatusCode::UnavailableForLegalReasons => 451,
switchy_http_models::StatusCode::NotImplemented => 501,
switchy_http_models::StatusCode::BadGateway => 502,
switchy_http_models::StatusCode::ServiceUnavailable => 503,
switchy_http_models::StatusCode::GatewayTimeout => 504,
switchy_http_models::StatusCode::HTTPVersionNotSupported => 505,
switchy_http_models::StatusCode::VariantAlsoNegotiates => 506,
switchy_http_models::StatusCode::InsufficientStorage => 507,
switchy_http_models::StatusCode::LoopDetected => 508,
switchy_http_models::StatusCode::NotExtended => 510,
switchy_http_models::StatusCode::NetworkAuthenticationRequired => 511,
_ => 500, }
}
#[derive(Debug, Clone)]
pub struct SimulationRequest {
pub method: Method,
pub path: String,
pub query_string: String,
pub headers: BTreeMap<String, String>,
pub body: Option<Bytes>,
pub cookies: BTreeMap<String, String>,
pub remote_addr: Option<String>,
pub path_params: PathParams,
}
impl SimulationRequest {
#[must_use]
pub fn new(method: Method, path: impl Into<String>) -> Self {
Self {
method,
path: path.into(),
query_string: String::new(),
headers: BTreeMap::new(),
body: None,
cookies: BTreeMap::new(),
remote_addr: None,
path_params: PathParams::new(),
}
}
#[must_use]
pub fn with_query_string(mut self, query: impl Into<String>) -> Self {
self.query_string = query.into();
self
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
#[must_use]
pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
self.body = Some(body.into());
self
}
#[must_use]
pub fn with_cookies(mut self, cookies: impl IntoIterator<Item = (String, String)>) -> Self {
self.cookies.extend(cookies);
self
}
#[must_use]
pub fn with_cookie(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.cookies.insert(name.into(), value.into());
self
}
#[must_use]
pub fn with_remote_addr(mut self, addr: impl Into<String>) -> Self {
self.remote_addr = Some(addr.into());
self
}
#[must_use]
pub fn with_path_params(mut self, params: PathParams) -> Self {
self.path_params = params;
self
}
}
#[derive(Debug, Clone)]
pub struct SimulationStub {
pub request: SimulationRequest,
pub state_container: Option<Arc<RwLock<crate::extractors::state::StateContainer>>>,
}
impl SimulationStub {
#[must_use]
pub const fn new(request: SimulationRequest) -> Self {
Self {
request,
state_container: None,
}
}
#[must_use]
pub fn with_state_container(
mut self,
container: Arc<RwLock<crate::extractors::state::StateContainer>>,
) -> Self {
self.state_container = Some(container);
self
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
self.request.headers.get(name).map(String::as_str)
}
#[must_use]
pub fn path(&self) -> &str {
&self.request.path
}
#[must_use]
pub fn query_string(&self) -> &str {
&self.request.query_string
}
#[must_use]
pub const fn method(&self) -> &Method {
&self.request.method
}
#[must_use]
pub const fn body(&self) -> Option<&Bytes> {
self.request.body.as_ref()
}
#[must_use]
pub fn cookie(&self, name: &str) -> Option<&str> {
self.request.cookies.get(name).map(String::as_str)
}
#[must_use]
pub const fn cookies(&self) -> &BTreeMap<String, String> {
&self.request.cookies
}
#[must_use]
pub fn remote_addr(&self) -> Option<&str> {
self.request.remote_addr.as_deref()
}
#[must_use]
pub fn state<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
self.state_container.as_ref().and_then(|container| {
container
.read()
.map_or_else(|_| None, |state| state.get::<T>())
})
}
#[must_use]
pub fn path_param(&self, name: &str) -> Option<&str> {
self.request.path_params.get(name).map(String::as_str)
}
#[must_use]
pub const fn app_state(
&self,
) -> Option<&Arc<RwLock<crate::extractors::state::StateContainer>>> {
self.state_container.as_ref()
}
}
impl From<SimulationRequest> for SimulationStub {
fn from(request: SimulationRequest) -> Self {
Self::new(request)
}
}
impl crate::request::HttpRequestTrait for SimulationStub {
fn path(&self) -> &str {
&self.request.path
}
fn query_string(&self) -> &str {
&self.request.query_string
}
fn method(&self) -> Method {
self.request.method
}
fn header(&self, name: &str) -> Option<&str> {
self.request.headers.get(name).map(String::as_str)
}
fn headers(&self) -> BTreeMap<String, String> {
self.request.headers.clone()
}
fn body(&self) -> Option<&Bytes> {
self.request.body.as_ref()
}
fn cookie(&self, name: &str) -> Option<String> {
self.request.cookies.get(name).cloned()
}
fn cookies(&self) -> BTreeMap<String, String> {
self.request.cookies.clone()
}
fn remote_addr(&self) -> Option<String> {
self.request.remote_addr.clone()
}
fn path_params(&self) -> &PathParams {
&self.request.path_params
}
fn app_state_any(&self, type_id: std::any::TypeId) -> Option<crate::request::ErasedState> {
self.state_container.as_ref().and_then(|container| {
container
.read()
.map_or_else(|_| None, |state| state.get_any(type_id))
})
}
}
pub struct SimulatorWebServer {
pub scopes: Vec<crate::Scope>,
pub routes: BTreeMap<(Method, String), RouteHandler>,
pub state: Arc<RwLock<crate::extractors::state::StateContainer>>,
pub static_files: Option<StaticFiles>,
}
impl SimulatorWebServer {
#[allow(dead_code)] pub fn register_route(&mut self, method: Method, path: &str, handler: RouteHandler) {
self.routes.insert((method, path.to_string()), handler);
}
#[allow(dead_code)] pub fn register_scope(&mut self, scope: &crate::Scope) {
self.process_scope_recursive(scope, "");
}
fn process_scope_recursive(&mut self, scope: &crate::Scope, parent_prefix: &str) {
let full_prefix = if parent_prefix.is_empty() {
scope.path.clone()
} else {
format!("{}{}", parent_prefix, scope.path)
};
for route in &scope.routes {
let full_path = if full_prefix.is_empty() {
route.path.clone()
} else {
format!("{}{}", full_prefix, route.path)
};
let handler_arc = Arc::clone(&route.handler);
let handler: RouteHandler = Box::new(move |req| {
let handler_arc = Arc::clone(&handler_arc);
handler_arc(req)
});
self.register_route(route.method, &full_path, handler);
}
for nested_scope in &scope.scopes {
self.process_scope_recursive(nested_scope, &full_prefix);
}
}
#[allow(unused)] #[must_use]
pub fn find_route(&self, method: Method, path: &str) -> Option<(&RouteHandler, PathParams)> {
let mut exact_matches = Vec::new();
let mut parameterized_matches = Vec::new();
for ((route_method, route_path), handler) in &self.routes {
if *route_method != method {
continue;
}
let route_pattern = parse_path_pattern(route_path);
if let Some(params) = match_path(&route_pattern, path) {
if params.is_empty() {
exact_matches.push((handler, params));
} else {
parameterized_matches.push((handler, params));
}
}
}
if let Some((handler, params)) = exact_matches.into_iter().next() {
Some((handler, params))
} else {
parameterized_matches.into_iter().next()
}
}
#[allow(unused)] pub async fn process_request(&self, mut request: SimulationRequest) -> SimulationResponse {
let route_result = self.find_route(request.method, &request.path);
let Some((handler, path_params)) = route_result else {
return SimulationResponse::not_found().with_body("Not Found");
};
request.path_params = path_params;
let simulation_stub =
SimulationStub::new(request).with_state_container(Arc::clone(&self.state));
let http_request = crate::HttpRequest::new(simulation_stub);
handler(http_request).await.map_or_else(
|_| {
SimulationResponse::internal_server_error().with_body("Internal Server Error")
},
|http_response| {
convert_http_response_to_simulation_response(http_response)
},
)
}
#[allow(dead_code)] pub fn insert_state<T: Send + Sync + 'static>(&self, state: T) {
if let Ok(mut state_container) = self.state.write() {
state_container.insert(state);
}
}
#[must_use]
#[allow(dead_code)] pub fn get_state<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
self.state
.read()
.map_or_else(|_| None, |state_container| state_container.get::<T>())
}
#[must_use]
pub fn new(scopes: Vec<crate::Scope>) -> Self {
Self {
scopes,
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
static_files: None,
}
}
#[must_use]
pub fn with_static_files(scopes: Vec<crate::Scope>, static_files: StaticFiles) -> Self {
Self {
scopes,
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
static_files: Some(static_files),
}
}
#[must_use]
pub const fn static_files(&self) -> Option<&StaticFiles> {
self.static_files.as_ref()
}
#[cfg(feature = "simulator")]
fn get_mime_type(path: &str) -> &'static str {
let extension = path.rsplit('.').next().unwrap_or("");
match extension.to_lowercase().as_str() {
"html" | "htm" => "text/html; charset=utf-8",
"css" => "text/css; charset=utf-8",
"js" | "mjs" => "application/javascript; charset=utf-8",
"json" => "application/json; charset=utf-8",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"webp" => "image/webp",
"ico" => "image/x-icon",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"eot" => "application/vnd.ms-fontobject",
"otf" => "font/otf",
"txt" => "text/plain; charset=utf-8",
"xml" => "application/xml; charset=utf-8",
"pdf" => "application/pdf",
"map" => "application/json",
"wasm" => "application/wasm",
_ => "application/octet-stream",
}
}
#[cfg(feature = "simulator")]
pub async fn serve_static_file(&self, request_path: &str) -> Option<SimulationResponse> {
let config = self.static_files.as_ref()?;
let mount_path = config.mount_path();
let relative_path = if mount_path == "/" {
request_path.strip_prefix('/').unwrap_or(request_path)
} else {
request_path
.strip_prefix(mount_path)?
.strip_prefix('/')
.unwrap_or("")
};
if relative_path.contains("..") {
return None;
}
let file_path = config.directory().join(relative_path);
let contents = switchy_fs::unsync::read(&file_path).await.ok()?;
let mime_type = Self::get_mime_type(request_path);
Some(
SimulationResponse::ok()
.with_header("Content-Type", mime_type)
.with_body(contents),
)
}
#[must_use]
pub fn with_test_routes() -> Self {
let mut server = Self::new(Vec::new());
server.register_route(
Method::Get,
"/test",
Box::new(|_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_header("content-type", "application/json")
.with_body(r#"{"message":"Hello from test route!"}"#))
})
}),
);
server.register_route(
Method::Get,
"/health",
Box::new(|_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_header("content-type", "application/json")
.with_body(r#"{"status":"ok"}"#))
})
}),
);
server
}
#[must_use]
pub fn with_api_routes() -> Self {
let mut server = Self::new(Vec::new());
server.register_route(
Method::Get,
"/api/status",
Box::new(|_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_header("content-type", "application/json")
.with_body(r#"{"service":"running"}"#))
})
}),
);
server.register_route(
Method::Post,
"/api/echo",
Box::new(|_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_header("content-type", "application/json")
.with_body(r#"{"echoed":"data"}"#))
})
}),
);
server
}
}
#[derive(Debug, thiserror::Error)]
pub enum SimulatorWebServerError {
#[error("Server startup failed: {0}")]
Startup(String),
#[error("Server shutdown failed: {0}")]
Shutdown(String),
}
impl crate::test_client::GenericTestServer for SimulatorWebServer {
type Error = SimulatorWebServerError;
fn url(&self) -> String {
"http://simulator".to_string()
}
fn port(&self) -> u16 {
8080
}
fn start(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn stop(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
impl WebServer for SimulatorWebServer {
fn start(&self) -> Pin<Box<dyn Future<Output = ()>>> {
let scopes = self.scopes.clone();
Box::pin(async move {
log::info!("Simulator web server started with {} scopes", scopes.len());
for scope in &scopes {
log::debug!("Scope '{}' has {} routes", scope.path, scope.routes.len());
for route in &scope.routes {
log::debug!(" {:?} {}{}", route.method, scope.path, route.path);
}
}
})
}
fn stop(&self) -> Pin<Box<dyn Future<Output = ()>>> {
Box::pin(async {
log::info!("Simulator web server stopped");
})
}
}
impl WebServerBuilder {
#[must_use]
pub fn build_simulator(self) -> Box<dyn WebServer> {
Box::new(SimulatorWebServer {
scopes: self.scopes,
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
static_files: self.static_files,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{HttpRequest, HttpResponse};
fn create_test_handler() -> RouteHandler {
Box::new(|_req: HttpRequest| {
Box::pin(async move { Ok(HttpResponse::ok().with_body("test response")) })
})
}
#[test]
fn test_route_registration_stores_handler_correctly() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let handler = create_test_handler();
server.register_route(Method::Get, "/test", handler);
assert!(
server
.routes
.contains_key(&(Method::Get, "/test".to_string()))
);
assert_eq!(server.routes.len(), 1);
}
#[test]
fn test_multiple_routes_can_be_registered_without_conflict() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let handler1 = create_test_handler();
let handler2 = create_test_handler();
let handler3 = create_test_handler();
server.register_route(Method::Get, "/users", handler1);
server.register_route(Method::Post, "/users", handler2);
server.register_route(Method::Get, "/posts", handler3);
assert!(
server
.routes
.contains_key(&(Method::Get, "/users".to_string()))
);
assert!(
server
.routes
.contains_key(&(Method::Post, "/users".to_string()))
);
assert!(
server
.routes
.contains_key(&(Method::Get, "/posts".to_string()))
);
assert_eq!(server.routes.len(), 3);
}
#[test]
fn test_parse_literal_path_pattern() {
let pattern = parse_path_pattern("/users/profile");
assert_eq!(pattern.segments().len(), 2);
assert_eq!(
pattern.segments()[0],
PathSegment::Literal("users".to_string())
);
assert_eq!(
pattern.segments()[1],
PathSegment::Literal("profile".to_string())
);
}
#[test]
fn test_parse_parameterized_path_pattern() {
let pattern = parse_path_pattern("/{id}");
assert_eq!(pattern.segments().len(), 1);
assert_eq!(
pattern.segments()[0],
PathSegment::Parameter("id".to_string())
);
}
#[test]
fn test_parse_mixed_literal_and_parameter_path_pattern() {
let pattern = parse_path_pattern("/users/{id}/posts/{post_id}");
assert_eq!(pattern.segments().len(), 4);
assert_eq!(
pattern.segments()[0],
PathSegment::Literal("users".to_string())
);
assert_eq!(
pattern.segments()[1],
PathSegment::Parameter("id".to_string())
);
assert_eq!(
pattern.segments()[2],
PathSegment::Literal("posts".to_string())
);
assert_eq!(
pattern.segments()[3],
PathSegment::Parameter("post_id".to_string())
);
}
#[test]
fn test_parse_empty_path_pattern() {
let pattern = parse_path_pattern("");
assert_eq!(pattern.segments().len(), 0);
let pattern = parse_path_pattern("/");
assert_eq!(pattern.segments().len(), 0);
}
#[test]
fn test_parse_path_pattern_without_leading_slash() {
let pattern = parse_path_pattern("users/{id}");
assert_eq!(pattern.segments().len(), 2);
assert_eq!(
pattern.segments()[0],
PathSegment::Literal("users".to_string())
);
assert_eq!(
pattern.segments()[1],
PathSegment::Parameter("id".to_string())
);
}
#[test]
fn test_match_path_exact_route() {
let pattern = parse_path_pattern("/api/users");
let params = match_path(&pattern, "/api/users").unwrap();
assert!(params.is_empty());
}
#[test]
fn test_match_path_parameterized_route() {
let pattern = parse_path_pattern("/users/{id}");
let params = match_path(&pattern, "/users/123").unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_match_path_multiple_parameters() {
let pattern = parse_path_pattern("/users/{id}/posts/{post_id}");
let params = match_path(&pattern, "/users/123/posts/456").unwrap();
assert_eq!(params.len(), 2);
assert_eq!(params.get("id"), Some(&"123".to_string()));
assert_eq!(params.get("post_id"), Some(&"456".to_string()));
}
#[test]
fn test_match_path_no_match_different_segments() {
let pattern = parse_path_pattern("/users/{id}");
let result = match_path(&pattern, "/posts/123");
assert!(result.is_none());
}
#[test]
fn test_match_path_no_match_different_length() {
let pattern = parse_path_pattern("/users/{id}");
let result = match_path(&pattern, "/users/123/extra");
assert!(result.is_none());
}
#[test]
fn test_find_route_exact_match() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let handler = create_test_handler();
server.register_route(Method::Get, "/api/users", handler);
let result = server.find_route(Method::Get, "/api/users");
assert!(result.is_some());
let (_, params) = result.unwrap();
assert!(params.is_empty());
}
#[test]
fn test_find_route_parameterized_match() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let handler = create_test_handler();
server.register_route(Method::Get, "/users/{id}", handler);
let result = server.find_route(Method::Get, "/users/123");
assert!(result.is_some());
let (_, params) = result.unwrap();
assert_eq!(params.len(), 1);
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_find_route_method_discrimination() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let get_handler = create_test_handler();
let post_handler = create_test_handler();
server.register_route(Method::Get, "/users", get_handler);
server.register_route(Method::Post, "/users", post_handler);
let get_result = server.find_route(Method::Get, "/users");
assert!(get_result.is_some());
let post_result = server.find_route(Method::Post, "/users");
assert!(post_result.is_some());
let put_result = server.find_route(Method::Put, "/users");
assert!(put_result.is_none());
}
#[test]
fn test_find_route_no_match_404() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let handler = create_test_handler();
server.register_route(Method::Get, "/users", handler);
let result = server.find_route(Method::Get, "/posts");
assert!(result.is_none());
let result = server.find_route(Method::Post, "/users");
assert!(result.is_none());
}
#[test]
fn test_find_route_precedence_exact_over_parameterized() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let exact_handler = create_test_handler();
let param_handler = create_test_handler();
server.register_route(Method::Get, "/users/{id}", param_handler);
server.register_route(Method::Get, "/users/profile", exact_handler);
let result = server.find_route(Method::Get, "/users/profile");
assert!(result.is_some());
let (_, params) = result.unwrap();
assert!(params.is_empty()); }
#[test]
fn test_process_request_integration_setup() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let handler = create_test_handler();
server.register_route(Method::Get, "/hello", handler);
let route_result = server.find_route(Method::Get, "/hello");
assert!(route_result.is_some());
let not_found_result = server.find_route(Method::Get, "/nonexistent");
assert!(not_found_result.is_none());
}
#[test]
fn test_simulation_response_builders() {
let response = SimulationResponse::ok()
.with_header("Content-Type", "application/json")
.with_body("{}");
assert_eq!(response.status, 200);
assert_eq!(
response.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
assert_eq!(response.body_str(), Some("{}"));
}
#[test]
fn test_simulation_request_with_path_params() {
let mut params = PathParams::new();
params.insert("id".to_string(), "123".to_string());
let request = SimulationRequest::new(Method::Get, "/users/123").with_path_params(params);
assert_eq!(request.path_params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_simulation_stub_path_param() {
let mut params = PathParams::new();
params.insert("id".to_string(), "456".to_string());
params.insert("name".to_string(), "john".to_string());
let request = SimulationRequest::new(Method::Get, "/users/456").with_path_params(params);
let stub = SimulationStub::new(request);
assert_eq!(stub.path_param("id"), Some("456"));
assert_eq!(stub.path_param("name"), Some("john"));
assert_eq!(stub.path_param("nonexistent"), None);
}
#[test]
#[cfg(feature = "serde")]
fn test_json_response_conversion_preserves_content_type() {
use serde_json::json;
let test_data = json!({"message": "Hello, World!", "status": "success"});
let http_response = HttpResponse::json(&test_data).unwrap();
let simulation_response = convert_http_response_to_simulation_response(http_response);
assert_eq!(simulation_response.status, 200);
assert_eq!(
simulation_response.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
assert!(simulation_response.body.is_some());
let body = simulation_response.body_str().unwrap();
let parsed: serde_json::Value = serde_json::from_str(body).unwrap();
assert_eq!(parsed["message"], "Hello, World!");
assert_eq!(parsed["status"], "success");
}
#[test]
fn test_status_codes_are_preserved() {
let ok_response = HttpResponse::ok();
let sim_response = convert_http_response_to_simulation_response(ok_response);
assert_eq!(sim_response.status, 200);
let not_found_response = HttpResponse::not_found();
let sim_response = convert_http_response_to_simulation_response(not_found_response);
assert_eq!(sim_response.status, 404);
let error_response =
HttpResponse::from_status_code(switchy_http_models::StatusCode::InternalServerError);
let sim_response = convert_http_response_to_simulation_response(error_response);
assert_eq!(sim_response.status, 500);
let created_response =
HttpResponse::from_status_code(switchy_http_models::StatusCode::Created);
let sim_response = convert_http_response_to_simulation_response(created_response);
assert_eq!(sim_response.status, 201);
let unauthorized_response =
HttpResponse::from_status_code(switchy_http_models::StatusCode::Unauthorized);
let sim_response = convert_http_response_to_simulation_response(unauthorized_response);
assert_eq!(sim_response.status, 401);
}
#[test]
fn test_custom_headers_are_preserved() {
let http_response = HttpResponse::ok()
.with_header("X-Custom-Header", "custom-value")
.with_header("X-Another-Header", "another-value")
.with_content_type("text/plain")
.with_body("Hello, World!");
let simulation_response = convert_http_response_to_simulation_response(http_response);
assert_eq!(simulation_response.status, 200);
assert_eq!(
simulation_response.headers.get("X-Custom-Header"),
Some(&"custom-value".to_string())
);
assert_eq!(
simulation_response.headers.get("X-Another-Header"),
Some(&"another-value".to_string())
);
assert_eq!(
simulation_response.headers.get("Content-Type"),
Some(&"text/plain".to_string())
);
assert_eq!(simulation_response.body_str(), Some("Hello, World!"));
}
#[test]
fn test_html_response_conversion() {
let html_content = "<h1>Hello, World!</h1><p>This is a test.</p>";
let http_response = HttpResponse::html(html_content);
let simulation_response = convert_http_response_to_simulation_response(http_response);
assert_eq!(simulation_response.status, 200);
assert_eq!(
simulation_response.headers.get("Content-Type"),
Some(&"text/html; charset=utf-8".to_string())
);
assert_eq!(simulation_response.body_str(), Some(html_content));
}
#[test]
fn test_text_response_conversion() {
let text_content = "This is plain text content.";
let http_response = HttpResponse::text(text_content);
let simulation_response = convert_http_response_to_simulation_response(http_response);
assert_eq!(simulation_response.status, 200);
assert_eq!(
simulation_response.headers.get("Content-Type"),
Some(&"text/plain; charset=utf-8".to_string())
);
assert_eq!(simulation_response.body_str(), Some(text_content));
}
#[test]
fn test_location_header_backwards_compatibility() {
let redirect_response =
HttpResponse::temporary_redirect().with_location("https://example.com/new-location");
let simulation_response = convert_http_response_to_simulation_response(redirect_response);
assert_eq!(simulation_response.status, 307);
assert_eq!(
simulation_response.headers.get("Location"),
Some(&"https://example.com/new-location".to_string())
);
}
#[test]
fn test_simulator_state_management_string_state() {
let server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let test_string = "Hello, World!".to_string();
server.insert_state(test_string.clone());
let retrieved_state: Option<Arc<String>> = server.get_state();
assert!(retrieved_state.is_some());
assert_eq!(*retrieved_state.unwrap(), test_string);
}
#[test]
fn test_simulator_state_management_custom_struct_state() {
#[derive(Debug, Clone, PartialEq)]
struct AppConfig {
name: String,
version: u32,
debug: bool,
}
let server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let config = AppConfig {
name: "TestApp".to_string(),
version: 42,
debug: true,
};
server.insert_state(config.clone());
let retrieved_config: Option<Arc<AppConfig>> = server.get_state();
assert!(retrieved_config.is_some());
assert_eq!(*retrieved_config.unwrap(), config);
}
#[test]
fn test_simulator_state_management_multiple_types() {
let server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
server.insert_state("Hello".to_string());
server.insert_state(42u32);
server.insert_state(true);
let string_state: Option<Arc<String>> = server.get_state();
let u32_state: Option<Arc<u32>> = server.get_state();
let bool_state: Option<Arc<bool>> = server.get_state();
assert!(string_state.is_some());
assert!(u32_state.is_some());
assert!(bool_state.is_some());
assert_eq!(*string_state.unwrap(), "Hello");
assert_eq!(*u32_state.unwrap(), 42);
assert!(*bool_state.unwrap());
}
#[test]
fn test_simulator_state_management_shared_across_requests() {
let server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let shared_data = "Shared across requests".to_string();
server.insert_state(shared_data.clone());
let request1 = SimulationRequest::new(Method::Get, "/test1");
let request2 = SimulationRequest::new(Method::Get, "/test2");
let stub1 = SimulationStub::new(request1).with_state_container(Arc::clone(&server.state));
let stub2 = SimulationStub::new(request2).with_state_container(Arc::clone(&server.state));
let state1: Option<Arc<String>> = stub1.state();
let state2: Option<Arc<String>> = stub2.state();
assert!(state1.is_some());
assert!(state2.is_some());
assert_eq!(*state1.unwrap(), shared_data);
assert_eq!(*state2.unwrap(), shared_data);
let state1_again: Option<Arc<String>> = stub1.state();
let state2_again: Option<Arc<String>> = stub2.state();
assert!(Arc::ptr_eq(&state1_again.unwrap(), &state2_again.unwrap()));
}
#[test]
fn test_simulator_state_management_handler_extraction() {
use crate::{extractors::state::State, from_request::FromRequest};
let server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let app_name = "TestApplication".to_string();
server.insert_state(app_name.clone());
let request = SimulationRequest::new(Method::Get, "/app-info");
let simulation_stub =
SimulationStub::new(request).with_state_container(Arc::clone(&server.state));
let http_request = crate::HttpRequest::new(simulation_stub);
let state_result: Result<State<String>, _> = State::from_request_sync(&http_request);
assert!(state_result.is_ok());
let State(extracted_name) = state_result.unwrap();
assert_eq!(*extracted_name, app_name);
}
#[test]
fn test_register_scope_with_single_route() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let scope = crate::Scope::new("/api").with_route(crate::Route::new(
Method::Get,
"/users",
|_req: crate::HttpRequest| {
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body("test response")) })
},
));
server.register_scope(&scope);
assert!(
server
.routes
.contains_key(&(Method::Get, "/api/users".to_string()))
);
assert_eq!(server.routes.len(), 1);
}
#[test]
fn test_register_scope_with_multiple_routes() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let scope = crate::Scope::new("/api")
.with_route(crate::Route::new(
Method::Get,
"/users",
|_req: crate::HttpRequest| {
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body("get users")) })
},
))
.with_route(crate::Route::new(
Method::Post,
"/users",
|_req: crate::HttpRequest| {
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body("create user")) })
},
));
server.register_scope(&scope);
assert!(
server
.routes
.contains_key(&(Method::Get, "/api/users".to_string()))
);
assert!(
server
.routes
.contains_key(&(Method::Post, "/api/users".to_string()))
);
assert_eq!(server.routes.len(), 2);
}
#[test]
fn test_register_scope_with_nested_scopes() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let nested_scope = crate::Scope::new("/v1").with_route(crate::Route::new(
Method::Get,
"/users",
|_req: crate::HttpRequest| {
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body("v1 users")) })
},
));
let scope = crate::Scope::new("/api")
.with_route(crate::Route::new(
Method::Get,
"/health",
|_req: crate::HttpRequest| {
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body("healthy")) })
},
))
.with_scope(nested_scope)
.with_route(crate::Route::new(
Method::Post,
"/auth",
|_req: crate::HttpRequest| {
Box::pin(
async move { Ok(crate::HttpResponse::ok().with_body("authenticated")) },
)
},
));
server.register_scope(&scope);
assert!(
server
.routes
.contains_key(&(Method::Get, "/api/health".to_string()))
);
assert!(
server
.routes
.contains_key(&(Method::Get, "/api/v1/users".to_string()))
);
assert!(
server
.routes
.contains_key(&(Method::Post, "/api/auth".to_string()))
);
assert_eq!(server.routes.len(), 3);
}
#[test]
fn test_register_scope_with_empty_prefix() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let scope = crate::Scope::new("").with_route(crate::Route::new(
Method::Get,
"/users",
|_req: crate::HttpRequest| {
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body("users")) })
},
));
server.register_scope(&scope);
assert!(
server
.routes
.contains_key(&(Method::Get, "/users".to_string()))
);
assert_eq!(server.routes.len(), 1);
}
#[test]
fn test_register_scope_with_deeply_nested_scopes() {
let mut server = SimulatorWebServer {
static_files: None,
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(RwLock::new(crate::extractors::state::StateContainer::new())),
};
let admin_scope = crate::Scope::new("/admin").with_route(crate::Route::new(
Method::Delete,
"/users/{id}",
|_req: crate::HttpRequest| {
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body("user deleted")) })
},
));
let v1_scope = crate::Scope::new("/v1").with_scope(admin_scope);
let api_scope = crate::Scope::new("/api").with_scope(v1_scope);
server.register_scope(&api_scope);
assert!(
server
.routes
.contains_key(&(Method::Delete, "/api/v1/admin/users/{id}".to_string()))
);
assert_eq!(server.routes.len(), 1);
}
#[test]
fn test_simulation_request_with_single_cookie() {
let req = SimulationRequest::new(Method::Get, "/test").with_cookie("session_id", "abc123");
assert_eq!(req.cookies.len(), 1);
assert_eq!(req.cookies.get("session_id"), Some(&"abc123".to_string()));
}
#[test]
fn test_simulation_request_with_multiple_cookies() {
let cookies = vec![
("session_id".to_string(), "abc123".to_string()),
("user_pref".to_string(), "dark_mode".to_string()),
("tracking".to_string(), "opt_out".to_string()),
];
let req = SimulationRequest::new(Method::Get, "/test").with_cookies(cookies);
assert_eq!(req.cookies.len(), 3);
assert_eq!(req.cookies.get("session_id"), Some(&"abc123".to_string()));
assert_eq!(req.cookies.get("user_pref"), Some(&"dark_mode".to_string()));
assert_eq!(req.cookies.get("tracking"), Some(&"opt_out".to_string()));
}
#[test]
fn test_simulation_request_with_remote_addr() {
let req =
SimulationRequest::new(Method::Get, "/test").with_remote_addr("192.168.1.100:8080");
assert_eq!(req.remote_addr, Some("192.168.1.100:8080".to_string()));
}
#[test]
fn test_simulation_request_with_multiple_path_params() {
let mut params = PathParams::new();
params.insert("user_id".to_string(), "123".to_string());
params.insert("post_id".to_string(), "456".to_string());
let req = SimulationRequest::new(Method::Get, "/users/{user_id}/posts/{post_id}")
.with_path_params(params);
assert_eq!(req.path_params.len(), 2);
assert_eq!(req.path_params.get("user_id"), Some(&"123".to_string()));
assert_eq!(req.path_params.get("post_id"), Some(&"456".to_string()));
}
#[test]
fn test_simulation_request_chained_builder() {
let mut path_params = PathParams::new();
path_params.insert("id".to_string(), "42".to_string());
let req = SimulationRequest::new(Method::Post, "/api/resource")
.with_query_string("format=json&pretty=true")
.with_header("Content-Type", "application/json")
.with_header("Authorization", "Bearer token123")
.with_cookie("session", "xyz789")
.with_remote_addr("10.0.0.1:12345")
.with_path_params(path_params)
.with_body(r#"{"name": "test"}"#);
assert_eq!(req.method, Method::Post);
assert_eq!(req.path, "/api/resource");
assert_eq!(req.query_string, "format=json&pretty=true");
assert_eq!(
req.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
assert_eq!(
req.headers.get("Authorization"),
Some(&"Bearer token123".to_string())
);
assert_eq!(req.cookies.get("session"), Some(&"xyz789".to_string()));
assert_eq!(req.remote_addr, Some("10.0.0.1:12345".to_string()));
assert_eq!(req.path_params.get("id"), Some(&"42".to_string()));
assert!(req.body.is_some());
}
#[test]
fn test_simulation_stub_cookie_access() {
let req = SimulationRequest::new(Method::Get, "/test")
.with_cookie("auth_token", "secret123")
.with_cookie("user_id", "user_42");
let stub = SimulationStub::new(req);
assert_eq!(stub.cookie("auth_token"), Some("secret123"));
assert_eq!(stub.cookie("user_id"), Some("user_42"));
assert_eq!(stub.cookie("nonexistent"), None);
}
#[test]
fn test_simulation_stub_cookies_returns_all() {
let req = SimulationRequest::new(Method::Get, "/test")
.with_cookie("a", "1")
.with_cookie("b", "2")
.with_cookie("c", "3");
let stub = SimulationStub::new(req);
let cookies = stub.cookies();
assert_eq!(cookies.len(), 3);
assert_eq!(cookies.get("a"), Some(&"1".to_string()));
assert_eq!(cookies.get("b"), Some(&"2".to_string()));
assert_eq!(cookies.get("c"), Some(&"3".to_string()));
}
#[test]
fn test_simulation_stub_remote_addr_access() {
let req = SimulationRequest::new(Method::Get, "/test").with_remote_addr("127.0.0.1:55555");
let stub = SimulationStub::new(req);
assert_eq!(stub.remote_addr(), Some("127.0.0.1:55555"));
}
#[test]
fn test_simulation_stub_remote_addr_none() {
let req = SimulationRequest::new(Method::Get, "/test");
let stub = SimulationStub::new(req);
assert_eq!(stub.remote_addr(), None);
}
#[test]
fn test_simulation_stub_path_param_access() {
let mut params = PathParams::new();
params.insert("id".to_string(), "999".to_string());
params.insert("action".to_string(), "edit".to_string());
let req =
SimulationRequest::new(Method::Get, "/resource/{id}/{action}").with_path_params(params);
let stub = SimulationStub::new(req);
assert_eq!(stub.path_param("id"), Some("999"));
assert_eq!(stub.path_param("action"), Some("edit"));
assert_eq!(stub.path_param("nonexistent"), None);
}
#[test]
fn test_simulation_stub_with_state_container() {
use crate::extractors::state::StateContainer;
#[derive(Debug, Clone, PartialEq)]
struct TestState {
value: i32,
}
let req = SimulationRequest::new(Method::Get, "/test");
let mut container = StateContainer::new();
container.insert(TestState { value: 42 });
let stub = SimulationStub::new(req).with_state_container(Arc::new(RwLock::new(container)));
let retrieved_state = stub.state::<TestState>();
assert!(retrieved_state.is_some());
assert_eq!(retrieved_state.unwrap().value, 42);
}
#[test]
fn test_simulation_stub_state_not_found() {
use crate::extractors::state::StateContainer;
#[derive(Debug, Clone)]
struct UnregisteredState;
let req = SimulationRequest::new(Method::Get, "/test");
let container = StateContainer::new();
let stub = SimulationStub::new(req).with_state_container(Arc::new(RwLock::new(container)));
let result = stub.state::<UnregisteredState>();
assert!(result.is_none());
}
#[test]
fn test_simulation_stub_app_state_access() {
use crate::extractors::state::StateContainer;
let req = SimulationRequest::new(Method::Get, "/test");
let container = Arc::new(RwLock::new(StateContainer::new()));
let stub = SimulationStub::new(req).with_state_container(Arc::clone(&container));
assert!(stub.app_state().is_some());
}
#[test]
fn test_simulation_stub_app_state_none_when_not_set() {
let req = SimulationRequest::new(Method::Get, "/test");
let stub = SimulationStub::new(req);
assert!(stub.app_state().is_none());
}
#[test]
fn test_simulation_stub_from_simulation_request() {
let req =
SimulationRequest::new(Method::Post, "/api/data").with_header("X-Custom", "value");
let stub: SimulationStub = req.into();
assert_eq!(stub.method(), &Method::Post);
assert_eq!(stub.path(), "/api/data");
assert_eq!(stub.header("X-Custom"), Some("value"));
}
#[test]
fn test_match_path_with_multiple_parameters() {
let pattern = parse_path_pattern("/users/{user_id}/posts/{post_id}/comments/{comment_id}");
let result = match_path(&pattern, "/users/123/posts/456/comments/789");
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("user_id"), Some(&"123".to_string()));
assert_eq!(params.get("post_id"), Some(&"456".to_string()));
assert_eq!(params.get("comment_id"), Some(&"789".to_string()));
}
#[test]
fn test_match_path_parameter_with_special_characters() {
let pattern = parse_path_pattern("/files/{filename}");
let result = match_path(&pattern, "/files/my-document_v2.pdf");
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(
params.get("filename"),
Some(&"my-document_v2.pdf".to_string())
);
}
#[test]
fn test_match_path_empty_parameter_value() {
let pattern = parse_path_pattern("/api/{version}/resource");
let result = match_path(&pattern, "/api//resource");
assert!(result.is_none());
}
#[test]
fn test_parse_path_pattern_root_only() {
let pattern = parse_path_pattern("/");
assert!(pattern.segments().is_empty());
}
#[test]
fn test_parse_path_pattern_single_parameter() {
let pattern = parse_path_pattern("/{id}");
assert_eq!(pattern.segments().len(), 1);
assert_eq!(
pattern.segments()[0],
PathSegment::Parameter("id".to_string())
);
}
#[test]
fn test_parse_path_pattern_consecutive_literals() {
let pattern = parse_path_pattern("/api/v1/users/list");
assert_eq!(pattern.segments().len(), 4);
assert!(
pattern
.segments()
.iter()
.all(|s| matches!(s, PathSegment::Literal(_)))
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_html() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.html"),
"text/html; charset=utf-8"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.htm"),
"text/html; charset=utf-8"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_css() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.css"),
"text/css; charset=utf-8"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_javascript() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.js"),
"application/javascript; charset=utf-8"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.mjs"),
"application/javascript; charset=utf-8"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_json() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.json"),
"application/json; charset=utf-8"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_images() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.png"),
"image/png"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.jpg"),
"image/jpeg"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.jpeg"),
"image/jpeg"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.gif"),
"image/gif"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.svg"),
"image/svg+xml"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.webp"),
"image/webp"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.ico"),
"image/x-icon"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_fonts() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.woff"),
"font/woff"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.woff2"),
"font/woff2"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.ttf"),
"font/ttf"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.otf"),
"font/otf"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.eot"),
"application/vnd.ms-fontobject"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_other() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.txt"),
"text/plain; charset=utf-8"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.xml"),
"application/xml; charset=utf-8"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.pdf"),
"application/pdf"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.map"),
"application/json"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.wasm"),
"application/wasm"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_unknown() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.unknown"),
"application/octet-stream"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file"),
"application/octet-stream"
);
}
#[test]
#[cfg(feature = "simulator")]
fn test_get_mime_type_case_insensitive() {
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.HTML"),
"text/html; charset=utf-8"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.CSS"),
"text/css; charset=utf-8"
);
assert_eq!(
SimulatorWebServer::get_mime_type("/path/file.PNG"),
"image/png"
);
}
#[test]
fn test_simulator_static_files_none_by_default() {
let server = SimulatorWebServer::new(Vec::new());
assert!(server.static_files().is_none());
}
#[test]
fn test_simulator_with_static_files_constructor() {
let config = crate::StaticFiles::new("/static", "./public");
let server = SimulatorWebServer::with_static_files(Vec::new(), config);
let sf = server.static_files().unwrap();
assert_eq!(sf.mount_path(), "/static");
assert_eq!(sf.directory(), &std::path::PathBuf::from("./public"));
}
#[test]
fn test_web_server_builder_static_files() {
let builder = crate::WebServerBuilder::new()
.with_static_files(crate::StaticFiles::new("/assets", "./dist"));
let sf = builder.static_files().unwrap();
assert_eq!(sf.mount_path(), "/assets");
assert_eq!(sf.directory(), &std::path::PathBuf::from("./dist"));
}
#[test]
fn test_web_server_builder_static_files_none_by_default() {
let builder = crate::WebServerBuilder::new();
assert!(builder.static_files().is_none());
}
}