Skip to main content

mockforge_core/
stateful_handler.rs

1//! Stateful response handler for HTTP requests
2//!
3//! Integrates state machines with HTTP request handling to provide dynamic responses
4//! based on request history and state transitions.
5
6use crate::{Error, Result};
7use axum::http::{HeaderMap, Method, Uri};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::debug;
14
15/// Simple state instance for tracking resource state
16#[derive(Debug, Clone)]
17struct StateInstance {
18    /// Resource identifier
19    resource_id: String,
20    /// Current state
21    current_state: String,
22    /// Resource type
23    resource_type: String,
24    /// State data (key-value pairs)
25    state_data: HashMap<String, Value>,
26}
27
28impl StateInstance {
29    fn new(resource_id: String, resource_type: String, initial_state: String) -> Self {
30        Self {
31            resource_id,
32            current_state: initial_state,
33            resource_type,
34            state_data: HashMap::new(),
35        }
36    }
37
38    fn transition_to(&mut self, new_state: String) {
39        self.current_state = new_state;
40    }
41}
42
43/// Simple state machine manager for stateful responses
44struct StateMachineManager {
45    /// State instances by resource ID
46    instances: Arc<RwLock<HashMap<String, StateInstance>>>,
47}
48
49impl StateMachineManager {
50    fn new() -> Self {
51        Self {
52            instances: Arc::new(RwLock::new(HashMap::new())),
53        }
54    }
55
56    async fn get_or_create_instance(
57        &self,
58        resource_id: String,
59        resource_type: String,
60        initial_state: String,
61    ) -> Result<StateInstance> {
62        let mut instances = self.instances.write().await;
63        if let Some(instance) = instances.get(&resource_id) {
64            Ok(instance.clone())
65        } else {
66            let instance = StateInstance::new(resource_id.clone(), resource_type, initial_state);
67            instances.insert(resource_id, instance.clone());
68            Ok(instance)
69        }
70    }
71
72    async fn update_instance(&self, resource_id: String, instance: StateInstance) -> Result<()> {
73        let mut instances = self.instances.write().await;
74        instances.insert(resource_id, instance);
75        Ok(())
76    }
77}
78
79/// Configuration for stateful response handling
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct StatefulConfig {
82    /// Resource ID extraction configuration
83    pub resource_id_extract: ResourceIdExtract,
84    /// Resource type for this endpoint
85    pub resource_type: String,
86    /// State-based response configurations
87    pub state_responses: HashMap<String, StateResponse>,
88    /// Transition triggers (method + path combinations)
89    pub transitions: Vec<TransitionTrigger>,
90}
91
92/// Resource ID extraction configuration
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(tag = "type", rename_all = "snake_case")]
95pub enum ResourceIdExtract {
96    /// Extract from path parameter (e.g., "/orders/{order_id}" -> extract "order_id")
97    PathParam {
98        /// Path parameter name to extract
99        param: String,
100    },
101    /// Extract from JSONPath in request body
102    JsonPath {
103        /// JSONPath expression to extract the resource ID
104        path: String,
105    },
106    /// Extract from header value
107    Header {
108        /// Header name to extract the resource ID from
109        name: String,
110    },
111    /// Extract from query parameter
112    QueryParam {
113        /// Query parameter name to extract
114        param: String,
115    },
116    /// Use a combination of values
117    Composite {
118        /// List of extractors to try in order
119        extractors: Vec<ResourceIdExtract>,
120    },
121}
122
123/// State-based response configuration
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct StateResponse {
126    /// HTTP status code for this state
127    pub status_code: u16,
128    /// Response headers
129    pub headers: HashMap<String, String>,
130    /// Response body template
131    pub body_template: String,
132    /// Content type
133    pub content_type: String,
134}
135
136/// Transition trigger configuration
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TransitionTrigger {
139    /// HTTP method that triggers this transition (as string)
140    #[serde(with = "method_serde")]
141    pub method: Method,
142    /// Path pattern that triggers this transition
143    pub path_pattern: String,
144    /// Source state
145    pub from_state: String,
146    /// Target state
147    pub to_state: String,
148    /// Optional condition (JSONPath expression)
149    pub condition: Option<String>,
150}
151
152mod method_serde {
153    use axum::http::Method;
154    use serde::{Deserialize, Deserializer, Serialize, Serializer};
155
156    pub fn serialize<S>(method: &Method, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: Serializer,
159    {
160        method.as_str().serialize(serializer)
161    }
162
163    pub fn deserialize<'de, D>(deserializer: D) -> Result<Method, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let s = String::deserialize(deserializer)?;
168        Method::from_bytes(s.as_bytes()).map_err(serde::de::Error::custom)
169    }
170}
171
172/// Stateful response handler
173pub struct StatefulResponseHandler {
174    /// State machine manager
175    state_manager: Arc<StateMachineManager>,
176    /// Stateful configurations by path pattern
177    configs: Arc<RwLock<HashMap<String, StatefulConfig>>>,
178}
179
180impl StatefulResponseHandler {
181    /// Create a new stateful response handler
182    pub fn new() -> Result<Self> {
183        Ok(Self {
184            state_manager: Arc::new(StateMachineManager::new()),
185            configs: Arc::new(RwLock::new(HashMap::new())),
186        })
187    }
188
189    /// Add a stateful configuration for a path
190    pub async fn add_config(&self, path_pattern: String, config: StatefulConfig) {
191        let mut configs = self.configs.write().await;
192        configs.insert(path_pattern, config);
193    }
194
195    /// Check if this handler can process the request
196    pub async fn can_handle(&self, _method: &Method, path: &str) -> bool {
197        let configs = self.configs.read().await;
198        for (pattern, _) in configs.iter() {
199            if self.path_matches(pattern, path) {
200                return true;
201            }
202        }
203        false
204    }
205
206    /// Process a request and return stateful response if applicable
207    pub async fn process_request(
208        &self,
209        method: &Method,
210        uri: &Uri,
211        headers: &HeaderMap,
212        body: Option<&[u8]>,
213    ) -> Result<Option<StatefulResponse>> {
214        let path = uri.path();
215
216        // Find matching configuration
217        let config = {
218            let configs = self.configs.read().await;
219            configs
220                .iter()
221                .find(|(pattern, _)| self.path_matches(pattern, path))
222                .map(|(_, config)| config.clone())
223        };
224
225        let config = match config {
226            Some(c) => c,
227            None => return Ok(None),
228        };
229
230        // Extract resource ID
231        let resource_id =
232            self.extract_resource_id(&config.resource_id_extract, uri, headers, body)?;
233
234        // Get or create state instance
235        let state_instance = self
236            .state_manager
237            .get_or_create_instance(
238                resource_id.clone(),
239                config.resource_type.clone(),
240                "initial".to_string(), // Default initial state
241            )
242            .await?;
243
244        // Check for transition triggers
245        let new_state = self
246            .check_transitions(&config, method, path, &state_instance, headers, body)
247            .await?;
248
249        // Get current state (after potential transition)
250        let current_state = if let Some(ref state) = new_state {
251            state.clone()
252        } else {
253            state_instance.current_state.clone()
254        };
255
256        // Generate response based on current state
257        let state_response = config.state_responses.get(&current_state).ok_or_else(|| {
258            Error::generic(format!("No response configuration for state '{}'", current_state))
259        })?;
260
261        // Update state instance if transition occurred
262        if let Some(ref new_state) = new_state {
263            let mut updated_instance = state_instance.clone();
264            updated_instance.transition_to(new_state.clone());
265            self.state_manager
266                .update_instance(resource_id.clone(), updated_instance)
267                .await?;
268        }
269
270        Ok(Some(StatefulResponse {
271            status_code: state_response.status_code,
272            headers: state_response.headers.clone(),
273            body: self.render_body_template(&state_response.body_template, &state_instance)?,
274            content_type: state_response.content_type.clone(),
275            state: current_state,
276            resource_id: resource_id.clone(),
277        }))
278    }
279
280    /// Extract resource ID from request
281    fn extract_resource_id(
282        &self,
283        extract: &ResourceIdExtract,
284        uri: &Uri,
285        headers: &HeaderMap,
286        body: Option<&[u8]>,
287    ) -> Result<String> {
288        let path = uri.path();
289        match extract {
290            ResourceIdExtract::PathParam { param } => {
291                // Extract from path (e.g., "/orders/123" with pattern "/orders/{order_id}" -> "123")
292                // Simple implementation: extract last segment or use regex
293                let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
294                if let Some(last) = segments.last() {
295                    Ok(last.to_string())
296                } else {
297                    Err(Error::generic(format!(
298                        "Could not extract path parameter '{}' from path '{}'",
299                        param, path
300                    )))
301                }
302            }
303            ResourceIdExtract::Header { name } => headers
304                .get(name)
305                .and_then(|v| v.to_str().ok())
306                .map(|s| s.to_string())
307                .ok_or_else(|| Error::generic(format!("Header '{}' not found", name))),
308            ResourceIdExtract::QueryParam { param } => {
309                // Extract from query string
310                uri.query()
311                    .and_then(|q| {
312                        url::form_urlencoded::parse(q.as_bytes())
313                            .find(|(k, _)| k == param)
314                            .map(|(_, v)| v.to_string())
315                    })
316                    .ok_or_else(|| Error::generic(format!("Query parameter '{}' not found", param)))
317            }
318            ResourceIdExtract::JsonPath { path: json_path } => {
319                let body_str = body
320                    .and_then(|b| std::str::from_utf8(b).ok())
321                    .ok_or_else(|| Error::generic("Request body is not valid UTF-8".to_string()))?;
322
323                let json: Value = serde_json::from_str(body_str)
324                    .map_err(|e| Error::generic(format!("Invalid JSON body: {}", e)))?;
325
326                // Simple JSONPath implementation (supports $.field notation)
327                self.extract_json_path(&json, json_path)
328            }
329            ResourceIdExtract::Composite { extractors } => {
330                // Try each extractor in order
331                for extract in extractors {
332                    if let Ok(id) = self.extract_resource_id(extract, uri, headers, body) {
333                        return Ok(id);
334                    }
335                }
336                Err(Error::generic("Could not extract resource ID from any source".to_string()))
337            }
338        }
339    }
340
341    /// Extract value from JSON using simple JSONPath
342    fn extract_json_path(&self, json: &Value, path: &str) -> Result<String> {
343        let path = path.trim_start_matches('$').trim_start_matches('.');
344        let parts: Vec<&str> = path.split('.').collect();
345
346        let mut current = json;
347        for part in parts {
348            match current {
349                Value::Object(map) => {
350                    current = map
351                        .get(part)
352                        .ok_or_else(|| Error::generic(format!("Path '{}' not found", path)))?;
353                }
354                Value::Array(arr) => {
355                    let idx: usize = part
356                        .parse()
357                        .map_err(|_| Error::generic(format!("Invalid array index: {}", part)))?;
358                    current = arr.get(idx).ok_or_else(|| {
359                        Error::generic(format!("Array index {} out of bounds", idx))
360                    })?;
361                }
362                _ => {
363                    return Err(Error::generic(format!(
364                        "Cannot traverse path '{}' at '{}'",
365                        path, part
366                    )));
367                }
368            }
369        }
370
371        match current {
372            Value::String(s) => Ok(s.clone()),
373            Value::Number(n) => Ok(n.to_string()),
374            _ => {
375                Err(Error::generic(format!("Path '{}' does not point to a string or number", path)))
376            }
377        }
378    }
379
380    /// Check for transition triggers
381    async fn check_transitions(
382        &self,
383        config: &StatefulConfig,
384        method: &Method,
385        path: &str,
386        instance: &StateInstance,
387        headers: &HeaderMap,
388        body: Option<&[u8]>,
389    ) -> Result<Option<String>> {
390        for transition in &config.transitions {
391            // Check if method and path match
392            if transition.method != *method {
393                continue;
394            }
395
396            if !self.path_matches(&transition.path_pattern, path) {
397                continue;
398            }
399
400            // Check if current state matches
401            if instance.current_state != transition.from_state {
402                continue;
403            }
404
405            // Check condition if present
406            if let Some(ref condition) = transition.condition {
407                if !self.evaluate_condition(condition, headers, body)? {
408                    continue;
409                }
410            }
411
412            // Transition matches!
413            debug!(
414                "State transition triggered: {} -> {} for resource {}",
415                transition.from_state, transition.to_state, instance.resource_id
416            );
417
418            return Ok(Some(transition.to_state.clone()));
419        }
420
421        Ok(None)
422    }
423
424    /// Evaluate a condition expression
425    fn evaluate_condition(
426        &self,
427        condition: &str,
428        _headers: &HeaderMap,
429        body: Option<&[u8]>,
430    ) -> Result<bool> {
431        // Simple condition evaluation (can be enhanced with Rhai later)
432        // For now, support basic JSONPath expressions on body
433        if condition.starts_with("$.") {
434            let body_str = body
435                .and_then(|b| std::str::from_utf8(b).ok())
436                .ok_or_else(|| Error::generic("Request body is not valid UTF-8".to_string()))?;
437
438            let json: Value = serde_json::from_str(body_str)
439                .map_err(|e| Error::generic(format!("Invalid JSON body: {}", e)))?;
440
441            // Extract value and check if it's truthy
442            let value = self.extract_json_path(&json, condition)?;
443            Ok(!value.is_empty() && value != "false" && value != "0")
444        } else {
445            // Default: condition is true if present
446            Ok(true)
447        }
448    }
449
450    /// Render body template with state data
451    fn render_body_template(&self, template: &str, instance: &StateInstance) -> Result<String> {
452        let mut result = template.to_string();
453
454        // Replace {{state}} with current state
455        result = result.replace("{{state}}", &instance.current_state);
456
457        // Replace {{resource_id}} with resource ID
458        result = result.replace("{{resource_id}}", &instance.resource_id);
459
460        // Replace state data variables {{state_data.key}}
461        for (key, value) in &instance.state_data {
462            let placeholder = format!("{{{{state_data.{}}}}}", key);
463            let value_str = match value {
464                Value::String(s) => s.clone(),
465                Value::Number(n) => n.to_string(),
466                Value::Bool(b) => b.to_string(),
467                _ => serde_json::to_string(value).unwrap_or_default(),
468            };
469            result = result.replace(&placeholder, &value_str);
470        }
471
472        Ok(result)
473    }
474
475    /// Process a stub with state machine configuration
476    ///
477    /// This method extracts resource ID, manages state, and returns state information
478    /// that can be used to select or modify stub responses based on current state.
479    ///
480    /// Returns:
481    /// - `Ok(Some(StateInfo))` if state machine config exists and state was processed
482    /// - `Ok(None)` if no state machine config or state processing not applicable
483    /// - `Err` if there was an error processing state
484    #[allow(clippy::too_many_arguments)]
485    pub async fn process_stub_state(
486        &self,
487        method: &Method,
488        uri: &Uri,
489        headers: &HeaderMap,
490        body: Option<&[u8]>,
491        resource_type: &str,
492        resource_id_extract: &ResourceIdExtract,
493        initial_state: &str,
494        transitions: Option<&[TransitionTrigger]>,
495    ) -> Result<Option<StateInfo>> {
496        // Extract resource ID
497        let resource_id = self.extract_resource_id(resource_id_extract, uri, headers, body)?;
498
499        // Get or create state instance
500        let state_instance = self
501            .state_manager
502            .get_or_create_instance(
503                resource_id.clone(),
504                resource_type.to_string(),
505                initial_state.to_string(),
506            )
507            .await?;
508
509        // Check for transition triggers if provided
510        let new_state = if let Some(transition_list) = transitions {
511            let path = uri.path();
512            // Create a temporary config-like structure for transition checking
513            // We'll check transitions manually since we don't have a full StatefulConfig
514            let mut transitioned_state = None;
515
516            for transition in transition_list {
517                // Check if method and path match
518                if transition.method != *method {
519                    continue;
520                }
521
522                if !self.path_matches(&transition.path_pattern, path) {
523                    continue;
524                }
525
526                // Check if current state matches
527                if state_instance.current_state != transition.from_state {
528                    continue;
529                }
530
531                // Check condition if present
532                if let Some(ref condition) = transition.condition {
533                    if !self.evaluate_condition(condition, headers, body)? {
534                        continue;
535                    }
536                }
537
538                // Transition matches!
539                debug!(
540                    "State transition triggered in stub processing: {} -> {} for resource {}",
541                    transition.from_state, transition.to_state, resource_id
542                );
543
544                transitioned_state = Some(transition.to_state.clone());
545                break; // Use first matching transition
546            }
547
548            transitioned_state
549        } else {
550            None
551        };
552
553        // Update state if transition occurred
554        let final_state = if let Some(ref new_state) = new_state {
555            let mut updated_instance = state_instance.clone();
556            updated_instance.transition_to(new_state.clone());
557            self.state_manager
558                .update_instance(resource_id.clone(), updated_instance)
559                .await?;
560            new_state.clone()
561        } else {
562            state_instance.current_state.clone()
563        };
564
565        Ok(Some(StateInfo {
566            resource_id: resource_id.clone(),
567            current_state: final_state,
568            state_data: state_instance.state_data.clone(),
569        }))
570    }
571
572    /// Update state for a resource (for use with stub transitions)
573    pub async fn update_resource_state(
574        &self,
575        resource_id: &str,
576        resource_type: &str,
577        new_state: &str,
578    ) -> Result<()> {
579        let mut instances = self.state_manager.instances.write().await;
580        if let Some(instance) = instances.get_mut(resource_id) {
581            if instance.resource_type == resource_type {
582                instance.transition_to(new_state.to_string());
583                return Ok(());
584            }
585        }
586        Err(Error::generic(format!(
587            "Resource '{}' of type '{}' not found",
588            resource_id, resource_type
589        )))
590    }
591
592    /// Get current state for a resource
593    pub async fn get_resource_state(
594        &self,
595        resource_id: &str,
596        resource_type: &str,
597    ) -> Result<Option<StateInfo>> {
598        let instances = self.state_manager.instances.read().await;
599        if let Some(instance) = instances.get(resource_id) {
600            if instance.resource_type == resource_type {
601                return Ok(Some(StateInfo {
602                    resource_id: resource_id.to_string(),
603                    current_state: instance.current_state.clone(),
604                    state_data: instance.state_data.clone(),
605                }));
606            }
607        }
608        Ok(None)
609    }
610
611    /// Check if path matches pattern (simple wildcard matching)
612    fn path_matches(&self, pattern: &str, path: &str) -> bool {
613        // Simple pattern matching: support {param} and * wildcards
614        let pattern_regex = pattern.replace("{", "(?P<").replace("}", ">[^/]+)").replace("*", ".*");
615        let regex = regex::Regex::new(&format!("^{}$", pattern_regex));
616        match regex {
617            Ok(re) => re.is_match(path),
618            Err(_) => pattern == path, // Fallback to exact match
619        }
620    }
621}
622
623/// State information for stub response selection
624#[derive(Debug, Clone)]
625pub struct StateInfo {
626    /// Resource ID
627    pub resource_id: String,
628    /// Current state name
629    pub current_state: String,
630    /// State data (key-value pairs)
631    pub state_data: HashMap<String, Value>,
632}
633
634/// Stateful response
635#[derive(Debug, Clone)]
636pub struct StatefulResponse {
637    /// HTTP status code
638    pub status_code: u16,
639    /// Response headers
640    pub headers: HashMap<String, String>,
641    /// Response body
642    pub body: String,
643    /// Content type
644    pub content_type: String,
645    /// Current state
646    pub state: String,
647    /// Resource ID
648    pub resource_id: String,
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    // =========================================================================
656    // StateInstance tests
657    // =========================================================================
658
659    #[test]
660    fn test_state_instance_new() {
661        let instance =
662            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
663        assert_eq!(instance.resource_id, "order-123");
664        assert_eq!(instance.resource_type, "order");
665        assert_eq!(instance.current_state, "pending");
666        assert!(instance.state_data.is_empty());
667    }
668
669    #[test]
670    fn test_state_instance_transition_to() {
671        let mut instance =
672            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
673        instance.transition_to("confirmed".to_string());
674        assert_eq!(instance.current_state, "confirmed");
675
676        instance.transition_to("shipped".to_string());
677        assert_eq!(instance.current_state, "shipped");
678    }
679
680    #[test]
681    fn test_state_instance_clone() {
682        let instance =
683            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
684        let cloned = instance.clone();
685        assert_eq!(cloned.resource_id, instance.resource_id);
686        assert_eq!(cloned.current_state, instance.current_state);
687    }
688
689    #[test]
690    fn test_state_instance_debug() {
691        let instance =
692            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
693        let debug_str = format!("{:?}", instance);
694        assert!(debug_str.contains("order-123"));
695        assert!(debug_str.contains("pending"));
696    }
697
698    // =========================================================================
699    // StateMachineManager tests
700    // =========================================================================
701
702    #[tokio::test]
703    async fn test_state_machine_manager_new() {
704        let manager = StateMachineManager::new();
705        let instances = manager.instances.read().await;
706        assert!(instances.is_empty());
707    }
708
709    #[tokio::test]
710    async fn test_state_machine_manager_get_or_create_new() {
711        let manager = StateMachineManager::new();
712        let instance = manager
713            .get_or_create_instance(
714                "order-123".to_string(),
715                "order".to_string(),
716                "pending".to_string(),
717            )
718            .await
719            .unwrap();
720        assert_eq!(instance.resource_id, "order-123");
721        assert_eq!(instance.current_state, "pending");
722    }
723
724    #[tokio::test]
725    async fn test_state_machine_manager_get_or_create_existing() {
726        let manager = StateMachineManager::new();
727
728        // Create initial instance
729        let instance1 = manager
730            .get_or_create_instance(
731                "order-123".to_string(),
732                "order".to_string(),
733                "pending".to_string(),
734            )
735            .await
736            .unwrap();
737        assert_eq!(instance1.current_state, "pending");
738
739        // Get the same instance - should return existing with same state
740        let instance2 = manager
741            .get_or_create_instance(
742                "order-123".to_string(),
743                "order".to_string(),
744                "confirmed".to_string(), // Different initial state - should be ignored
745            )
746            .await
747            .unwrap();
748        assert_eq!(instance2.current_state, "pending"); // Still pending
749    }
750
751    #[tokio::test]
752    async fn test_state_machine_manager_update_instance() {
753        let manager = StateMachineManager::new();
754
755        // Create initial instance
756        let mut instance = manager
757            .get_or_create_instance(
758                "order-123".to_string(),
759                "order".to_string(),
760                "pending".to_string(),
761            )
762            .await
763            .unwrap();
764
765        // Update state
766        instance.transition_to("confirmed".to_string());
767        manager.update_instance("order-123".to_string(), instance).await.unwrap();
768
769        // Verify update
770        let updated = manager
771            .get_or_create_instance(
772                "order-123".to_string(),
773                "order".to_string(),
774                "pending".to_string(),
775            )
776            .await
777            .unwrap();
778        assert_eq!(updated.current_state, "confirmed");
779    }
780
781    // =========================================================================
782    // StatefulConfig tests
783    // =========================================================================
784
785    #[test]
786    fn test_stateful_config_serialize_deserialize() {
787        let config = StatefulConfig {
788            resource_id_extract: ResourceIdExtract::PathParam {
789                param: "order_id".to_string(),
790            },
791            resource_type: "order".to_string(),
792            state_responses: {
793                let mut map = HashMap::new();
794                map.insert(
795                    "pending".to_string(),
796                    StateResponse {
797                        status_code: 200,
798                        headers: HashMap::new(),
799                        body_template: "{\"status\": \"pending\"}".to_string(),
800                        content_type: "application/json".to_string(),
801                    },
802                );
803                map
804            },
805            transitions: vec![],
806        };
807
808        let json = serde_json::to_string(&config).unwrap();
809        let deserialized: StatefulConfig = serde_json::from_str(&json).unwrap();
810        assert_eq!(deserialized.resource_type, "order");
811    }
812
813    #[test]
814    fn test_stateful_config_debug() {
815        let config = StatefulConfig {
816            resource_id_extract: ResourceIdExtract::PathParam {
817                param: "order_id".to_string(),
818            },
819            resource_type: "order".to_string(),
820            state_responses: HashMap::new(),
821            transitions: vec![],
822        };
823        let debug_str = format!("{:?}", config);
824        assert!(debug_str.contains("order"));
825    }
826
827    #[test]
828    fn test_stateful_config_clone() {
829        let config = StatefulConfig {
830            resource_id_extract: ResourceIdExtract::PathParam {
831                param: "id".to_string(),
832            },
833            resource_type: "user".to_string(),
834            state_responses: HashMap::new(),
835            transitions: vec![],
836        };
837        let cloned = config.clone();
838        assert_eq!(cloned.resource_type, "user");
839    }
840
841    // =========================================================================
842    // ResourceIdExtract tests
843    // =========================================================================
844
845    #[test]
846    fn test_resource_id_extract_path_param() {
847        let extract = ResourceIdExtract::PathParam {
848            param: "order_id".to_string(),
849        };
850        let json = serde_json::to_string(&extract).unwrap();
851        assert!(json.contains("path_param"));
852    }
853
854    #[test]
855    fn test_resource_id_extract_json_path() {
856        let extract = ResourceIdExtract::JsonPath {
857            path: "$.order.id".to_string(),
858        };
859        let json = serde_json::to_string(&extract).unwrap();
860        assert!(json.contains("json_path"));
861    }
862
863    #[test]
864    fn test_resource_id_extract_header() {
865        let extract = ResourceIdExtract::Header {
866            name: "X-Order-ID".to_string(),
867        };
868        let json = serde_json::to_string(&extract).unwrap();
869        assert!(json.contains("header"));
870    }
871
872    #[test]
873    fn test_resource_id_extract_query_param() {
874        let extract = ResourceIdExtract::QueryParam {
875            param: "order_id".to_string(),
876        };
877        let json = serde_json::to_string(&extract).unwrap();
878        assert!(json.contains("query_param"));
879    }
880
881    #[test]
882    fn test_resource_id_extract_composite() {
883        let extract = ResourceIdExtract::Composite {
884            extractors: vec![
885                ResourceIdExtract::PathParam {
886                    param: "id".to_string(),
887                },
888                ResourceIdExtract::Header {
889                    name: "X-ID".to_string(),
890                },
891            ],
892        };
893        let json = serde_json::to_string(&extract).unwrap();
894        assert!(json.contains("composite"));
895    }
896
897    // =========================================================================
898    // StateResponse tests
899    // =========================================================================
900
901    #[test]
902    fn test_state_response_serialize_deserialize() {
903        let response = StateResponse {
904            status_code: 200,
905            headers: {
906                let mut h = HashMap::new();
907                h.insert("X-State".to_string(), "pending".to_string());
908                h
909            },
910            body_template: "{\"state\": \"{{state}}\"}".to_string(),
911            content_type: "application/json".to_string(),
912        };
913        let json = serde_json::to_string(&response).unwrap();
914        let deserialized: StateResponse = serde_json::from_str(&json).unwrap();
915        assert_eq!(deserialized.status_code, 200);
916        assert_eq!(deserialized.content_type, "application/json");
917    }
918
919    #[test]
920    fn test_state_response_clone() {
921        let response = StateResponse {
922            status_code: 201,
923            headers: HashMap::new(),
924            body_template: "{}".to_string(),
925            content_type: "text/plain".to_string(),
926        };
927        let cloned = response.clone();
928        assert_eq!(cloned.status_code, 201);
929    }
930
931    #[test]
932    fn test_state_response_debug() {
933        let response = StateResponse {
934            status_code: 404,
935            headers: HashMap::new(),
936            body_template: "Not found".to_string(),
937            content_type: "text/plain".to_string(),
938        };
939        let debug_str = format!("{:?}", response);
940        assert!(debug_str.contains("404"));
941    }
942
943    // =========================================================================
944    // TransitionTrigger tests
945    // =========================================================================
946
947    #[test]
948    fn test_transition_trigger_serialize_deserialize() {
949        let trigger = TransitionTrigger {
950            method: Method::POST,
951            path_pattern: "/orders/{id}/confirm".to_string(),
952            from_state: "pending".to_string(),
953            to_state: "confirmed".to_string(),
954            condition: None,
955        };
956        let json = serde_json::to_string(&trigger).unwrap();
957        let deserialized: TransitionTrigger = serde_json::from_str(&json).unwrap();
958        assert_eq!(deserialized.from_state, "pending");
959        assert_eq!(deserialized.to_state, "confirmed");
960    }
961
962    #[test]
963    fn test_transition_trigger_with_condition() {
964        let trigger = TransitionTrigger {
965            method: Method::POST,
966            path_pattern: "/orders/{id}/ship".to_string(),
967            from_state: "confirmed".to_string(),
968            to_state: "shipped".to_string(),
969            condition: Some("$.payment.verified".to_string()),
970        };
971        let json = serde_json::to_string(&trigger).unwrap();
972        assert!(json.contains("payment.verified"));
973    }
974
975    #[test]
976    fn test_transition_trigger_clone() {
977        let trigger = TransitionTrigger {
978            method: Method::DELETE,
979            path_pattern: "/orders/{id}".to_string(),
980            from_state: "pending".to_string(),
981            to_state: "cancelled".to_string(),
982            condition: None,
983        };
984        let cloned = trigger.clone();
985        assert_eq!(cloned.method, Method::DELETE);
986    }
987
988    // =========================================================================
989    // StatefulResponseHandler tests
990    // =========================================================================
991
992    #[tokio::test]
993    async fn test_stateful_response_handler_new() {
994        let handler = StatefulResponseHandler::new().unwrap();
995        let configs = handler.configs.read().await;
996        assert!(configs.is_empty());
997    }
998
999    #[tokio::test]
1000    async fn test_stateful_response_handler_add_config() {
1001        let handler = StatefulResponseHandler::new().unwrap();
1002        let config = StatefulConfig {
1003            resource_id_extract: ResourceIdExtract::PathParam {
1004                param: "id".to_string(),
1005            },
1006            resource_type: "order".to_string(),
1007            state_responses: HashMap::new(),
1008            transitions: vec![],
1009        };
1010
1011        handler.add_config("/orders/{id}".to_string(), config).await;
1012
1013        let configs = handler.configs.read().await;
1014        assert!(configs.contains_key("/orders/{id}"));
1015    }
1016
1017    #[tokio::test]
1018    async fn test_stateful_response_handler_can_handle_true() {
1019        let handler = StatefulResponseHandler::new().unwrap();
1020        let config = StatefulConfig {
1021            resource_id_extract: ResourceIdExtract::PathParam {
1022                param: "id".to_string(),
1023            },
1024            resource_type: "order".to_string(),
1025            state_responses: HashMap::new(),
1026            transitions: vec![],
1027        };
1028
1029        handler.add_config("/orders/{id}".to_string(), config).await;
1030
1031        assert!(handler.can_handle(&Method::GET, "/orders/123").await);
1032    }
1033
1034    #[tokio::test]
1035    async fn test_stateful_response_handler_can_handle_false() {
1036        let handler = StatefulResponseHandler::new().unwrap();
1037        assert!(!handler.can_handle(&Method::GET, "/orders/123").await);
1038    }
1039
1040    // =========================================================================
1041    // Path matching tests
1042    // =========================================================================
1043
1044    #[test]
1045    fn test_path_matching() {
1046        let handler = StatefulResponseHandler::new().unwrap();
1047
1048        assert!(handler.path_matches("/orders/{id}", "/orders/123"));
1049        assert!(handler.path_matches("/api/*", "/api/users"));
1050        assert!(!handler.path_matches("/orders/{id}", "/orders/123/items"));
1051    }
1052
1053    #[test]
1054    fn test_path_matching_exact() {
1055        let handler = StatefulResponseHandler::new().unwrap();
1056        assert!(handler.path_matches("/api/health", "/api/health"));
1057        assert!(!handler.path_matches("/api/health", "/api/health/check"));
1058    }
1059
1060    #[test]
1061    fn test_path_matching_multiple_params() {
1062        let handler = StatefulResponseHandler::new().unwrap();
1063        assert!(handler.path_matches("/users/{user_id}/orders/{order_id}", "/users/1/orders/2"));
1064    }
1065
1066    #[test]
1067    fn test_path_matching_wildcard() {
1068        let handler = StatefulResponseHandler::new().unwrap();
1069        assert!(handler.path_matches("/api/*", "/api/anything"));
1070        assert!(handler.path_matches("/api/*", "/api/users/123"));
1071    }
1072
1073    // =========================================================================
1074    // Resource ID extraction tests
1075    // =========================================================================
1076
1077    #[tokio::test]
1078    async fn test_extract_resource_id_from_path() {
1079        let handler = StatefulResponseHandler::new().unwrap();
1080        let extract = ResourceIdExtract::PathParam {
1081            param: "order_id".to_string(),
1082        };
1083        let uri: Uri = "/orders/12345".parse().unwrap();
1084        let headers = HeaderMap::new();
1085
1086        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1087        assert_eq!(id, "12345");
1088    }
1089
1090    #[tokio::test]
1091    async fn test_extract_resource_id_from_header() {
1092        let handler = StatefulResponseHandler::new().unwrap();
1093        let extract = ResourceIdExtract::Header {
1094            name: "x-order-id".to_string(),
1095        };
1096        let uri: Uri = "/orders".parse().unwrap();
1097        let mut headers = HeaderMap::new();
1098        headers.insert("x-order-id", "order-abc".parse().unwrap());
1099
1100        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1101        assert_eq!(id, "order-abc");
1102    }
1103
1104    #[tokio::test]
1105    async fn test_extract_resource_id_from_query_param() {
1106        let handler = StatefulResponseHandler::new().unwrap();
1107        let extract = ResourceIdExtract::QueryParam {
1108            param: "id".to_string(),
1109        };
1110        let uri: Uri = "/orders?id=query-123".parse().unwrap();
1111        let headers = HeaderMap::new();
1112
1113        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1114        assert_eq!(id, "query-123");
1115    }
1116
1117    #[tokio::test]
1118    async fn test_extract_resource_id_from_json_body() {
1119        let handler = StatefulResponseHandler::new().unwrap();
1120        let extract = ResourceIdExtract::JsonPath {
1121            path: "$.order.id".to_string(),
1122        };
1123        let uri: Uri = "/orders".parse().unwrap();
1124        let headers = HeaderMap::new();
1125        let body = br#"{"order": {"id": "json-456"}}"#;
1126
1127        let id = handler.extract_resource_id(&extract, &uri, &headers, Some(body)).unwrap();
1128        assert_eq!(id, "json-456");
1129    }
1130
1131    #[tokio::test]
1132    async fn test_extract_resource_id_composite() {
1133        let handler = StatefulResponseHandler::new().unwrap();
1134        let extract = ResourceIdExtract::Composite {
1135            extractors: vec![
1136                ResourceIdExtract::Header {
1137                    name: "x-id".to_string(),
1138                },
1139                ResourceIdExtract::PathParam {
1140                    param: "id".to_string(),
1141                },
1142            ],
1143        };
1144        let uri: Uri = "/orders/fallback-123".parse().unwrap();
1145        let headers = HeaderMap::new(); // No header, should fall back to path
1146
1147        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1148        assert_eq!(id, "fallback-123");
1149    }
1150
1151    #[tokio::test]
1152    async fn test_extract_resource_id_header_not_found() {
1153        let handler = StatefulResponseHandler::new().unwrap();
1154        let extract = ResourceIdExtract::Header {
1155            name: "x-missing".to_string(),
1156        };
1157        let uri: Uri = "/orders".parse().unwrap();
1158        let headers = HeaderMap::new();
1159
1160        let result = handler.extract_resource_id(&extract, &uri, &headers, None);
1161        assert!(result.is_err());
1162    }
1163
1164    // =========================================================================
1165    // JSON path extraction tests
1166    // =========================================================================
1167
1168    #[test]
1169    fn test_extract_json_path_simple() {
1170        let handler = StatefulResponseHandler::new().unwrap();
1171        let json: Value = serde_json::json!({"id": "123"});
1172        let result = handler.extract_json_path(&json, "$.id").unwrap();
1173        assert_eq!(result, "123");
1174    }
1175
1176    #[test]
1177    fn test_extract_json_path_nested() {
1178        let handler = StatefulResponseHandler::new().unwrap();
1179        let json: Value = serde_json::json!({"order": {"details": {"id": "456"}}});
1180        let result = handler.extract_json_path(&json, "$.order.details.id").unwrap();
1181        assert_eq!(result, "456");
1182    }
1183
1184    #[test]
1185    fn test_extract_json_path_number() {
1186        let handler = StatefulResponseHandler::new().unwrap();
1187        let json: Value = serde_json::json!({"count": 42});
1188        let result = handler.extract_json_path(&json, "$.count").unwrap();
1189        assert_eq!(result, "42");
1190    }
1191
1192    #[test]
1193    fn test_extract_json_path_array_index() {
1194        let handler = StatefulResponseHandler::new().unwrap();
1195        let json: Value = serde_json::json!({"items": ["a", "b", "c"]});
1196        let result = handler.extract_json_path(&json, "$.items.1").unwrap();
1197        assert_eq!(result, "b");
1198    }
1199
1200    #[test]
1201    fn test_extract_json_path_not_found() {
1202        let handler = StatefulResponseHandler::new().unwrap();
1203        let json: Value = serde_json::json!({"other": "value"});
1204        let result = handler.extract_json_path(&json, "$.missing");
1205        assert!(result.is_err());
1206    }
1207
1208    // =========================================================================
1209    // Body template rendering tests
1210    // =========================================================================
1211
1212    #[test]
1213    fn test_render_body_template_state() {
1214        let handler = StatefulResponseHandler::new().unwrap();
1215        let instance =
1216            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
1217        let template = r#"{"status": "{{state}}"}"#;
1218        let result = handler.render_body_template(template, &instance).unwrap();
1219        assert_eq!(result, r#"{"status": "pending"}"#);
1220    }
1221
1222    #[test]
1223    fn test_render_body_template_resource_id() {
1224        let handler = StatefulResponseHandler::new().unwrap();
1225        let instance =
1226            StateInstance::new("order-456".to_string(), "order".to_string(), "shipped".to_string());
1227        let template = r#"{"id": "{{resource_id}}"}"#;
1228        let result = handler.render_body_template(template, &instance).unwrap();
1229        assert_eq!(result, r#"{"id": "order-456"}"#);
1230    }
1231
1232    #[test]
1233    fn test_render_body_template_state_data() {
1234        let handler = StatefulResponseHandler::new().unwrap();
1235        let mut instance =
1236            StateInstance::new("order-789".to_string(), "order".to_string(), "shipped".to_string());
1237        instance
1238            .state_data
1239            .insert("carrier".to_string(), Value::String("FedEx".to_string()));
1240        let template = r#"{"carrier": "{{state_data.carrier}}"}"#;
1241        let result = handler.render_body_template(template, &instance).unwrap();
1242        assert_eq!(result, r#"{"carrier": "FedEx"}"#);
1243    }
1244
1245    #[test]
1246    fn test_render_body_template_multiple_placeholders() {
1247        let handler = StatefulResponseHandler::new().unwrap();
1248        let instance = StateInstance::new(
1249            "order-abc".to_string(),
1250            "order".to_string(),
1251            "confirmed".to_string(),
1252        );
1253        let template = r#"{"id": "{{resource_id}}", "status": "{{state}}"}"#;
1254        let result = handler.render_body_template(template, &instance).unwrap();
1255        assert_eq!(result, r#"{"id": "order-abc", "status": "confirmed"}"#);
1256    }
1257
1258    // =========================================================================
1259    // Process request tests
1260    // =========================================================================
1261
1262    #[tokio::test]
1263    async fn test_process_request_no_config() {
1264        let handler = StatefulResponseHandler::new().unwrap();
1265        let uri: Uri = "/orders/123".parse().unwrap();
1266        let headers = HeaderMap::new();
1267
1268        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1269        assert!(result.is_none());
1270    }
1271
1272    #[tokio::test]
1273    async fn test_process_request_with_config() {
1274        let handler = StatefulResponseHandler::new().unwrap();
1275        let mut state_responses = HashMap::new();
1276        state_responses.insert(
1277            "initial".to_string(),
1278            StateResponse {
1279                status_code: 200,
1280                headers: HashMap::new(),
1281                body_template: r#"{"state": "{{state}}", "id": "{{resource_id}}"}"#.to_string(),
1282                content_type: "application/json".to_string(),
1283            },
1284        );
1285
1286        let config = StatefulConfig {
1287            resource_id_extract: ResourceIdExtract::PathParam {
1288                param: "id".to_string(),
1289            },
1290            resource_type: "order".to_string(),
1291            state_responses,
1292            transitions: vec![],
1293        };
1294
1295        handler.add_config("/orders/{id}".to_string(), config).await;
1296
1297        let uri: Uri = "/orders/test-123".parse().unwrap();
1298        let headers = HeaderMap::new();
1299
1300        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1301        assert!(result.is_some());
1302
1303        let response = result.unwrap();
1304        assert_eq!(response.status_code, 200);
1305        assert_eq!(response.state, "initial");
1306        assert_eq!(response.resource_id, "test-123");
1307        assert!(response.body.contains("test-123"));
1308    }
1309
1310    #[tokio::test]
1311    async fn test_process_request_with_transition() {
1312        let handler = StatefulResponseHandler::new().unwrap();
1313        let mut state_responses = HashMap::new();
1314        state_responses.insert(
1315            "initial".to_string(),
1316            StateResponse {
1317                status_code: 200,
1318                headers: HashMap::new(),
1319                body_template: r#"{"state": "{{state}}"}"#.to_string(),
1320                content_type: "application/json".to_string(),
1321            },
1322        );
1323        state_responses.insert(
1324            "confirmed".to_string(),
1325            StateResponse {
1326                status_code: 200,
1327                headers: HashMap::new(),
1328                body_template: r#"{"state": "{{state}}"}"#.to_string(),
1329                content_type: "application/json".to_string(),
1330            },
1331        );
1332
1333        let config = StatefulConfig {
1334            resource_id_extract: ResourceIdExtract::PathParam {
1335                param: "id".to_string(),
1336            },
1337            resource_type: "order".to_string(),
1338            state_responses,
1339            transitions: vec![TransitionTrigger {
1340                method: Method::POST,
1341                path_pattern: "/orders/{id}".to_string(),
1342                from_state: "initial".to_string(),
1343                to_state: "confirmed".to_string(),
1344                condition: None,
1345            }],
1346        };
1347
1348        handler.add_config("/orders/{id}".to_string(), config).await;
1349
1350        // First request - should be in initial state
1351        let uri: Uri = "/orders/order-1".parse().unwrap();
1352        let headers = HeaderMap::new();
1353
1354        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1355        assert_eq!(result.unwrap().state, "initial");
1356
1357        // POST to trigger transition
1358        let result = handler.process_request(&Method::POST, &uri, &headers, None).await.unwrap();
1359        assert_eq!(result.unwrap().state, "confirmed");
1360
1361        // Subsequent GET should show confirmed state
1362        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1363        assert_eq!(result.unwrap().state, "confirmed");
1364    }
1365
1366    // =========================================================================
1367    // Stub state processing tests
1368    // =========================================================================
1369
1370    #[tokio::test]
1371    async fn test_process_stub_state() {
1372        let handler = StatefulResponseHandler::new().unwrap();
1373        let extract = ResourceIdExtract::PathParam {
1374            param: "id".to_string(),
1375        };
1376        let uri: Uri = "/users/user-123".parse().unwrap();
1377        let headers = HeaderMap::new();
1378
1379        let result = handler
1380            .process_stub_state(
1381                &Method::GET,
1382                &uri,
1383                &headers,
1384                None,
1385                "user",
1386                &extract,
1387                "active",
1388                None,
1389            )
1390            .await
1391            .unwrap();
1392
1393        assert!(result.is_some());
1394        let info = result.unwrap();
1395        assert_eq!(info.resource_id, "user-123");
1396        assert_eq!(info.current_state, "active");
1397    }
1398
1399    #[tokio::test]
1400    async fn test_process_stub_state_with_transition() {
1401        let handler = StatefulResponseHandler::new().unwrap();
1402        let extract = ResourceIdExtract::PathParam {
1403            param: "id".to_string(),
1404        };
1405        let uri: Uri = "/users/user-456".parse().unwrap();
1406        let headers = HeaderMap::new();
1407
1408        // Create initial state
1409        let _ = handler
1410            .process_stub_state(
1411                &Method::GET,
1412                &uri,
1413                &headers,
1414                None,
1415                "user",
1416                &extract,
1417                "active",
1418                None,
1419            )
1420            .await
1421            .unwrap();
1422
1423        // Now process with transition
1424        let transitions = vec![TransitionTrigger {
1425            method: Method::DELETE,
1426            path_pattern: "/users/{id}".to_string(),
1427            from_state: "active".to_string(),
1428            to_state: "deleted".to_string(),
1429            condition: None,
1430        }];
1431
1432        let result = handler
1433            .process_stub_state(
1434                &Method::DELETE,
1435                &uri,
1436                &headers,
1437                None,
1438                "user",
1439                &extract,
1440                "active",
1441                Some(&transitions),
1442            )
1443            .await
1444            .unwrap();
1445
1446        let info = result.unwrap();
1447        assert_eq!(info.current_state, "deleted");
1448    }
1449
1450    // =========================================================================
1451    // Get/Update resource state tests
1452    // =========================================================================
1453
1454    #[tokio::test]
1455    async fn test_get_resource_state_not_found() {
1456        let handler = StatefulResponseHandler::new().unwrap();
1457        let result = handler.get_resource_state("nonexistent", "order").await.unwrap();
1458        assert!(result.is_none());
1459    }
1460
1461    #[tokio::test]
1462    async fn test_get_resource_state_exists() {
1463        let handler = StatefulResponseHandler::new().unwrap();
1464
1465        // Create a resource via stub processing
1466        let extract = ResourceIdExtract::PathParam {
1467            param: "id".to_string(),
1468        };
1469        let uri: Uri = "/orders/order-999".parse().unwrap();
1470        let headers = HeaderMap::new();
1471
1472        handler
1473            .process_stub_state(
1474                &Method::GET,
1475                &uri,
1476                &headers,
1477                None,
1478                "order",
1479                &extract,
1480                "pending",
1481                None,
1482            )
1483            .await
1484            .unwrap();
1485
1486        // Now get the state
1487        let result = handler.get_resource_state("order-999", "order").await.unwrap();
1488        assert!(result.is_some());
1489        assert_eq!(result.unwrap().current_state, "pending");
1490    }
1491
1492    #[tokio::test]
1493    async fn test_update_resource_state() {
1494        let handler = StatefulResponseHandler::new().unwrap();
1495
1496        // Create a resource via stub processing
1497        let extract = ResourceIdExtract::PathParam {
1498            param: "id".to_string(),
1499        };
1500        let uri: Uri = "/orders/order-update".parse().unwrap();
1501        let headers = HeaderMap::new();
1502
1503        handler
1504            .process_stub_state(
1505                &Method::GET,
1506                &uri,
1507                &headers,
1508                None,
1509                "order",
1510                &extract,
1511                "pending",
1512                None,
1513            )
1514            .await
1515            .unwrap();
1516
1517        // Update the state
1518        handler.update_resource_state("order-update", "order", "shipped").await.unwrap();
1519
1520        // Verify update
1521        let result = handler.get_resource_state("order-update", "order").await.unwrap();
1522        assert_eq!(result.unwrap().current_state, "shipped");
1523    }
1524
1525    #[tokio::test]
1526    async fn test_update_resource_state_not_found() {
1527        let handler = StatefulResponseHandler::new().unwrap();
1528        let result = handler.update_resource_state("nonexistent", "order", "shipped").await;
1529        assert!(result.is_err());
1530    }
1531
1532    #[tokio::test]
1533    async fn test_update_resource_state_wrong_type() {
1534        let handler = StatefulResponseHandler::new().unwrap();
1535
1536        // Create a resource with type "order"
1537        let extract = ResourceIdExtract::PathParam {
1538            param: "id".to_string(),
1539        };
1540        let uri: Uri = "/orders/order-type-test".parse().unwrap();
1541        let headers = HeaderMap::new();
1542
1543        handler
1544            .process_stub_state(
1545                &Method::GET,
1546                &uri,
1547                &headers,
1548                None,
1549                "order",
1550                &extract,
1551                "pending",
1552                None,
1553            )
1554            .await
1555            .unwrap();
1556
1557        // Try to update with wrong type
1558        let result = handler.update_resource_state("order-type-test", "user", "active").await;
1559        assert!(result.is_err());
1560    }
1561
1562    // =========================================================================
1563    // Condition evaluation tests
1564    // =========================================================================
1565
1566    #[test]
1567    fn test_evaluate_condition_json_path_truthy() {
1568        let handler = StatefulResponseHandler::new().unwrap();
1569        let headers = HeaderMap::new();
1570        let body = br#"{"verified": "true"}"#;
1571        let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1572        assert!(result);
1573    }
1574
1575    #[test]
1576    fn test_evaluate_condition_json_path_falsy() {
1577        let handler = StatefulResponseHandler::new().unwrap();
1578        let headers = HeaderMap::new();
1579        let body = br#"{"verified": "false"}"#;
1580        let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1581        assert!(!result);
1582    }
1583
1584    #[test]
1585    fn test_evaluate_condition_non_jsonpath() {
1586        let handler = StatefulResponseHandler::new().unwrap();
1587        let headers = HeaderMap::new();
1588        let result = handler.evaluate_condition("some_condition", &headers, None).unwrap();
1589        assert!(result); // Non-JSONPath conditions default to true
1590    }
1591
1592    // =========================================================================
1593    // StateInfo tests
1594    // =========================================================================
1595
1596    #[test]
1597    fn test_state_info_clone() {
1598        let info = StateInfo {
1599            resource_id: "res-1".to_string(),
1600            current_state: "active".to_string(),
1601            state_data: HashMap::new(),
1602        };
1603        let cloned = info.clone();
1604        assert_eq!(cloned.resource_id, "res-1");
1605    }
1606
1607    #[test]
1608    fn test_state_info_debug() {
1609        let info = StateInfo {
1610            resource_id: "res-2".to_string(),
1611            current_state: "inactive".to_string(),
1612            state_data: HashMap::new(),
1613        };
1614        let debug_str = format!("{:?}", info);
1615        assert!(debug_str.contains("res-2"));
1616        assert!(debug_str.contains("inactive"));
1617    }
1618
1619    // =========================================================================
1620    // StatefulResponse tests
1621    // =========================================================================
1622
1623    #[test]
1624    fn test_stateful_response_clone() {
1625        let response = StatefulResponse {
1626            status_code: 200,
1627            headers: HashMap::new(),
1628            body: "{}".to_string(),
1629            content_type: "application/json".to_string(),
1630            state: "active".to_string(),
1631            resource_id: "res-3".to_string(),
1632        };
1633        let cloned = response.clone();
1634        assert_eq!(cloned.status_code, 200);
1635        assert_eq!(cloned.state, "active");
1636    }
1637
1638    #[test]
1639    fn test_stateful_response_debug() {
1640        let response = StatefulResponse {
1641            status_code: 404,
1642            headers: HashMap::new(),
1643            body: "Not found".to_string(),
1644            content_type: "text/plain".to_string(),
1645            state: "deleted".to_string(),
1646            resource_id: "res-4".to_string(),
1647        };
1648        let debug_str = format!("{:?}", response);
1649        assert!(debug_str.contains("404"));
1650        assert!(debug_str.contains("deleted"));
1651    }
1652}