use std::collections::BTreeMap;
use super::traits::GenericTestClient;
use super::{HttpMethod, TestClient, TestRequestBuilder, TestResponse};
use crate::{
PathParams,
simulator::{SimulationRequest, SimulatorWebServer},
};
use bytes::Bytes;
use switchy_http_models::Method;
pub struct SimulatorTestClient {
server: SimulatorWebServer,
}
impl SimulatorTestClient {
#[must_use]
pub const fn new(server: SimulatorWebServer) -> Self {
Self { server }
}
#[must_use]
pub const fn server(&self) -> &SimulatorWebServer {
&self.server
}
#[must_use]
pub const fn server_mut(&mut self) -> &mut SimulatorWebServer {
&mut self.server
}
}
#[derive(Debug, thiserror::Error)]
pub enum SimulatorTestClientError {
#[error("Request processing failed: {0}")]
RequestProcessing(String),
#[error("Invalid request data: {0}")]
InvalidRequest(String),
}
impl TestClient for SimulatorTestClient {
type Error = SimulatorTestClientError;
fn get(&self, path: &str) -> TestRequestBuilder<'_, Self> {
TestRequestBuilder::new(self, HttpMethod::Get, path.to_string())
}
fn post(&self, path: &str) -> TestRequestBuilder<'_, Self> {
TestRequestBuilder::new(self, HttpMethod::Post, path.to_string())
}
fn put(&self, path: &str) -> TestRequestBuilder<'_, Self> {
TestRequestBuilder::new(self, HttpMethod::Put, path.to_string())
}
fn delete(&self, path: &str) -> TestRequestBuilder<'_, Self> {
TestRequestBuilder::new(self, HttpMethod::Delete, path.to_string())
}
fn execute_request(
&self,
method: &str,
path: &str,
headers: &BTreeMap<String, String>,
body: Option<&[u8]>,
) -> Result<TestResponse, Self::Error> {
let method_enum = match method.to_uppercase().as_str() {
"GET" => Method::Get,
"POST" => Method::Post,
"PUT" => Method::Put,
"DELETE" => Method::Delete,
"PATCH" => Method::Patch,
"HEAD" => Method::Head,
"OPTIONS" => Method::Options,
_ => {
return Err(SimulatorTestClientError::InvalidRequest(format!(
"Unsupported HTTP method: {method}"
)));
}
};
let (path_part, query_string) = path.find('?').map_or_else(
|| (path.to_string(), String::new()),
|pos| (path[..pos].to_string(), path[pos + 1..].to_string()),
);
let request = SimulationRequest {
method: method_enum,
path: path_part,
query_string,
headers: headers.clone(),
body: body.map(|b| Bytes::from(b.to_vec())),
cookies: BTreeMap::new(),
remote_addr: None,
path_params: PathParams::new(),
};
let response = futures::executor::block_on(self.server.process_request(request));
let response_body = response.body.unwrap_or_default().to_vec();
Ok(TestResponse::new(
response.status,
response.headers,
response_body,
))
}
}
impl GenericTestClient for SimulatorTestClient {
type Error = SimulatorTestClientError;
fn execute_request(
&self,
method: &str,
path: &str,
headers: &BTreeMap<String, String>,
body: Option<&[u8]>,
) -> Result<TestResponse, Self::Error> {
<Self as TestClient>::execute_request(self, method, path, headers, body)
}
fn base_url(&self) -> String {
"http://simulator".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simulator::SimulatorWebServer;
use crate::test_client::TestResponseExt;
#[test]
fn test_simulator_test_client_get() {
let mut server = create_test_server();
server.register_route(
switchy_http_models::Method::Get,
"/test",
Box::new(|_req| {
Box::pin(async { Ok(crate::HttpResponse::ok().with_body("Hello, World!")) })
}),
);
let client = SimulatorTestClient::new(server);
let response = client.get("/test").send().expect("Request should succeed");
response
.assert_status(200)
.assert_text_equals("Hello, World!");
}
fn create_test_server() -> SimulatorWebServer {
use std::sync::Arc;
SimulatorWebServer {
scopes: Vec::new(),
routes: BTreeMap::new(),
state: Arc::new(std::sync::RwLock::new(
crate::extractors::state::StateContainer::new(),
)),
static_files: None,
}
}
#[test]
#[cfg(feature = "serde")]
fn test_simulator_test_client_post_json() {
let mut server = create_test_server();
server.register_route(
switchy_http_models::Method::Post,
"/echo",
Box::new(|req| {
let body_str = req.body().map_or_else(
|| "{}".to_string(),
|body| String::from_utf8_lossy(body).to_string(),
);
Box::pin(async move {
Ok(crate::HttpResponse::ok()
.with_content_type("application/json")
.with_body(body_str))
})
}),
);
let client = SimulatorTestClient::new(server);
let test_data = serde_json::json!({"message": "test"});
let response = client
.post("/echo")
.json(&test_data)
.send()
.expect("Request should succeed");
response
.assert_status(200)
.assert_header("Content-Type", "application/json")
.assert_json_equals(&test_data);
}
#[test]
fn test_simulator_test_client_with_headers() {
let mut server = create_test_server();
server.register_route(
switchy_http_models::Method::Get,
"/auth",
Box::new(|req| {
let auth_header = req
.header("authorization")
.unwrap_or("No auth header")
.to_string();
Box::pin(async move { Ok(crate::HttpResponse::ok().with_body(auth_header)) })
}),
);
let client = SimulatorTestClient::new(server);
let response = client
.get("/auth")
.bearer_token("test-token")
.send()
.expect("Request should succeed");
response
.assert_status(200)
.assert_text_equals("Bearer test-token");
}
#[test]
fn test_simulator_test_client_404() {
let server = create_test_server();
let client = SimulatorTestClient::new(server);
let response = client
.get("/nonexistent")
.send()
.expect("Request should succeed");
response.assert_status(404);
}
#[test]
fn test_simulator_test_client_form_data() {
let mut server = create_test_server();
server.register_route(
switchy_http_models::Method::Post,
"/form",
Box::new(|req| {
let body_str = req.body().map_or_else(String::new, |body| {
String::from_utf8_lossy(body).to_string()
});
Box::pin(async move {
let response_body =
if body_str.contains("name=test") && body_str.contains("value=123") {
"Form processed successfully"
} else {
"Invalid form data"
};
Ok(crate::HttpResponse::ok().with_body(response_body.to_string()))
})
}),
);
let client = SimulatorTestClient::new(server);
let response = client
.post("/form")
.form([("name", "test"), ("value", "123")])
.send()
.expect("Request should succeed");
response
.assert_status(200)
.assert_text_equals("Form processed successfully");
}
}