elif_testing/
client.rs

1//! HTTP testing client and response utilities
2//!
3//! Provides a fluent API for making HTTP requests in tests and
4//! comprehensive assertions for validating responses.
5
6use std::collections::HashMap;
7use serde_json::{Value as JsonValue, json};
8use crate::{TestError, TestResult};
9
10/// HTTP test client for making requests in tests
11#[derive(Clone)]
12pub struct TestClient {
13    base_url: String,
14    headers: HashMap<String, String>,
15    auth_token: Option<String>,
16}
17
18impl TestClient {
19    /// Create a new test client
20    pub fn new() -> Self {
21        Self {
22            base_url: "http://localhost:3000".to_string(),
23            headers: HashMap::new(),
24            auth_token: None,
25        }
26    }
27    
28    /// Create a test client with a custom base URL
29    pub fn with_base_url(base_url: impl Into<String>) -> Self {
30        Self {
31            base_url: base_url.into(),
32            headers: HashMap::new(),
33            auth_token: None,
34        }
35    }
36    
37    /// Set a header for all requests
38    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
39        self.headers.insert(name.into(), value.into());
40        self
41    }
42    
43    /// Set multiple headers
44    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
45        self.headers.extend(headers);
46        self
47    }
48    
49    /// Set authentication token (JWT)
50    pub fn authenticated_with_token(mut self, token: impl Into<String>) -> Self {
51        let token = token.into();
52        self.auth_token = Some(token.clone());
53        self.headers.insert("Authorization".to_string(), format!("Bearer {}", token));
54        self
55    }
56    
57    /// Authenticate as a specific user (requires user to implement auth traits)
58    pub fn authenticated_as<T>(self, _user: &T) -> Self 
59    where 
60        T: AuthenticatedUser,
61    {
62        // This would generate a JWT token for the user in tests
63        let token = "test_jwt_token"; // Placeholder - would generate real token
64        self.authenticated_with_token(token)
65    }
66    
67    /// Make a GET request
68    pub fn get(self, path: impl Into<String>) -> RequestBuilder {
69        RequestBuilder::new(self, "GET".to_string(), path.into())
70    }
71    
72    /// Make a POST request
73    pub fn post(self, path: impl Into<String>) -> RequestBuilder {
74        RequestBuilder::new(self, "POST".to_string(), path.into())
75    }
76    
77    /// Make a PUT request
78    pub fn put(self, path: impl Into<String>) -> RequestBuilder {
79        RequestBuilder::new(self, "PUT".to_string(), path.into())
80    }
81    
82    /// Make a PATCH request
83    pub fn patch(self, path: impl Into<String>) -> RequestBuilder {
84        RequestBuilder::new(self, "PATCH".to_string(), path.into())
85    }
86    
87    /// Make a DELETE request
88    pub fn delete(self, path: impl Into<String>) -> RequestBuilder {
89        RequestBuilder::new(self, "DELETE".to_string(), path.into())
90    }
91}
92
93impl Default for TestClient {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99/// Trait for users that can be authenticated in tests
100pub trait AuthenticatedUser {
101    /// Get the user ID for authentication
102    fn id(&self) -> String;
103    
104    /// Get user roles for RBAC testing
105    fn roles(&self) -> Vec<String> {
106        vec![]
107    }
108    
109    /// Get user permissions for testing
110    fn permissions(&self) -> Vec<String> {
111        vec![]
112    }
113}
114
115/// Request builder for fluent API
116pub struct RequestBuilder {
117    client: TestClient,
118    method: String,
119    path: String,
120    headers: HashMap<String, String>,
121    body: Option<String>,
122    query_params: HashMap<String, String>,
123}
124
125impl RequestBuilder {
126    fn new(client: TestClient, method: String, path: String) -> Self {
127        Self {
128            client,
129            method,
130            path,
131            headers: HashMap::new(),
132            body: None,
133            query_params: HashMap::new(),
134        }
135    }
136    
137    /// Set a request header
138    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
139        self.headers.insert(name.into(), value.into());
140        self
141    }
142    
143    /// Set JSON body for the request
144    pub fn json<T: serde::Serialize>(mut self, data: &T) -> Self {
145        match serde_json::to_string(data) {
146            Ok(json_str) => {
147                self.body = Some(json_str);
148                self.headers.insert("Content-Type".to_string(), "application/json".to_string());
149            },
150            Err(_) => {
151                // This would be handled in send()
152            }
153        }
154        self
155    }
156    
157    /// Set form data body
158    pub fn form(mut self, data: HashMap<String, String>) -> Self {
159        let form_data = data.iter()
160            .map(|(k, v)| format!("{}={}", k, v))
161            .collect::<Vec<_>>()
162            .join("&");
163        self.body = Some(form_data);
164        self.headers.insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string());
165        self
166    }
167    
168    /// Set plain text body
169    pub fn body(mut self, body: impl Into<String>) -> Self {
170        self.body = Some(body.into());
171        self
172    }
173    
174    /// Add query parameter
175    pub fn query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
176        self.query_params.insert(key.into(), value.into());
177        self
178    }
179    
180    /// Add multiple query parameters
181    pub fn queries(mut self, params: HashMap<String, String>) -> Self {
182        self.query_params.extend(params);
183        self
184    }
185    
186    /// Send the request and return a test response
187    pub async fn send(self) -> TestResult<TestResponse> {
188        // Build the full URL
189        let mut url = format!("{}{}", self.client.base_url, self.path);
190        if !self.query_params.is_empty() {
191            let query_string = self.query_params.iter()
192                .map(|(k, v)| format!("{}={}", k, v))
193                .collect::<Vec<_>>()
194                .join("&");
195            url.push_str(&format!("?{}", query_string));
196        }
197        
198        // In a real implementation, this would make an HTTP request
199        // For now, we'll create a mock response
200        let response = TestResponse {
201            status_code: 200,
202            headers: {
203                let mut headers = HashMap::new();
204                headers.insert("Content-Type".to_string(), "application/json".to_string());
205                headers
206            },
207            body: json!({"message": "Test response", "method": self.method, "path": self.path}).to_string(),
208        };
209        
210        Ok(response)
211    }
212}
213
214/// Test response wrapper with assertion methods
215pub struct TestResponse {
216    status_code: u16,
217    headers: HashMap<String, String>,
218    body: String,
219}
220
221impl TestResponse {
222    /// Get the response status code
223    pub fn status(&self) -> u16 {
224        self.status_code
225    }
226    
227    /// Get response headers
228    pub fn headers(&self) -> &HashMap<String, String> {
229        &self.headers
230    }
231    
232    /// Get response body as string
233    pub fn body(&self) -> &str {
234        &self.body
235    }
236    
237    /// Get response body as JSON
238    pub fn json(&self) -> TestResult<JsonValue> {
239        let json_value: JsonValue = serde_json::from_str(&self.body)?;
240        Ok(json_value)
241    }
242    
243    /// Assert the response status code
244    pub fn assert_status(self, expected_status: u16) -> Self {
245        if self.status_code != expected_status {
246            panic!("Expected status {}, got {}", expected_status, self.status_code);
247        }
248        self
249    }
250    
251    /// Assert the response status is successful (2xx)
252    pub fn assert_success(self) -> Self {
253        if self.status_code < 200 || self.status_code >= 300 {
254            panic!("Expected successful status, got {}", self.status_code);
255        }
256        self
257    }
258    
259    /// Assert response header value
260    pub fn assert_header(self, name: &str, expected_value: &str) -> Self {
261        if let Some(value) = self.headers.get(name) {
262            if value != expected_value {
263                panic!("Expected header '{}' to be '{}', got '{}'", name, expected_value, value);
264            }
265        } else {
266            panic!("Expected header '{}' not found", name);
267        }
268        self
269    }
270    
271    /// Assert response header exists
272    pub fn assert_header_exists(self, name: &str) -> Self {
273        if !self.headers.contains_key(name) {
274            panic!("Expected header '{}' to exist", name);
275        }
276        self
277    }
278    
279    /// Assert JSON response contains specific fields/values
280    pub fn assert_json_contains(self, expected: JsonValue) -> TestResult<Self> {
281        let actual_json = self.json()?;
282        
283        if !json_contains(&actual_json, &expected) {
284            return Err(TestError::Assertion {
285                message: format!("Expected JSON to contain: {}, got: {}", expected, actual_json),
286            });
287        }
288        
289        Ok(self)
290    }
291    
292    /// Assert JSON response equals expected value
293    pub fn assert_json_equals(self, expected: JsonValue) -> TestResult<Self> {
294        let actual_json = self.json()?;
295        
296        if actual_json != expected {
297            return Err(TestError::Assertion {
298                message: format!("Expected JSON: {}, got: {}", expected, actual_json),
299            });
300        }
301        
302        Ok(self)
303    }
304    
305    /// Assert response body contains text
306    pub fn assert_body_contains(self, expected_text: &str) -> TestResult<Self> {
307        let body = self.body();
308        
309        if !body.contains(expected_text) {
310            return Err(TestError::Assertion {
311                message: format!("Expected body to contain '{}', got: {}", expected_text, body),
312            });
313        }
314        
315        Ok(self)
316    }
317    
318    /// Assert validation error for specific field
319    pub fn assert_validation_error(self, field: &str, _error_type: &str) -> TestResult<Self> {
320        let json = self.json()?;
321        
322        // Check if it's a validation error response
323        if let Some(errors) = json.get("errors") {
324            if let Some(field_errors) = errors.get(field) {
325                if field_errors.as_array().map_or(false, |arr| !arr.is_empty()) {
326                    return Ok(self);
327                }
328            }
329        }
330        
331        Err(TestError::Assertion {
332            message: format!("Expected validation error for field '{}', got: {}", field, json),
333        })
334    }
335}
336
337/// Helper function to check if JSON contains expected values
338fn json_contains(actual: &JsonValue, expected: &JsonValue) -> bool {
339    match (actual, expected) {
340        (JsonValue::Object(actual_map), JsonValue::Object(expected_map)) => {
341            for (key, expected_value) in expected_map {
342                if let Some(actual_value) = actual_map.get(key) {
343                    if !json_contains(actual_value, expected_value) {
344                        return false;
345                    }
346                } else {
347                    return false;
348                }
349            }
350            true
351        },
352        (JsonValue::Array(actual_arr), JsonValue::Array(expected_arr)) => {
353            // For arrays, check if all expected items exist in actual array
354            expected_arr.iter().all(|expected_item| {
355                actual_arr.iter().any(|actual_item| json_contains(actual_item, expected_item))
356            })
357        },
358        _ => actual == expected,
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use serde_json::json;
366    
367    #[test]
368    fn test_client_creation() {
369        let client = TestClient::new();
370        assert_eq!(client.base_url, "http://localhost:3000");
371        assert!(client.headers.is_empty());
372    }
373    
374    #[test]
375    fn test_client_with_custom_url() {
376        let client = TestClient::with_base_url("http://example.com");
377        assert_eq!(client.base_url, "http://example.com");
378    }
379    
380    #[test]
381    fn test_client_headers() {
382        let client = TestClient::new()
383            .header("X-Test", "value");
384        assert_eq!(client.headers.get("X-Test"), Some(&"value".to_string()));
385    }
386    
387    #[test]
388    fn test_json_contains() {
389        let actual = json!({"name": "John", "age": 30, "active": true});
390        let expected = json!({"name": "John"});
391        
392        assert!(json_contains(&actual, &expected));
393        
394        let expected_false = json!({"name": "Jane"});
395        assert!(!json_contains(&actual, &expected_false));
396    }
397    
398    #[test]
399    fn test_json_contains_nested() {
400        let actual = json!({
401            "user": {
402                "name": "John",
403                "profile": {
404                    "email": "john@example.com"
405                }
406            }
407        });
408        let expected = json!({
409            "user": {
410                "name": "John"
411            }
412        });
413        
414        assert!(json_contains(&actual, &expected));
415    }
416    
417    #[test]
418    fn test_json_contains_array() {
419        let actual = json!({"items": ["a", "b", "c"]});
420        let expected = json!({"items": ["a", "c"]});
421        
422        assert!(json_contains(&actual, &expected));
423    }
424}