use std::collections::BTreeMap;
#[cfg(feature = "serde")]
use serde::Serialize;
mod macros;
mod traits;
mod wrappers;
pub use traits::{GenericTestClient, GenericTestServer};
pub use wrappers::{
TestClientWrapper, TestClientWrapperError, TestServerWrapper, TestServerWrapperError,
};
#[cfg(all(feature = "actix", not(feature = "simulator")))]
pub mod actix_impl;
pub mod simulator_impl;
pub mod request_builder;
pub mod response;
pub use request_builder::TestRequestBuilder;
pub use response::{TestResponse, TestResponseExt};
pub trait TestClient {
type Error: std::error::Error + Send + Sync + 'static;
fn get(&self, path: &str) -> TestRequestBuilder<'_, Self>;
fn post(&self, path: &str) -> TestRequestBuilder<'_, Self>;
fn put(&self, path: &str) -> TestRequestBuilder<'_, Self>;
fn delete(&self, path: &str) -> TestRequestBuilder<'_, Self>;
fn execute_request(
&self,
method: &str,
path: &str,
headers: &BTreeMap<String, String>,
body: Option<&[u8]>,
) -> Result<TestResponse, Self::Error>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
}
impl HttpMethod {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Patch => "PATCH",
Self::Head => "HEAD",
Self::Options => "OPTIONS",
}
}
}
#[derive(Debug, Clone)]
pub enum RequestBody {
Bytes(Vec<u8>),
#[cfg(feature = "serde")]
Json(serde_json::Value),
Form(BTreeMap<String, String>),
Text(String),
}
impl RequestBody {
#[cfg(feature = "serde")]
pub fn to_bytes_and_content_type(&self) -> Result<(Vec<u8>, String), serde_json::Error> {
match self {
Self::Bytes(bytes) => Ok((bytes.clone(), "application/octet-stream".to_string())),
Self::Json(value) => {
let bytes = serde_json::to_vec(value)?;
Ok((bytes, "application/json".to_string()))
}
Self::Form(form) => {
let encoded = form
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
Ok((
encoded.into_bytes(),
"application/x-www-form-urlencoded".to_string(),
))
}
Self::Text(text) => Ok((text.as_bytes().to_vec(), "text/plain".to_string())),
}
}
#[cfg(not(feature = "serde"))]
#[allow(clippy::must_use_candidate)]
pub fn to_bytes_and_content_type(&self) -> Result<(Vec<u8>, String), std::convert::Infallible> {
match self {
Self::Bytes(bytes) => Ok((bytes.clone(), "application/octet-stream".to_string())),
Self::Form(form) => {
let encoded = form
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
Ok((
encoded.into_bytes(),
"application/x-www-form-urlencoded".to_string(),
))
}
Self::Text(text) => Ok((text.as_bytes().to_vec(), "text/plain".to_string())),
}
}
#[cfg(feature = "serde")]
pub fn json<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> {
let json_value = serde_json::to_value(value)?;
Ok(Self::Json(json_value))
}
#[must_use]
pub fn form<K: Into<String>, V: Into<String>>(data: impl IntoIterator<Item = (K, V)>) -> Self {
let form = data
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
Self::Form(form)
}
}
use macros::impl_test_client;
impl_test_client!(
simulator_impl::SimulatorTestClient,
crate::simulator::SimulatorWebServer
);
pub use ConcreteTestClient as TestClientImpl;
pub use ConcreteTestServer as TestServerImpl;
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
fn test_http_method_as_str() {
assert_eq!(HttpMethod::Get.as_str(), "GET");
assert_eq!(HttpMethod::Post.as_str(), "POST");
assert_eq!(HttpMethod::Put.as_str(), "PUT");
assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
assert_eq!(HttpMethod::Patch.as_str(), "PATCH");
assert_eq!(HttpMethod::Head.as_str(), "HEAD");
assert_eq!(HttpMethod::Options.as_str(), "OPTIONS");
}
#[test_log::test]
fn test_request_body_bytes_to_bytes_and_content_type() {
let body = RequestBody::Bytes(vec![1, 2, 3, 4]);
let (bytes, content_type) = body.to_bytes_and_content_type().unwrap();
assert_eq!(bytes, vec![1, 2, 3, 4]);
assert_eq!(content_type, "application/octet-stream");
}
#[test_log::test]
fn test_request_body_text_to_bytes_and_content_type() {
let body = RequestBody::Text("Hello, World!".to_string());
let (bytes, content_type) = body.to_bytes_and_content_type().unwrap();
assert_eq!(bytes, b"Hello, World!");
assert_eq!(content_type, "text/plain");
}
#[test_log::test]
fn test_request_body_form_to_bytes_and_content_type() {
let body = RequestBody::form([("key1", "value1"), ("key2", "value2")]);
let (bytes, content_type) = body.to_bytes_and_content_type().unwrap();
let body_str = String::from_utf8(bytes).unwrap();
assert!(body_str.contains("key1=value1"));
assert!(body_str.contains("key2=value2"));
assert_eq!(content_type, "application/x-www-form-urlencoded");
}
#[test_log::test]
fn test_request_body_form_with_special_characters() {
let body = RequestBody::form([("name", "John Doe"), ("email", "test@example.com")]);
let (bytes, _) = body.to_bytes_and_content_type().unwrap();
let body_str = String::from_utf8(bytes).unwrap();
assert!(body_str.contains("name=John%20Doe") || body_str.contains("name=John+Doe"));
assert!(body_str.contains("test%40example.com"));
}
#[test_log::test]
#[cfg(feature = "serde")]
fn test_request_body_json_to_bytes_and_content_type() {
let body = RequestBody::Json(serde_json::json!({"key": "value", "number": 42}));
let (bytes, content_type) = body.to_bytes_and_content_type().unwrap();
let parsed: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(parsed["key"], "value");
assert_eq!(parsed["number"], 42);
assert_eq!(content_type, "application/json");
}
#[test_log::test]
#[cfg(feature = "serde")]
fn test_request_body_json_from_serialize() {
#[derive(serde::Serialize)]
struct TestData {
name: String,
value: i32,
}
let data = TestData {
name: "test".to_string(),
value: 123,
};
let body = RequestBody::json(&data).unwrap();
match body {
RequestBody::Json(value) => {
assert_eq!(value["name"], "test");
assert_eq!(value["value"], 123);
}
_ => panic!("Expected Json variant"),
}
}
#[test_log::test]
fn test_request_body_form_from_iterator() {
let vec_data: Vec<(String, String)> = vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string()),
];
let body = RequestBody::form(vec_data);
match body {
RequestBody::Form(form) => {
assert_eq!(form.get("a"), Some(&"1".to_string()));
assert_eq!(form.get("b"), Some(&"2".to_string()));
}
_ => panic!("Expected Form variant"),
}
}
#[test_log::test]
fn test_request_body_form_empty() {
let empty: Vec<(String, String)> = vec![];
let body = RequestBody::form(empty);
let (bytes, content_type) = body.to_bytes_and_content_type().unwrap();
assert!(bytes.is_empty());
assert_eq!(content_type, "application/x-www-form-urlencoded");
}
#[test_log::test]
fn test_http_method_clone_and_eq() {
let method1 = HttpMethod::Get;
let method2 = method1.clone();
assert_eq!(method1, method2);
assert_ne!(HttpMethod::Get, HttpMethod::Post);
}
#[test_log::test]
fn test_request_builder_basic_auth_with_password() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/api/test").basic_auth("user", Some("pass123"));
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_basic_auth_without_password() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/test").basic_auth("username_only", None);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_query_appends_to_existing_query_string() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/api/search?existing=value");
let builder = builder.query([("new_param", "new_value")]);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_query_creates_query_string_when_none_exists() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/api/search");
let builder = builder.query([("q", "rust"), ("limit", "10")]);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_query_with_empty_params() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/api/test");
let empty_params: Vec<(&str, &str)> = vec![];
let builder = builder.query(empty_params);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_query_url_encodes_special_chars() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/api/search");
let builder = builder.query([("query", "hello world"), ("filter", "a=b&c=d")]);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_bearer_token() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client
.get("/api/protected")
.bearer_token("my_jwt_token_123");
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_user_agent() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/api/test").user_agent("TestClient/1.0.0");
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_headers_multiple() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client.get("/api/test").headers([
("X-Custom-Header-1", "value1"),
("X-Custom-Header-2", "value2"),
("X-Request-ID", "abc123"),
]);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_body_bytes() {
let client = ConcreteTestClient::new_with_test_routes();
let binary_data = vec![0x00, 0x01, 0x02, 0x03, 0xFF];
let builder = client.post("/api/upload").body_bytes(binary_data);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_text_body() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client
.post("/api/text")
.text("Hello, this is plain text body".to_string());
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_form_post() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client
.post("/api/form")
.form_post([("username", "john"), ("password", "secret")]);
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_content_type_explicit() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client
.post("/api/data")
.content_type("application/xml")
.text("<root><value>test</value></root>".to_string());
let response = builder.send();
let _ = response;
}
#[test_log::test]
fn test_request_builder_authorization_header() {
let client = ConcreteTestClient::new_with_test_routes();
let builder = client
.get("/api/protected")
.authorization("Custom auth-scheme token-value");
let response = builder.send();
let _ = response;
}
}