use std::collections::HashMap;
use serde_json::{Value as JsonValue, json};
use crate::{TestError, TestResult};
#[derive(Clone)]
pub struct TestClient {
base_url: String,
headers: HashMap<String, String>,
auth_token: Option<String>,
}
impl TestClient {
pub fn new() -> Self {
Self {
base_url: "http://localhost:3000".to_string(),
headers: HashMap::new(),
auth_token: None,
}
}
pub fn with_base_url(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
headers: HashMap::new(),
auth_token: None,
}
}
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: HashMap<String, String>) -> Self {
self.headers.extend(headers);
self
}
pub fn authenticated_with_token(mut self, token: impl Into<String>) -> Self {
let token = token.into();
self.auth_token = Some(token.clone());
self.headers.insert("Authorization".to_string(), format!("Bearer {}", token));
self
}
pub fn authenticated_as<T>(self, _user: &T) -> Self
where
T: AuthenticatedUser,
{
let token = "test_jwt_token"; self.authenticated_with_token(token)
}
pub fn get(self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(self, "GET".to_string(), path.into())
}
pub fn post(self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(self, "POST".to_string(), path.into())
}
pub fn put(self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(self, "PUT".to_string(), path.into())
}
pub fn patch(self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(self, "PATCH".to_string(), path.into())
}
pub fn delete(self, path: impl Into<String>) -> RequestBuilder {
RequestBuilder::new(self, "DELETE".to_string(), path.into())
}
}
impl Default for TestClient {
fn default() -> Self {
Self::new()
}
}
pub trait AuthenticatedUser {
fn id(&self) -> String;
fn roles(&self) -> Vec<String> {
vec![]
}
fn permissions(&self) -> Vec<String> {
vec![]
}
}
pub struct RequestBuilder {
client: TestClient,
method: String,
path: String,
headers: HashMap<String, String>,
body: Option<String>,
query_params: HashMap<String, String>,
}
impl RequestBuilder {
fn new(client: TestClient, method: String, path: String) -> Self {
Self {
client,
method,
path,
headers: HashMap::new(),
body: None,
query_params: HashMap::new(),
}
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn json<T: serde::Serialize>(mut self, data: &T) -> Self {
match serde_json::to_string(data) {
Ok(json_str) => {
self.body = Some(json_str);
self.headers.insert("Content-Type".to_string(), "application/json".to_string());
},
Err(_) => {
}
}
self
}
pub fn form(mut self, data: HashMap<String, String>) -> Self {
let form_data = data.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
self.body = Some(form_data);
self.headers.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.query_params.insert(key.into(), value.into());
self
}
pub fn queries(mut self, params: HashMap<String, String>) -> Self {
self.query_params.extend(params);
self
}
pub async fn send(self) -> TestResult<TestResponse> {
let mut url = format!("{}{}", self.client.base_url, self.path);
if !self.query_params.is_empty() {
let query_string = self.query_params.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join("&");
url.push_str(&format!("?{}", query_string));
}
let response = TestResponse {
status_code: 200,
headers: {
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
headers
},
body: json!({"message": "Test response", "method": self.method, "path": self.path}).to_string(),
};
Ok(response)
}
}
pub struct TestResponse {
status_code: u16,
headers: HashMap<String, String>,
body: String,
}
impl TestResponse {
pub fn status(&self) -> u16 {
self.status_code
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub fn body(&self) -> &str {
&self.body
}
pub fn json(&self) -> TestResult<JsonValue> {
let json_value: JsonValue = serde_json::from_str(&self.body)?;
Ok(json_value)
}
pub fn assert_status(self, expected_status: u16) -> Self {
if self.status_code != expected_status {
panic!("Expected status {}, got {}", expected_status, self.status_code);
}
self
}
pub fn assert_success(self) -> Self {
if self.status_code < 200 || self.status_code >= 300 {
panic!("Expected successful status, got {}", self.status_code);
}
self
}
pub fn assert_header(self, name: &str, expected_value: &str) -> Self {
if let Some(value) = self.headers.get(name) {
if value != expected_value {
panic!("Expected header '{}' to be '{}', got '{}'", name, expected_value, value);
}
} else {
panic!("Expected header '{}' not found", name);
}
self
}
pub fn assert_header_exists(self, name: &str) -> Self {
if !self.headers.contains_key(name) {
panic!("Expected header '{}' to exist", name);
}
self
}
pub fn assert_json_contains(self, expected: JsonValue) -> TestResult<Self> {
let actual_json = self.json()?;
if !json_contains(&actual_json, &expected) {
return Err(TestError::Assertion {
message: format!("Expected JSON to contain: {}, got: {}", expected, actual_json),
});
}
Ok(self)
}
pub fn assert_json_equals(self, expected: JsonValue) -> TestResult<Self> {
let actual_json = self.json()?;
if actual_json != expected {
return Err(TestError::Assertion {
message: format!("Expected JSON: {}, got: {}", expected, actual_json),
});
}
Ok(self)
}
pub fn assert_body_contains(self, expected_text: &str) -> TestResult<Self> {
let body = self.body();
if !body.contains(expected_text) {
return Err(TestError::Assertion {
message: format!("Expected body to contain '{}', got: {}", expected_text, body),
});
}
Ok(self)
}
pub fn assert_validation_error(self, field: &str, _error_type: &str) -> TestResult<Self> {
let json = self.json()?;
if let Some(errors) = json.get("errors") {
if let Some(field_errors) = errors.get(field) {
if field_errors.as_array().map_or(false, |arr| !arr.is_empty()) {
return Ok(self);
}
}
}
Err(TestError::Assertion {
message: format!("Expected validation error for field '{}', got: {}", field, json),
})
}
}
fn json_contains(actual: &JsonValue, expected: &JsonValue) -> bool {
match (actual, expected) {
(JsonValue::Object(actual_map), JsonValue::Object(expected_map)) => {
for (key, expected_value) in expected_map {
if let Some(actual_value) = actual_map.get(key) {
if !json_contains(actual_value, expected_value) {
return false;
}
} else {
return false;
}
}
true
},
(JsonValue::Array(actual_arr), JsonValue::Array(expected_arr)) => {
expected_arr.iter().all(|expected_item| {
actual_arr.iter().any(|actual_item| json_contains(actual_item, expected_item))
})
},
_ => actual == expected,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_client_creation() {
let client = TestClient::new();
assert_eq!(client.base_url, "http://localhost:3000");
assert!(client.headers.is_empty());
}
#[test]
fn test_client_with_custom_url() {
let client = TestClient::with_base_url("http://example.com");
assert_eq!(client.base_url, "http://example.com");
}
#[test]
fn test_client_headers() {
let client = TestClient::new()
.header("X-Test", "value");
assert_eq!(client.headers.get("X-Test"), Some(&"value".to_string()));
}
#[test]
fn test_json_contains() {
let actual = json!({"name": "John", "age": 30, "active": true});
let expected = json!({"name": "John"});
assert!(json_contains(&actual, &expected));
let expected_false = json!({"name": "Jane"});
assert!(!json_contains(&actual, &expected_false));
}
#[test]
fn test_json_contains_nested() {
let actual = json!({
"user": {
"name": "John",
"profile": {
"email": "john@example.com"
}
}
});
let expected = json!({
"user": {
"name": "John"
}
});
assert!(json_contains(&actual, &expected));
}
#[test]
fn test_json_contains_array() {
let actual = json!({"items": ["a", "b", "c"]});
let expected = json!({"items": ["a", "c"]});
assert!(json_contains(&actual, &expected));
}
}