mockforge_core/
request_chaining.rs

1//! Request chaining for MockForge
2//!
3//! This module provides functionality to chain multiple HTTP requests together,
4//! allowing responses from previous requests to be used as input for subsequent requests.
5
6use crate::{Error, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use tokio::sync::RwLock;
10
11/// Configuration for request chaining
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ChainConfig {
15    /// Enable request chaining
16    pub enabled: bool,
17    /// Maximum chain length to prevent infinite loops
18    pub max_chain_length: usize,
19    /// Global timeout for chain execution
20    pub global_timeout_secs: u64,
21    /// Parallel execution when dependencies allow
22    pub enable_parallel_execution: bool,
23}
24
25/// Context store for maintaining state across a chain execution
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ChainContext {
28    /// Responses from previous requests in the chain
29    #[serde(default)]
30    pub responses: HashMap<String, ChainResponse>,
31    /// Global variables shared across the chain
32    #[serde(default)]
33    pub variables: HashMap<String, serde_json::Value>,
34    /// Chain execution metadata
35    #[serde(default)]
36    pub metadata: HashMap<String, String>,
37}
38
39impl ChainContext {
40    /// Create a new empty chain context
41    pub fn new() -> Self {
42        Self {
43            responses: HashMap::new(),
44            variables: HashMap::new(),
45            metadata: HashMap::new(),
46        }
47    }
48
49    /// Store a response with the given name
50    pub fn store_response(&mut self, name: String, response: ChainResponse) {
51        self.responses.insert(name, response);
52    }
53
54    /// Get a stored response by name
55    pub fn get_response(&self, name: &str) -> Option<&ChainResponse> {
56        self.responses.get(name)
57    }
58
59    /// Store a global variable
60    pub fn set_variable(&mut self, name: String, value: serde_json::Value) {
61        self.variables.insert(name, value);
62    }
63
64    /// Get a global variable
65    pub fn get_variable(&self, name: &str) -> Option<&serde_json::Value> {
66        self.variables.get(name)
67    }
68
69    /// Set metadata
70    pub fn set_metadata(&mut self, key: String, value: String) {
71        self.metadata.insert(key, value);
72    }
73
74    /// Get metadata
75    pub fn get_metadata(&self, key: &str) -> Option<&String> {
76        self.metadata.get(key)
77    }
78}
79
80impl Default for ChainContext {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86/// Pre/Post request scripting configuration
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct RequestScripting {
90    /// Script to execute before the request
91    pub pre_script: Option<String>,
92    /// Script to execute after the request completes
93    pub post_script: Option<String>,
94    /// Scripting runtime (javascript, typescript)
95    #[serde(default = "default_script_runtime")]
96    pub runtime: String,
97    /// Script timeout in milliseconds
98    #[serde(default = "default_script_timeout")]
99    pub timeout_ms: u64,
100}
101
102/// Request body types supported by MockForge
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(tag = "type", content = "data")]
105pub enum RequestBody {
106    /// JSON or text body (default)
107    #[serde(rename = "json")]
108    Json(serde_json::Value),
109    /// Binary file body - references a file on disk
110    #[serde(rename = "binary_file")]
111    BinaryFile {
112        /// Path to the binary file
113        path: String,
114        /// Optional content type
115        content_type: Option<String>,
116    },
117}
118
119impl RequestBody {
120    /// Create a JSON request body from a serde_json::Value
121    pub fn json(value: serde_json::Value) -> Self {
122        Self::Json(value)
123    }
124
125    /// Create a binary file request body
126    pub fn binary_file(path: String, content_type: Option<String>) -> Self {
127        Self::BinaryFile { path, content_type }
128    }
129
130    /// Convert the request body to bytes for HTTP transmission
131    pub async fn to_bytes(&self) -> crate::Result<Vec<u8>> {
132        match self {
133            RequestBody::Json(value) => Ok(serde_json::to_vec(value)?),
134            RequestBody::BinaryFile { path, .. } => tokio::fs::read(path).await.map_err(|e| {
135                crate::Error::generic(format!("Failed to read binary file '{}': {}", path, e))
136            }),
137        }
138    }
139
140    /// Get the content type for this request body
141    pub fn content_type(&self) -> Option<&str> {
142        match self {
143            RequestBody::Json(_) => Some("application/json"),
144            RequestBody::BinaryFile { content_type, .. } => content_type.as_deref(),
145        }
146    }
147}
148
149/// A single request in a chain
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct ChainRequest {
153    /// Unique identifier for this request in the chain
154    pub id: String,
155    /// HTTP method
156    pub method: String,
157    /// Request URL (can contain template variables)
158    pub url: String,
159    /// Request headers
160    #[serde(default)]
161    pub headers: HashMap<String, String>,
162    /// Request body (can contain template variables)
163    pub body: Option<RequestBody>,
164    /// Dependencies - IDs of other requests that must complete first
165    #[serde(default)]
166    pub depends_on: Vec<String>,
167    /// Timeout for this individual request
168    pub timeout_secs: Option<u64>,
169    /// Expected status code range (optional validation)
170    pub expected_status: Option<Vec<u16>>,
171    /// Pre/Post request scripting
172    #[serde(default)]
173    pub scripting: Option<RequestScripting>,
174}
175
176/// Response from a chain request
177#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct ChainResponse {
180    /// HTTP status code
181    pub status: u16,
182    /// Response headers
183    pub headers: HashMap<String, String>,
184    /// Response body
185    pub body: Option<serde_json::Value>,
186    /// Execution duration in milliseconds
187    pub duration_ms: u64,
188    /// Timestamp when the request was executed
189    pub executed_at: String,
190    /// Any error that occurred
191    pub error: Option<String>,
192}
193
194/// A single link in the chain
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct ChainLink {
198    /// Request definition
199    pub request: ChainRequest,
200    /// Extract variables from the response
201    #[serde(default)]
202    pub extract: HashMap<String, String>,
203    /// Store the entire response with this name
204    pub store_as: Option<String>,
205}
206
207/// Chain definition
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct ChainDefinition {
211    /// Unique identifier for the chain
212    pub id: String,
213    /// Human-readable name
214    pub name: String,
215    /// Description of what this chain does
216    pub description: Option<String>,
217    /// Chain configuration
218    pub config: ChainConfig,
219    /// Ordered list of requests to execute
220    pub links: Vec<ChainLink>,
221    /// Initial variables to set
222    #[serde(default)]
223    pub variables: HashMap<String, serde_json::Value>,
224    /// Tags for categorization
225    #[serde(default)]
226    pub tags: Vec<String>,
227}
228
229/// Script execution context for pre/post request scripts
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ScriptExecutionContext {
232    /// Chain context with stored responses
233    pub chain_context: ChainContext,
234    /// Request-scoped variables
235    pub request_variables: HashMap<String, serde_json::Value>,
236    /// Current request being executed
237    pub current_request: Option<ChainRequest>,
238    /// Current response (for post-scripts)
239    pub current_response: Option<ChainResponse>,
240}
241
242/// Context for template expansion during chain execution
243#[derive(Debug, Clone)]
244pub struct ChainTemplatingContext {
245    /// Chain context with stored responses
246    pub chain_context: ChainContext,
247    /// Request-scoped variables
248    pub request_variables: HashMap<String, serde_json::Value>,
249    /// Current request being executed
250    pub current_request: Option<ChainRequest>,
251}
252
253impl ChainTemplatingContext {
254    /// Create a new templating context
255    pub fn new(chain_context: ChainContext) -> Self {
256        Self {
257            chain_context,
258            request_variables: HashMap::new(),
259            current_request: None,
260        }
261    }
262
263    /// Set request-scoped variable
264    pub fn set_request_variable(&mut self, name: String, value: serde_json::Value) {
265        self.request_variables.insert(name, value);
266    }
267
268    /// Set current request
269    pub fn set_current_request(&mut self, request: ChainRequest) {
270        self.current_request = Some(request);
271    }
272
273    /// Extract a value using JSONPath-like syntax
274    pub fn extract_value(&self, path: &str) -> Option<serde_json::Value> {
275        // Split path like "response1.body.user.id"
276        let parts: Vec<&str> = path.split('.').collect();
277
278        if parts.is_empty() {
279            return None;
280        }
281
282        let root = parts[0];
283
284        // Get the root object
285        let root_value = if let Some(resp) = self.chain_context.get_response(root) {
286            // For response references, get body
287            resp.body.clone()?
288        } else if let Some(var) = self.chain_context.get_variable(root) {
289            var.clone()
290        } else if let Some(var) = self.request_variables.get(root) {
291            var.clone()
292        } else {
293            return None;
294        };
295
296        // Navigate the path
297        self.navigate_json_path(&root_value, &parts[1..])
298    }
299
300    /// Navigate JSON value using path segments
301    #[allow(clippy::only_used_in_recursion)]
302    fn navigate_json_path(
303        &self,
304        value: &serde_json::Value,
305        path: &[&str],
306    ) -> Option<serde_json::Value> {
307        if path.is_empty() {
308            return Some(value.clone());
309        }
310
311        match value {
312            serde_json::Value::Object(map) => {
313                if let Some(next_value) = map.get(path[0]) {
314                    self.navigate_json_path(next_value, &path[1..])
315                } else {
316                    None
317                }
318            }
319            serde_json::Value::Array(arr) => {
320                // Handle array indexing like [0]
321                if path[0].starts_with('[') && path[0].ends_with(']') {
322                    let index_str = &path[0][1..path[0].len() - 1];
323                    if let Ok(index) = index_str.parse::<usize>() {
324                        if let Some(item) = arr.get(index) {
325                            self.navigate_json_path(item, &path[1..])
326                        } else {
327                            None
328                        }
329                    } else {
330                        None
331                    }
332                } else {
333                    None
334                }
335            }
336            _ => None,
337        }
338    }
339}
340
341/// Chain store for managing multiple chain definitions
342#[derive(Debug)]
343pub struct ChainStore {
344    /// Registry of chain definitions
345    chains: RwLock<HashMap<String, ChainDefinition>>,
346    /// Configuration
347    config: ChainConfig,
348}
349
350impl ChainStore {
351    /// Create a new chain store
352    pub fn new(config: ChainConfig) -> Self {
353        Self {
354            chains: RwLock::new(HashMap::new()),
355            config,
356        }
357    }
358
359    /// Register a chain definition
360    pub async fn register_chain(&self, chain: ChainDefinition) -> Result<()> {
361        let mut chains = self.chains.write().await;
362        chains.insert(chain.id.clone(), chain);
363        Ok(())
364    }
365
366    /// Get a chain definition by ID
367    pub async fn get_chain(&self, id: &str) -> Option<ChainDefinition> {
368        let chains = self.chains.read().await;
369        chains.get(id).cloned()
370    }
371
372    /// List all registered chains
373    pub async fn list_chains(&self) -> Vec<String> {
374        let chains = self.chains.read().await;
375        chains.keys().cloned().collect()
376    }
377
378    /// Remove a chain definition
379    pub async fn remove_chain(&self, id: &str) -> Result<()> {
380        let mut chains = self.chains.write().await;
381        chains.remove(id);
382        Ok(())
383    }
384
385    /// Update chain configuration
386    pub fn update_config(&mut self, config: ChainConfig) {
387        self.config = config;
388    }
389}
390
391/// Context for chain execution
392#[derive(Debug)]
393pub struct ChainExecutionContext {
394    /// Chain definition being executed
395    pub definition: ChainDefinition,
396    /// Chain templating context
397    pub templating: ChainTemplatingContext,
398    /// Execution start time
399    pub start_time: std::time::Instant,
400    /// Chain configuration
401    pub config: ChainConfig,
402}
403
404impl ChainExecutionContext {
405    /// Create a new execution context
406    pub fn new(definition: ChainDefinition) -> Self {
407        let chain_context = ChainContext::new();
408        let templating = ChainTemplatingContext::new(chain_context);
409        let config = definition.config.clone();
410
411        Self {
412            definition,
413            templating,
414            start_time: std::time::Instant::now(),
415            config,
416        }
417    }
418
419    /// Get elapsed time
420    pub fn elapsed_ms(&self) -> u128 {
421        self.start_time.elapsed().as_millis()
422    }
423}
424
425/// Main registry for managing request chains
426#[derive(Debug)]
427pub struct RequestChainRegistry {
428    /// Chain store
429    store: ChainStore,
430}
431
432impl RequestChainRegistry {
433    /// Create a new registry
434    pub fn new(config: ChainConfig) -> Self {
435        Self {
436            store: ChainStore::new(config),
437        }
438    }
439
440    /// Register a chain from YAML string
441    pub async fn register_from_yaml(&self, yaml: &str) -> Result<String> {
442        let chain: ChainDefinition = serde_yaml::from_str(yaml)
443            .map_err(|e| Error::generic(format!("Failed to parse chain YAML: {}", e)))?;
444        self.store.register_chain(chain.clone()).await?;
445        Ok(chain.id.clone())
446    }
447
448    /// Register a chain from JSON string
449    pub async fn register_from_json(&self, json: &str) -> Result<String> {
450        let chain: ChainDefinition = serde_json::from_str(json)
451            .map_err(|e| Error::generic(format!("Failed to parse chain JSON: {}", e)))?;
452        self.store.register_chain(chain.clone()).await?;
453        Ok(chain.id.clone())
454    }
455
456    /// Get a chain by ID
457    pub async fn get_chain(&self, id: &str) -> Option<ChainDefinition> {
458        self.store.get_chain(id).await
459    }
460
461    /// List all chains
462    pub async fn list_chains(&self) -> Vec<String> {
463        self.store.list_chains().await
464    }
465
466    /// Remove a chain
467    pub async fn remove_chain(&self, id: &str) -> Result<()> {
468        self.store.remove_chain(id).await
469    }
470
471    /// Validate chain dependencies and structure
472    pub async fn validate_chain(&self, chain: &ChainDefinition) -> Result<()> {
473        if chain.links.is_empty() {
474            return Err(Error::generic("Chain must have at least one link"));
475        }
476
477        if chain.links.len() > self.store.config.max_chain_length {
478            return Err(Error::generic(format!(
479                "Chain length {} exceeds maximum allowed length {}",
480                chain.links.len(),
481                self.store.config.max_chain_length
482            )));
483        }
484
485        // Check for circular dependencies and invalid references
486        let mut visited = std::collections::HashSet::new();
487        let mut rec_stack = std::collections::HashSet::new();
488
489        for link in &chain.links {
490            self.validate_link_dependencies(link, &mut visited, &mut rec_stack, chain)?;
491        }
492
493        // Check for duplicate request IDs
494        let request_ids: std::collections::HashSet<_> =
495            chain.links.iter().map(|link| &link.request.id).collect();
496
497        if request_ids.len() != chain.links.len() {
498            return Err(Error::generic("Duplicate request IDs found in chain"));
499        }
500
501        Ok(())
502    }
503
504    /// Validate link dependencies for circular references
505    #[allow(clippy::only_used_in_recursion)]
506    fn validate_link_dependencies(
507        &self,
508        link: &ChainLink,
509        visited: &mut std::collections::HashSet<String>,
510        rec_stack: &mut std::collections::HashSet<String>,
511        chain: &ChainDefinition,
512    ) -> Result<()> {
513        if rec_stack.contains(&link.request.id) {
514            return Err(Error::generic(format!(
515                "Circular dependency detected involving request '{}'",
516                link.request.id
517            )));
518        }
519
520        if visited.contains(&link.request.id) {
521            return Ok(());
522        }
523
524        visited.insert(link.request.id.clone());
525        rec_stack.insert(link.request.id.clone());
526
527        for dep in &link.request.depends_on {
528            // Check if dependency exists in the chain
529            if !chain.links.iter().any(|l| &l.request.id == dep) {
530                return Err(Error::generic(format!(
531                    "Request '{}' depends on '{}' which does not exist in the chain",
532                    link.request.id, dep
533                )));
534            }
535
536            // Recursively check the dependency
537            if let Some(dep_link) = chain.links.iter().find(|l| &l.request.id == dep) {
538                self.validate_link_dependencies(dep_link, visited, rec_stack, chain)?;
539            }
540        }
541
542        rec_stack.remove(&link.request.id);
543        Ok(())
544    }
545
546    /// Get the chain store (for internal use)
547    pub fn store(&self) -> &ChainStore {
548        &self.store
549    }
550}
551
552fn default_script_runtime() -> String {
553    "javascript".to_string()
554}
555
556fn default_script_timeout() -> u64 {
557    5000 // 5 seconds
558}
559
560impl Default for ChainConfig {
561    fn default() -> Self {
562        Self {
563            enabled: false,
564            max_chain_length: 20,
565            global_timeout_secs: 300,
566            enable_parallel_execution: false,
567        }
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use serde_json::json;
575
576    #[test]
577    fn test_chain_context() {
578        let mut ctx = ChainContext::new();
579
580        // Test variable storage
581        ctx.set_variable("user_id".to_string(), json!("12345"));
582        assert_eq!(ctx.get_variable("user_id"), Some(&json!("12345")));
583
584        // Test response storage
585        let response = ChainResponse {
586            status: 200,
587            headers: HashMap::new(),
588            body: Some(json!({"user": {"id": 123, "name": "John"}})),
589            duration_ms: 150,
590            executed_at: "2023-01-01T00:00:00Z".to_string(),
591            error: None,
592        };
593        ctx.store_response("login".to_string(), response.clone());
594        assert_eq!(ctx.get_response("login"), Some(&response));
595    }
596
597    #[test]
598    fn test_chain_context_comprehensive() {
599        let mut ctx = ChainContext::new();
600
601        // Test multiple variables
602        ctx.set_variable("user_id".to_string(), json!("12345"));
603        ctx.set_variable("token".to_string(), json!("abc-def-ghi"));
604        ctx.set_variable("environment".to_string(), json!("production"));
605        ctx.set_variable("timeout".to_string(), json!(30));
606
607        assert_eq!(ctx.get_variable("user_id"), Some(&json!("12345")));
608        assert_eq!(ctx.get_variable("token"), Some(&json!("abc-def-ghi")));
609        assert_eq!(ctx.get_variable("environment"), Some(&json!("production")));
610        assert_eq!(ctx.get_variable("timeout"), Some(&json!(30)));
611
612        // Test non-existent variable
613        assert_eq!(ctx.get_variable("nonexistent"), None);
614
615        // Test variable overwriting
616        ctx.set_variable("user_id".to_string(), json!("67890"));
617        assert_eq!(ctx.get_variable("user_id"), Some(&json!("67890")));
618
619        // Test metadata
620        ctx.set_metadata("chain_id".to_string(), "test-chain-123".to_string());
621        ctx.set_metadata("version".to_string(), "1.0.0".to_string());
622        assert_eq!(ctx.get_metadata("chain_id"), Some(&"test-chain-123".to_string()));
623        assert_eq!(ctx.get_metadata("version"), Some(&"1.0.0".to_string()));
624
625        // Test multiple responses
626        let response1 = ChainResponse {
627            status: 200,
628            headers: vec![("Content-Type".to_string(), "application/json".to_string())]
629                .into_iter()
630                .collect(),
631            body: Some(json!({"message": "success1"})),
632            duration_ms: 100,
633            executed_at: "2023-01-01T00:00:00Z".to_string(),
634            error: None,
635        };
636
637        let response2 = ChainResponse {
638            status: 201,
639            headers: vec![("Location".to_string(), "/users/123".to_string())].into_iter().collect(),
640            body: Some(json!({"id": 123, "name": "John"})),
641            duration_ms: 150,
642            executed_at: "2023-01-01T00:00:01Z".to_string(),
643            error: None,
644        };
645
646        ctx.store_response("step1".to_string(), response1.clone());
647        ctx.store_response("step2".to_string(), response2.clone());
648
649        assert_eq!(ctx.get_response("step1"), Some(&response1));
650        assert_eq!(ctx.get_response("step2"), Some(&response2));
651        assert_eq!(ctx.get_response("nonexistent"), None);
652
653        // Test response overwriting
654        let updated_response = ChainResponse {
655            status: 202,
656            headers: HashMap::new(),
657            body: Some(json!({"message": "updated"})),
658            duration_ms: 200,
659            executed_at: "2023-01-01T00:00:02Z".to_string(),
660            error: None,
661        };
662        ctx.store_response("step1".to_string(), updated_response.clone());
663        assert_eq!(ctx.get_response("step1"), Some(&updated_response));
664    }
665
666    #[test]
667    fn test_chain_context_serialization() {
668        let mut ctx = ChainContext::new();
669
670        // Add some data
671        ctx.set_variable("test_var".to_string(), json!("test_value"));
672        ctx.set_metadata("test_meta".to_string(), "test_value".to_string());
673
674        let response = ChainResponse {
675            status: 200,
676            headers: HashMap::new(),
677            body: Some(json!({"data": "test"})),
678            duration_ms: 100,
679            executed_at: "2023-01-01T00:00:00Z".to_string(),
680            error: None,
681        };
682        ctx.store_response("test_response".to_string(), response);
683
684        // Test serialization
685        let json_str = serde_json::to_string(&ctx).unwrap();
686        assert!(json_str.contains("test_var"));
687        assert!(json_str.contains("test_value"));
688        assert!(json_str.contains("test_meta"));
689        assert!(json_str.contains("test_response"));
690
691        // Test deserialization
692        let deserialized: ChainContext = serde_json::from_str(&json_str).unwrap();
693        assert_eq!(deserialized.get_variable("test_var"), Some(&json!("test_value")));
694        assert_eq!(deserialized.get_metadata("test_meta"), Some(&"test_value".to_string()));
695        assert!(deserialized.get_response("test_response").is_some());
696    }
697
698    #[test]
699    fn test_chain_request_serialization() {
700        let request = ChainRequest {
701            id: "test-req".to_string(),
702            method: "POST".to_string(),
703            url: "https://api.example.com/test".to_string(),
704            headers: vec![("Content-Type".to_string(), "application/json".to_string())]
705                .into_iter()
706                .collect(),
707            body: Some(RequestBody::Json(json!({"key": "value"}))),
708            depends_on: vec!["req1".to_string(), "req2".to_string()],
709            timeout_secs: Some(30),
710            expected_status: Some(vec![200, 201, 202]),
711            scripting: Some(RequestScripting {
712                pre_script: Some("console.log('pre');".to_string()),
713                post_script: Some("console.log('post');".to_string()),
714                runtime: "javascript".to_string(),
715                timeout_ms: 5000,
716            }),
717        };
718
719        let json_str = serde_json::to_string(&request).unwrap();
720        assert!(json_str.contains("test-req"));
721        assert!(json_str.contains("POST"));
722        assert!(json_str.contains("Content-Type"));
723        assert!(json_str.contains("req1"));
724        assert!(json_str.contains("preScript"));
725
726        let deserialized: ChainRequest = serde_json::from_str(&json_str).unwrap();
727        assert_eq!(deserialized.id, request.id);
728        assert_eq!(deserialized.method, request.method);
729        assert_eq!(deserialized.depends_on, request.depends_on);
730    }
731
732    #[test]
733    fn test_chain_response_serialization() {
734        let response = ChainResponse {
735            status: 200,
736            headers: vec![
737                ("Content-Type".to_string(), "application/json".to_string()),
738                ("X-Request-ID".to_string(), "req-123".to_string()),
739            ]
740            .into_iter()
741            .collect(),
742            body: Some(json!({"result": "success", "data": [1, 2, 3]})),
743            duration_ms: 150,
744            executed_at: "2023-01-01T00:00:00Z".to_string(),
745            error: None,
746        };
747
748        let json_str = serde_json::to_string(&response).unwrap();
749        assert!(json_str.contains("200"));
750        assert!(json_str.contains("application/json"));
751        assert!(json_str.contains("success"));
752
753        let deserialized: ChainResponse = serde_json::from_str(&json_str).unwrap();
754        assert_eq!(deserialized.status, response.status);
755        assert_eq!(deserialized.duration_ms, response.duration_ms);
756        assert_eq!(deserialized.body, response.body);
757    }
758
759    #[test]
760    fn test_chain_response_with_error() {
761        let error_response = ChainResponse {
762            status: 500,
763            headers: HashMap::new(),
764            body: None,
765            duration_ms: 50,
766            executed_at: "2023-01-01T00:00:00Z".to_string(),
767            error: Some("Internal server error".to_string()),
768        };
769
770        let json_str = serde_json::to_string(&error_response).unwrap();
771        assert!(json_str.contains("500"));
772        assert!(json_str.contains("Internal server error"));
773
774        let deserialized: ChainResponse = serde_json::from_str(&json_str).unwrap();
775        assert_eq!(deserialized.error, Some("Internal server error".to_string()));
776        assert!(deserialized.body.is_none());
777    }
778
779    #[test]
780    fn test_request_body_types() {
781        // Test JSON body
782        let json_body = RequestBody::Json(json!({"key": "value", "number": 42}));
783        assert!(matches!(json_body, RequestBody::Json(_)));
784
785        // Test string body
786        let string_body =
787            RequestBody::Json(serde_json::Value::String("raw text content".to_string()));
788        assert!(matches!(string_body, RequestBody::Json(_)));
789
790        // Test binary file body
791        let binary_body = RequestBody::BinaryFile {
792            path: "/path/to/file.bin".to_string(),
793            content_type: Some("application/octet-stream".to_string()),
794        };
795        assert!(matches!(binary_body, RequestBody::BinaryFile { .. }));
796
797        // Test serialization of different body types
798        let test_cases = vec![
799            RequestBody::Json(json!({"test": "json"})),
800            RequestBody::Json(serde_json::Value::String("test string".to_string())),
801            RequestBody::BinaryFile {
802                path: "/path/to/bytes.bin".to_string(),
803                content_type: None,
804            },
805        ];
806
807        for body in test_cases {
808            let json_str = serde_json::to_string(&body).unwrap();
809            let deserialized: RequestBody = serde_json::from_str(&json_str).unwrap();
810            assert_eq!(format!("{:?}", body), format!("{:?}", deserialized));
811        }
812    }
813
814    #[test]
815    fn test_chain_link_dependencies() {
816        let link1 = ChainLink {
817            request: ChainRequest {
818                id: "req1".to_string(),
819                method: "GET".to_string(),
820                url: "https://api.example.com/users".to_string(),
821                headers: HashMap::new(),
822                body: None,
823                depends_on: vec![], // No dependencies
824                timeout_secs: None,
825                expected_status: None,
826                scripting: None,
827            },
828            extract: HashMap::new(),
829            store_as: Some("users".to_string()),
830        };
831
832        let link2 = ChainLink {
833            request: ChainRequest {
834                id: "req2".to_string(),
835                method: "POST".to_string(),
836                url: "https://api.example.com/posts".to_string(),
837                headers: HashMap::new(),
838                body: Some(RequestBody::Json(json!({"title": "Test"}))),
839                depends_on: vec!["req1".to_string()], // Depends on req1
840                timeout_secs: Some(30),
841                expected_status: Some(vec![200, 201]),
842                scripting: None,
843            },
844            extract: HashMap::new(),
845            store_as: Some("post".to_string()),
846        };
847
848        let link3 = ChainLink {
849            request: ChainRequest {
850                id: "req3".to_string(),
851                method: "PUT".to_string(),
852                url: "https://api.example.com/posts/{{chain.post.id}}".to_string(),
853                headers: HashMap::new(),
854                body: None,
855                depends_on: vec!["req1".to_string(), "req2".to_string()], // Multiple dependencies
856                timeout_secs: None,
857                expected_status: None,
858                scripting: None,
859            },
860            extract: HashMap::new(),
861            store_as: None,
862        };
863
864        assert!(link1.request.depends_on.is_empty());
865        assert_eq!(link2.request.depends_on, vec!["req1".to_string()]);
866        assert_eq!(link3.request.depends_on, vec!["req1".to_string(), "req2".to_string()]);
867    }
868
869    #[test]
870    fn test_chain_config_validation() {
871        // Test valid config
872        let valid_config = ChainConfig {
873            enabled: true,
874            max_chain_length: 10,
875            global_timeout_secs: 300,
876            enable_parallel_execution: true,
877        };
878
879        // Test invalid config
880        let invalid_config = ChainConfig {
881            enabled: true,
882            max_chain_length: 0, // Invalid: must be > 0
883            global_timeout_secs: 300,
884            enable_parallel_execution: true,
885        };
886
887        assert!(valid_config.max_chain_length > 0);
888        assert!(invalid_config.max_chain_length == 0);
889
890        // Test edge cases
891        let edge_config = ChainConfig {
892            enabled: false,
893            max_chain_length: 1,
894            global_timeout_secs: 0,
895            enable_parallel_execution: false,
896        };
897        assert_eq!(edge_config.max_chain_length, 1);
898        assert_eq!(edge_config.global_timeout_secs, 0);
899        assert!(!edge_config.enabled);
900    }
901
902    #[test]
903    fn test_request_scripting_config() {
904        let scripting = RequestScripting {
905            pre_script: Some("console.log('Starting request');".to_string()),
906            post_script: Some("console.log('Request completed');".to_string()),
907            runtime: "javascript".to_string(),
908            timeout_ms: 5000,
909        };
910
911        assert_eq!(scripting.runtime, "javascript");
912        assert_eq!(scripting.timeout_ms, 5000);
913        assert!(scripting.pre_script.is_some());
914        assert!(scripting.post_script.is_some());
915
916        // Test serialization
917        let json_str = serde_json::to_string(&scripting).unwrap();
918        assert!(json_str.contains("javascript"));
919        assert!(json_str.contains("Starting request"));
920        assert!(json_str.contains("Request completed"));
921
922        let deserialized: RequestScripting = serde_json::from_str(&json_str).unwrap();
923        assert_eq!(deserialized.runtime, scripting.runtime);
924        assert_eq!(deserialized.timeout_ms, scripting.timeout_ms);
925    }
926
927    #[test]
928    fn test_chain_definition_structure() {
929        let definition = ChainDefinition {
930            id: "test-chain".to_string(),
931            name: "Test Chain".to_string(),
932            description: Some("A comprehensive test chain".to_string()),
933            config: ChainConfig::default(),
934            links: vec![ChainLink {
935                request: ChainRequest {
936                    id: "req1".to_string(),
937                    method: "GET".to_string(),
938                    url: "https://api.example.com/users".to_string(),
939                    headers: HashMap::new(),
940                    body: None,
941                    depends_on: vec![],
942                    timeout_secs: None,
943                    expected_status: None,
944                    scripting: None,
945                },
946                extract: vec![("user_id".to_string(), "$.users[0].id".to_string())]
947                    .into_iter()
948                    .collect(),
949                store_as: Some("users".to_string()),
950            }],
951            variables: vec![
952                ("api_key".to_string(), json!("test-key")),
953                ("base_url".to_string(), json!("https://api.example.com")),
954            ]
955            .into_iter()
956            .collect(),
957            tags: vec!["test".to_string(), "integration".to_string()],
958        };
959
960        assert_eq!(definition.id, "test-chain");
961        assert_eq!(definition.name, "Test Chain");
962        assert!(definition.description.is_some());
963        assert_eq!(definition.links.len(), 1);
964        assert_eq!(definition.variables.len(), 2);
965        assert_eq!(definition.tags.len(), 2);
966
967        // Test serialization
968        let json_str = serde_json::to_string(&definition).unwrap();
969        assert!(json_str.contains("test-chain"));
970        assert!(json_str.contains("Test Chain"));
971        assert!(json_str.contains("comprehensive test chain"));
972        assert!(json_str.contains("api_key"));
973        assert!(json_str.contains("test-key"));
974    }
975
976    #[test]
977    fn test_chain_execution_context() {
978        let chain_def = ChainDefinition {
979            id: "test_chain".to_string(),
980            name: "Test Chain".to_string(),
981            description: Some("Test chain for unit tests".to_string()),
982            config: ChainConfig::default(),
983            links: vec![],
984            tags: vec![],
985            variables: HashMap::new(),
986        };
987        let exec_ctx = ChainExecutionContext::new(chain_def);
988
989        // Add a small delay to ensure some time has elapsed
990        std::thread::sleep(std::time::Duration::from_millis(1));
991
992        // Test elapsed time
993        assert!(exec_ctx.elapsed_ms() > 0);
994    }
995
996    #[tokio::test]
997    async fn test_chain_definition_validation() {
998        let registry = RequestChainRegistry::new(ChainConfig::default());
999
1000        let valid_chain = ChainDefinition {
1001            id: "test-chain".to_string(),
1002            name: "Test Chain".to_string(),
1003            description: Some("A test chain for validation".to_string()),
1004            config: ChainConfig::default(),
1005            links: vec![
1006                ChainLink {
1007                    request: ChainRequest {
1008                        id: "req1".to_string(),
1009                        method: "GET".to_string(),
1010                        url: "https://api.example.com/users".to_string(),
1011                        headers: HashMap::new(),
1012                        body: None,
1013                        depends_on: vec![],
1014                        timeout_secs: None,
1015                        expected_status: None,
1016                        scripting: None,
1017                    },
1018                    extract: HashMap::new(),
1019                    store_as: Some("users".to_string()),
1020                },
1021                ChainLink {
1022                    request: ChainRequest {
1023                        id: "req2".to_string(),
1024                        method: "POST".to_string(),
1025                        url: "https://api.example.com/users/{{chain.users.body[0].id}}/posts"
1026                            .to_string(),
1027                        headers: HashMap::new(),
1028                        body: Some(RequestBody::Json(json!({"title": "Hello World"}))),
1029                        depends_on: vec!["req1".to_string()],
1030                        timeout_secs: None,
1031                        expected_status: None,
1032                        scripting: None,
1033                    },
1034                    extract: HashMap::new(),
1035                    store_as: Some("post".to_string()),
1036                },
1037            ],
1038            variables: {
1039                let mut vars = HashMap::new();
1040                vars.insert("api_key".to_string(), json!("test-key-123"));
1041                vars
1042            },
1043            tags: vec!["test".to_string()],
1044        };
1045
1046        // Should validate successfully
1047        assert!(registry.validate_chain(&valid_chain).await.is_ok());
1048
1049        // Test invalid chain (empty)
1050        let invalid_chain = ChainDefinition {
1051            id: "empty-chain".to_string(),
1052            name: "Empty Chain".to_string(),
1053            description: None,
1054            config: ChainConfig::default(),
1055            links: vec![],
1056            variables: HashMap::new(),
1057            tags: vec![],
1058        };
1059        assert!(registry.validate_chain(&invalid_chain).await.is_err());
1060
1061        // Test chain with self-dependency
1062        let self_dep_chain = ChainDefinition {
1063            id: "self-dep-chain".to_string(),
1064            name: "Self Dependency Chain".to_string(),
1065            description: None,
1066            config: ChainConfig::default(),
1067            links: vec![ChainLink {
1068                request: ChainRequest {
1069                    id: "req1".to_string(),
1070                    method: "GET".to_string(),
1071                    url: "https://api.example.com/users".to_string(),
1072                    headers: HashMap::new(),
1073                    body: None,
1074                    depends_on: vec!["req1".to_string()], // Self dependency
1075                    timeout_secs: None,
1076                    expected_status: None,
1077                    scripting: None,
1078                },
1079                extract: HashMap::new(),
1080                store_as: None,
1081            }],
1082            variables: HashMap::new(),
1083            tags: vec![],
1084        };
1085        assert!(registry.validate_chain(&self_dep_chain).await.is_err());
1086    }
1087}