Skip to main content

mockforge_core/
workspace.rs

1//! Workspace and folder organization for MockForge requests
2//!
3//! This module has been refactored into sub-modules for better organization:
4//! - core: Core workspace and folder structures
5//! - registry: Workspace registry and management
6//! - environment: Environment configuration and management
7//! - sync: Synchronization functionality
8//! - request: Mock request handling and processing
9
10// Re-export sub-modules for backward compatibility
11pub mod core;
12pub mod environment;
13pub mod mock_environment;
14pub mod promotion_trait;
15pub mod rbac;
16pub mod registry;
17pub mod request;
18pub mod scenario_promotion;
19pub mod sync;
20pub mod template_application;
21
22// Re-export commonly used types
23pub use environment::*;
24pub use mock_environment::*;
25pub use rbac::*;
26pub use registry::*;
27pub use request::*;
28pub use scenario_promotion::*;
29pub use sync::*;
30pub use template_application::*;
31
32// Legacy imports for compatibility
33use crate::config::AuthConfig;
34use crate::encryption::AutoEncryptionConfig;
35use crate::fidelity::FidelityScore;
36use crate::reality::RealityLevel;
37use crate::routing::{HttpMethod, Route, RouteRegistry};
38use crate::{Error, Result};
39use chrono::{DateTime, Utc};
40use serde::{Deserialize, Serialize};
41use std::collections::HashMap;
42use uuid::Uuid;
43
44/// Unique identifier for workspace entities
45pub type EntityId = String;
46
47/// Workspace represents a top-level organizational unit
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Workspace {
50    /// Unique identifier
51    pub id: EntityId,
52    /// Human-readable name
53    pub name: String,
54    /// Optional description
55    pub description: Option<String>,
56    /// Creation timestamp
57    pub created_at: DateTime<Utc>,
58    /// Last modification timestamp
59    pub updated_at: DateTime<Utc>,
60    /// Associated tags for filtering and organization
61    pub tags: Vec<String>,
62    /// Configuration specific to this workspace
63    pub config: WorkspaceConfig,
64    /// Root folders in this workspace
65    pub folders: Vec<Folder>,
66    /// Root requests (not in any folder)
67    pub requests: Vec<MockRequest>,
68    /// Display order for UI sorting (lower numbers appear first)
69    #[serde(default)]
70    pub order: i32,
71}
72
73/// Configuration for request inheritance at folder level
74#[derive(Debug, Clone, Serialize, Deserialize, Default)]
75pub struct FolderInheritanceConfig {
76    /// Headers to be inherited by child requests (if not overridden)
77    #[serde(default)]
78    pub headers: HashMap<String, String>,
79    /// Authentication configuration for inheritance
80    pub auth: Option<AuthConfig>,
81}
82
83/// Folder represents a hierarchical grouping within a workspace
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Folder {
86    /// Unique identifier
87    pub id: EntityId,
88    /// Human-readable name
89    pub name: String,
90    /// Optional description
91    pub description: Option<String>,
92    /// Parent folder ID (None if root folder)
93    pub parent_id: Option<EntityId>,
94    /// Creation timestamp
95    pub created_at: DateTime<Utc>,
96    /// Last modification timestamp
97    pub updated_at: DateTime<Utc>,
98    /// Associated tags
99    pub tags: Vec<String>,
100    /// Inheritance configuration for this folder
101    #[serde(default)]
102    pub inheritance: FolderInheritanceConfig,
103    /// Child folders
104    pub folders: Vec<Folder>,
105    /// Requests in this folder
106    pub requests: Vec<MockRequest>,
107}
108
109/// Mock request definition with metadata
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct MockRequest {
112    /// Unique identifier
113    pub id: EntityId,
114    /// Human-readable name
115    pub name: String,
116    /// Optional description
117    pub description: Option<String>,
118    /// HTTP method
119    pub method: HttpMethod,
120    /// Request path
121    pub path: String,
122    /// HTTP headers
123    pub headers: HashMap<String, String>,
124    /// Query parameters
125    pub query_params: HashMap<String, String>,
126    /// Request body template
127    pub body: Option<String>,
128    /// Expected response
129    pub response: MockResponse,
130    /// History of actual request executions
131    pub response_history: Vec<ResponseHistoryEntry>,
132    /// Creation timestamp
133    pub created_at: DateTime<Utc>,
134    /// Last modification timestamp
135    pub updated_at: DateTime<Utc>,
136    /// Associated tags
137    pub tags: Vec<String>,
138    /// Authentication configuration
139    pub auth: Option<AuthConfig>,
140    /// Priority for route matching (higher = more specific)
141    pub priority: i32,
142}
143
144/// Mock response definition
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct MockResponse {
147    /// HTTP status code
148    pub status_code: u16,
149    /// Response headers
150    pub headers: HashMap<String, String>,
151    /// Response body template
152    pub body: Option<String>,
153    /// Content type
154    pub content_type: Option<String>,
155    /// Response delay in milliseconds
156    pub delay_ms: Option<u64>,
157}
158
159/// Response history entry for tracking actual request executions
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ResponseHistoryEntry {
162    /// Unique execution ID
163    pub id: String,
164    /// Execution timestamp
165    pub executed_at: DateTime<Utc>,
166    /// Actual request method used
167    pub request_method: HttpMethod,
168    /// Actual request path used
169    pub request_path: String,
170    /// Request headers sent
171    pub request_headers: HashMap<String, String>,
172    /// Request body sent
173    pub request_body: Option<String>,
174    /// Response status code received
175    pub response_status_code: u16,
176    /// Response headers received
177    pub response_headers: HashMap<String, String>,
178    /// Response body received
179    pub response_body: Option<String>,
180    /// Response time in milliseconds
181    pub response_time_ms: u64,
182    /// Response size in bytes
183    pub response_size_bytes: u64,
184    /// Error message if execution failed
185    pub error_message: Option<String>,
186}
187
188/// Represents a color for environment visualization
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct EnvironmentColor {
191    /// Hex color code (e.g., "#FF5733")
192    pub hex: String,
193    /// Optional color name for accessibility
194    pub name: Option<String>,
195}
196
197/// Environment for managing variable collections
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Environment {
200    /// Unique identifier
201    pub id: EntityId,
202    /// Human-readable name
203    pub name: String,
204    /// Optional description
205    pub description: Option<String>,
206    /// Color for visual distinction in UI
207    pub color: Option<EnvironmentColor>,
208    /// Environment variables
209    pub variables: HashMap<String, String>,
210    /// Creation timestamp
211    pub created_at: DateTime<Utc>,
212    /// Last modification timestamp
213    pub updated_at: DateTime<Utc>,
214    /// Display order for UI sorting (lower numbers appear first)
215    #[serde(default)]
216    pub order: i32,
217    /// Whether this environment can be shared/synced
218    #[serde(default)]
219    pub sharable: bool,
220}
221
222/// Directory sync configuration for a workspace
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct SyncConfig {
225    /// Enable directory syncing for this workspace
226    pub enabled: bool,
227    /// Target directory for sync (relative or absolute path)
228    pub target_directory: Option<String>,
229    /// Directory structure to use (flat, nested, grouped)
230    pub directory_structure: SyncDirectoryStructure,
231    /// Auto-sync direction (one-way workspace→directory, bidirectional, or manual)
232    pub sync_direction: SyncDirection,
233    /// Whether to include metadata files
234    pub include_metadata: bool,
235    /// Filesystem monitoring enabled for real-time sync
236    pub realtime_monitoring: bool,
237    /// Custom filename pattern for exported files
238    pub filename_pattern: String,
239    /// Regular expression for excluding workspaces/requests
240    pub exclude_pattern: Option<String>,
241    /// Force overwrite existing files during sync
242    pub force_overwrite: bool,
243    /// Last sync timestamp
244    pub last_sync: Option<DateTime<Utc>>,
245}
246
247/// Directory structure options for sync
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub enum SyncDirectoryStructure {
250    /// All workspaces in flat structure: workspace-name.yaml
251    Flat,
252    /// Nested by workspace: workspaces/{name}/workspace.yaml + requests/
253    Nested,
254    /// Grouped by type: requests/, responses/, metadata/
255    Grouped,
256}
257
258/// Sync direction options
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub enum SyncDirection {
261    /// Manual sync only (one-off operations)
262    Manual,
263    /// One-way: workspace changes sync silently to directory
264    WorkspaceToDirectory,
265    /// Bidirectional: changes in either direction trigger sync
266    Bidirectional,
267}
268
269/// Workspace-specific configuration
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct WorkspaceConfig {
272    /// Base URL for all requests in this workspace
273    pub base_url: Option<String>,
274    /// Default headers for all requests
275    pub default_headers: HashMap<String, String>,
276    /// Authentication configuration
277    pub auth: Option<AuthConfig>,
278    /// Global environment (always available)
279    pub global_environment: Environment,
280    /// Sub-environments (switchable)
281    pub environments: Vec<Environment>,
282    /// Currently active environment ID (None means only global)
283    pub active_environment_id: Option<EntityId>,
284    /// Mock environment manager for dev/test/prod environments
285    /// Manages environment-specific overrides for reality levels, chaos profiles, and drift budgets
286    #[serde(default)]
287    pub mock_environments: MockEnvironmentManager,
288    /// Directory sync configuration
289    pub sync: SyncConfig,
290    /// Automatic encryption configuration
291    #[serde(default)]
292    pub auto_encryption: AutoEncryptionConfig,
293    /// Reality level for this workspace (1-5)
294    /// Controls the realism of mock behavior (chaos, latency, MockAI)
295    /// This is the default reality level; can be overridden per environment
296    #[serde(default)]
297    pub reality_level: Option<RealityLevel>,
298    /// AI mode for this workspace
299    /// Controls how AI-generated artifacts are used at runtime
300    #[serde(default)]
301    pub ai_mode: Option<crate::ai_studio::config::AiMode>,
302    /// Current fidelity score for this workspace
303    /// Measures how close the mock is to the real upstream
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub fidelity_score: Option<FidelityScore>,
306}
307
308/// Workspace registry for managing multiple workspaces
309#[derive(Debug, Clone)]
310pub struct WorkspaceRegistry {
311    workspaces: HashMap<EntityId, Workspace>,
312    active_workspace: Option<EntityId>,
313}
314
315impl Workspace {
316    /// Create a new workspace
317    pub fn new(name: String) -> Self {
318        let now = Utc::now();
319        let workspace_id = Uuid::new_v4().to_string();
320        let mut workspace = Self {
321            id: workspace_id.clone(),
322            name,
323            description: None,
324            created_at: now,
325            updated_at: now,
326            tags: Vec::new(),
327            config: WorkspaceConfig::default(),
328            folders: Vec::new(),
329            requests: Vec::new(),
330            order: 0, // Default order will be updated when added to registry
331        };
332
333        // Initialize default mock environments (dev/test/prod)
334        workspace.initialize_default_mock_environments();
335
336        workspace
337    }
338
339    /// Initialize default mock environments for this workspace
340    /// This is called when creating a new workspace or when loading an existing one
341    /// that doesn't have mock environments configured
342    pub fn initialize_default_mock_environments(&mut self) {
343        // Only initialize if mock_environments is empty or has no workspace_id set
344        if self.config.mock_environments.workspace_id.is_empty()
345            || self.config.mock_environments.environments.is_empty()
346        {
347            // Update workspace_id if needed
348            if self.config.mock_environments.workspace_id.is_empty() {
349                self.config.mock_environments.workspace_id = self.id.clone();
350            }
351
352            // Create default dev environment if it doesn't exist
353            if !self
354                .config
355                .mock_environments
356                .environments
357                .contains_key(&MockEnvironmentName::Dev)
358            {
359                let dev_env = MockEnvironment::new(self.id.clone(), MockEnvironmentName::Dev);
360                self.config.mock_environments.add_environment(dev_env);
361            }
362
363            // Create default test environment if it doesn't exist
364            if !self
365                .config
366                .mock_environments
367                .environments
368                .contains_key(&MockEnvironmentName::Test)
369            {
370                let test_env = MockEnvironment::new(self.id.clone(), MockEnvironmentName::Test);
371                self.config.mock_environments.add_environment(test_env);
372            }
373
374            // Create default prod environment if it doesn't exist
375            if !self
376                .config
377                .mock_environments
378                .environments
379                .contains_key(&MockEnvironmentName::Prod)
380            {
381                let prod_env = MockEnvironment::new(self.id.clone(), MockEnvironmentName::Prod);
382                self.config.mock_environments.add_environment(prod_env);
383            }
384
385            // Set dev as the default active environment if none is set
386            if self.config.mock_environments.active_environment.is_none() {
387                let _ =
388                    self.config.mock_environments.set_active_environment(MockEnvironmentName::Dev);
389            }
390        }
391    }
392
393    /// Add a folder to this workspace
394    pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
395        let folder = Folder::new(name);
396        let id = folder.id.clone();
397        self.folders.push(folder);
398        self.updated_at = Utc::now();
399        Ok(id)
400    }
401
402    /// Create a new environment
403    pub fn create_environment(
404        &mut self,
405        name: String,
406        description: Option<String>,
407    ) -> Result<EntityId> {
408        // Check if environment name already exists
409        if self.config.environments.iter().any(|env| env.name == name) {
410            return Err(Error::validation(format!(
411                "Environment with name '{}' already exists",
412                name
413            )));
414        }
415
416        let mut environment = Environment::new(name);
417        environment.description = description;
418
419        // Set order to the end of the list
420        environment.order = self.config.environments.len() as i32;
421
422        let id = environment.id.clone();
423
424        self.config.environments.push(environment);
425        self.updated_at = Utc::now();
426        Ok(id)
427    }
428
429    /// Get all environments (global + sub-environments)
430    pub fn get_environments(&self) -> Vec<&Environment> {
431        let mut all_envs = vec![&self.config.global_environment];
432        all_envs.extend(self.config.environments.iter());
433        all_envs
434    }
435
436    /// Get environment by ID
437    pub fn get_environment(&self, id: &str) -> Option<&Environment> {
438        if self.config.global_environment.id == id {
439            Some(&self.config.global_environment)
440        } else {
441            self.config.environments.iter().find(|env| env.id == id)
442        }
443    }
444
445    /// Get environment by ID (mutable)
446    pub fn get_environment_mut(&mut self, id: &str) -> Option<&mut Environment> {
447        if self.config.global_environment.id == id {
448            Some(&mut self.config.global_environment)
449        } else {
450            self.config.environments.iter_mut().find(|env| env.id == id)
451        }
452    }
453
454    /// Set active environment
455    pub fn set_active_environment(&mut self, environment_id: Option<String>) -> Result<()> {
456        if let Some(ref id) = environment_id {
457            if self.get_environment(id).is_none() {
458                return Err(Error::not_found("Environment", id));
459            }
460        }
461        self.config.active_environment_id = environment_id;
462        self.updated_at = Utc::now();
463        Ok(())
464    }
465
466    /// Get active environment (returns global if no sub-environment is active)
467    pub fn get_active_environment(&self) -> &Environment {
468        if let Some(ref active_id) = self.config.active_environment_id {
469            self.get_environment(active_id).unwrap_or(&self.config.global_environment)
470        } else {
471            &self.config.global_environment
472        }
473    }
474
475    /// Get active environment ID
476    pub fn get_active_environment_id(&self) -> Option<&str> {
477        self.config.active_environment_id.as_deref()
478    }
479
480    /// Get variable value from current active environment
481    pub fn get_variable(&self, key: &str) -> Option<&String> {
482        // First check active environment, then global environment
483        let active_env = self.get_active_environment();
484        active_env.get_variable(key).or_else(|| {
485            if active_env.id != self.config.global_environment.id {
486                self.config.global_environment.get_variable(key)
487            } else {
488                None
489            }
490        })
491    }
492
493    /// Get all variables from current active environment context
494    pub fn get_all_variables(&self) -> HashMap<String, String> {
495        let mut variables = HashMap::new();
496
497        // Start with global environment variables
498        variables.extend(self.config.global_environment.variables.clone());
499
500        // Override with active environment variables if different from global
501        let active_env = self.get_active_environment();
502        if active_env.id != self.config.global_environment.id {
503            variables.extend(active_env.variables.clone());
504        }
505
506        variables
507    }
508
509    /// Delete an environment
510    pub fn delete_environment(&mut self, id: &str) -> Result<()> {
511        if id == self.config.global_environment.id {
512            return Err(Error::validation("Cannot delete global environment"));
513        }
514
515        let position = self.config.environments.iter().position(|env| env.id == id);
516        if let Some(pos) = position {
517            self.config.environments.remove(pos);
518
519            // Clear active environment if it was deleted
520            if self.config.active_environment_id.as_deref() == Some(id) {
521                self.config.active_environment_id = None;
522            }
523
524            self.updated_at = Utc::now();
525            Ok(())
526        } else {
527            Err(Error::not_found("Environment", id))
528        }
529    }
530
531    /// Update the order of environments
532    pub fn update_environments_order(&mut self, environment_ids: Vec<String>) -> Result<()> {
533        // Validate that all provided IDs exist
534        for env_id in &environment_ids {
535            if !self.config.environments.iter().any(|env| env.id == *env_id) {
536                return Err(Error::not_found("Environment", env_id));
537            }
538        }
539
540        // Update order for each environment
541        for (index, env_id) in environment_ids.iter().enumerate() {
542            if let Some(env) = self.config.environments.iter_mut().find(|env| env.id == *env_id) {
543                env.order = index as i32;
544                env.updated_at = Utc::now();
545            }
546        }
547
548        self.updated_at = Utc::now();
549        Ok(())
550    }
551
552    /// Get environments sorted by order
553    pub fn get_environments_ordered(&self) -> Vec<&Environment> {
554        let mut all_envs = vec![&self.config.global_environment];
555        all_envs.extend(self.config.environments.iter());
556        all_envs.sort_by_key(|env| env.order);
557        all_envs
558    }
559
560    /// Get the mock environment manager
561    pub fn get_mock_environments(&self) -> &MockEnvironmentManager {
562        &self.config.mock_environments
563    }
564
565    /// Get mutable access to the mock environment manager
566    pub fn get_mock_environments_mut(&mut self) -> &mut MockEnvironmentManager {
567        &mut self.config.mock_environments
568    }
569
570    /// Get a specific mock environment by name
571    pub fn get_mock_environment(&self, name: MockEnvironmentName) -> Option<&MockEnvironment> {
572        self.config.mock_environments.get_environment(name)
573    }
574
575    /// Get the active mock environment
576    pub fn get_active_mock_environment(&self) -> Option<&MockEnvironment> {
577        self.config.mock_environments.get_active_environment()
578    }
579
580    /// Set the active mock environment
581    pub fn set_active_mock_environment(&mut self, name: MockEnvironmentName) -> Result<()> {
582        self.config.mock_environments.set_active_environment(name)?;
583        self.updated_at = Utc::now();
584        Ok(())
585    }
586
587    /// List all mock environments
588    pub fn list_mock_environments(&self) -> Vec<&MockEnvironment> {
589        self.config.mock_environments.list_environments()
590    }
591
592    /// Add or update a mock environment configuration
593    pub fn set_mock_environment_config(
594        &mut self,
595        name: MockEnvironmentName,
596        reality_config: Option<crate::reality::RealityConfig>,
597        chaos_config: Option<crate::chaos_utilities::ChaosConfig>,
598        drift_budget_config: Option<crate::contract_drift::DriftBudgetConfig>,
599    ) -> Result<()> {
600        // Get or create the environment
601        let env = if let Some(existing) = self.config.mock_environments.get_environment(name) {
602            MockEnvironment::with_configs(
603                existing.workspace_id.clone(),
604                name,
605                reality_config,
606                chaos_config,
607                drift_budget_config,
608            )
609        } else {
610            MockEnvironment::with_configs(
611                self.id.clone(),
612                name,
613                reality_config,
614                chaos_config,
615                drift_budget_config,
616            )
617        };
618
619        self.config.mock_environments.add_environment(env);
620        self.updated_at = Utc::now();
621        Ok(())
622    }
623
624    /// Configure directory sync for this workspace
625    pub fn configure_sync(&mut self, config: SyncConfig) -> Result<()> {
626        self.config.sync = config;
627        self.updated_at = Utc::now();
628        Ok(())
629    }
630
631    /// Enable directory sync with default settings
632    pub fn enable_sync(&mut self, target_directory: String) -> Result<()> {
633        self.config.sync.enabled = true;
634        self.config.sync.target_directory = Some(target_directory);
635        self.config.sync.realtime_monitoring = true; // Enable realtime monitoring by default
636        self.updated_at = Utc::now();
637        Ok(())
638    }
639
640    /// Disable directory sync
641    pub fn disable_sync(&mut self) -> Result<()> {
642        self.config.sync.enabled = false;
643        self.updated_at = Utc::now();
644        Ok(())
645    }
646
647    /// Get sync configuration
648    pub fn get_sync_config(&self) -> &SyncConfig {
649        &self.config.sync
650    }
651
652    /// Check if sync is enabled
653    pub fn is_sync_enabled(&self) -> bool {
654        self.config.sync.enabled
655    }
656
657    /// Get the target sync directory
658    pub fn get_sync_directory(&self) -> Option<&str> {
659        self.config.sync.target_directory.as_deref()
660    }
661
662    /// Set sync directory
663    pub fn set_sync_directory(&mut self, directory: Option<String>) -> Result<()> {
664        self.config.sync.target_directory = directory;
665        self.updated_at = Utc::now();
666        Ok(())
667    }
668
669    /// Set sync direction
670    pub fn set_sync_direction(&mut self, direction: SyncDirection) -> Result<()> {
671        self.config.sync.sync_direction = direction;
672        self.updated_at = Utc::now();
673        Ok(())
674    }
675
676    /// Get sync direction
677    pub fn get_sync_direction(&self) -> &SyncDirection {
678        &self.config.sync.sync_direction
679    }
680
681    /// Enable/disable real-time monitoring
682    pub fn set_realtime_monitoring(&mut self, enabled: bool) -> Result<()> {
683        self.config.sync.realtime_monitoring = enabled;
684        self.updated_at = Utc::now();
685        Ok(())
686    }
687
688    /// Check if real-time monitoring is enabled
689    pub fn is_realtime_monitoring_enabled(&self) -> bool {
690        self.config.sync.realtime_monitoring && self.config.sync.enabled
691    }
692
693    /// Create filtered copy for directory sync (removes sensitive environments and non-sharable environments)
694    pub fn to_filtered_for_sync(&self) -> Workspace {
695        let mut filtered = self.clone();
696
697        // Remove sensitive environment variables before sync
698        // This implementation filters out common sensitive keys
699        filtered.config.global_environment.variables =
700            self.filter_sensitive_variables(&self.config.global_environment.variables);
701
702        // Filter out non-sharable environments
703        filtered.config.environments = filtered
704            .config
705            .environments
706            .into_iter()
707            .filter(|env| env.sharable)
708            .map(|mut env| {
709                env.variables = self.filter_sensitive_variables(&env.variables);
710                env
711            })
712            .collect();
713
714        filtered
715    }
716
717    /// Filter out sensitive environment variables
718    fn filter_sensitive_variables(
719        &self,
720        variables: &HashMap<String, String>,
721    ) -> HashMap<String, String> {
722        let sensitive_keys = [
723            // Common sensitive keys that should not be synced
724            "password",
725            "secret",
726            "key",
727            "token",
728            "credential",
729            "api_key",
730            "apikey",
731            "api_secret",
732            "db_password",
733            "database_password",
734            "aws_secret_key",
735            "aws_session_token",
736            "private_key",
737            "authorization",
738            "auth_token",
739            "access_token",
740            "refresh_token",
741            "cookie",
742            "session",
743            "csrf",
744            "jwt",
745            "bearer",
746        ];
747
748        variables
749            .iter()
750            .filter(|(key, _)| {
751                let key_lower = key.to_lowercase();
752                !sensitive_keys.iter().any(|sensitive| key_lower.contains(sensitive))
753            })
754            .map(|(k, v)| (k.clone(), v.clone()))
755            .collect()
756    }
757
758    /// Check if this workspace should be included in directory sync
759    pub fn should_sync(&self) -> bool {
760        self.config.sync.enabled && self.config.sync.target_directory.is_some()
761    }
762
763    /// Get the filename for this workspace in directory sync
764    pub fn get_sync_filename(&self) -> String {
765        // Apply filename pattern, default to {name}
766        let pattern = &self.config.sync.filename_pattern;
767
768        // Simple pattern replacement - {name} → workspace name, {id} → workspace id
769        let filename = pattern
770            .replace("{name}", &sanitize_filename(&self.name))
771            .replace("{id}", &self.id);
772
773        if filename.ends_with(".yaml") || filename.ends_with(".yml") {
774            filename
775        } else {
776            format!("{}.yaml", filename)
777        }
778    }
779}
780
781/// Helper function to sanitize filenames for cross-platform compatibility
782fn sanitize_filename(name: &str) -> String {
783    name.chars()
784        .map(|c| match c {
785            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
786            c if c.is_control() => '_',
787            c if c.is_whitespace() => '-',
788            c => c,
789        })
790        .collect::<String>()
791        .to_lowercase()
792}
793
794impl Folder {
795    /// Create a new folder
796    pub fn new(name: String) -> Self {
797        let now = Utc::now();
798        Self {
799            id: Uuid::new_v4().to_string(),
800            name,
801            description: None,
802            parent_id: None,
803            created_at: now,
804            updated_at: now,
805            tags: Vec::new(),
806            inheritance: FolderInheritanceConfig::default(),
807            folders: Vec::new(),
808            requests: Vec::new(),
809        }
810    }
811
812    /// Add a subfolder
813    pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
814        let mut folder = Folder::new(name);
815        folder.parent_id = Some(self.id.clone());
816        let id = folder.id.clone();
817        self.folders.push(folder);
818        self.updated_at = Utc::now();
819        Ok(id)
820    }
821
822    /// Add a request to this folder
823    pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
824        let id = request.id.clone();
825        self.requests.push(request);
826        self.updated_at = Utc::now();
827        Ok(id)
828    }
829
830    /// Find a folder by ID recursively
831    pub fn find_folder(&self, id: &str) -> Option<&Folder> {
832        for folder in &self.folders {
833            if folder.id == id {
834                return Some(folder);
835            }
836            if let Some(found) = folder.find_folder(id) {
837                return Some(found);
838            }
839        }
840        None
841    }
842
843    /// Find a folder by ID recursively (mutable)
844    pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
845        for folder in &mut self.folders {
846            if folder.id == id {
847                return Some(folder);
848            }
849            if let Some(found) = folder.find_folder_mut(id) {
850                return Some(found);
851            }
852        }
853        None
854    }
855
856    /// Find a request by ID recursively
857    pub fn find_request(&self, id: &str) -> Option<&MockRequest> {
858        // Check this folder's requests
859        for request in &self.requests {
860            if request.id == id {
861                return Some(request);
862            }
863        }
864
865        // Check subfolders
866        for folder in &self.folders {
867            if let Some(found) = folder.find_request(id) {
868                return Some(found);
869            }
870        }
871        None
872    }
873
874    /// Get all routes from this folder and subfolders
875    pub fn get_routes(&self, workspace_id: &str) -> Vec<Route> {
876        let mut routes = Vec::new();
877
878        // Add this folder's requests
879        for request in &self.requests {
880            routes.push(
881                Route::new(request.method.clone(), request.path.clone())
882                    .with_priority(request.priority)
883                    .with_metadata("request_id".to_string(), serde_json::json!(request.id))
884                    .with_metadata("folder_id".to_string(), serde_json::json!(self.id))
885                    .with_metadata("workspace_id".to_string(), serde_json::json!(workspace_id)),
886            );
887        }
888
889        // Add routes from subfolders
890        for folder in &self.folders {
891            routes.extend(folder.get_routes(workspace_id));
892        }
893
894        routes
895    }
896}
897
898impl Workspace {
899    /// Find a folder by ID recursively
900    pub fn find_folder(&self, id: &str) -> Option<&Folder> {
901        for folder in &self.folders {
902            if folder.id == id {
903                return Some(folder);
904            }
905            if let Some(found) = folder.find_folder(id) {
906                return Some(found);
907            }
908        }
909        None
910    }
911
912    /// Find a folder by ID recursively (mutable)
913    pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
914        for folder in &mut self.folders {
915            if folder.id == id {
916                return Some(folder);
917            }
918            if let Some(found) = folder.find_folder_mut(id) {
919                return Some(found);
920            }
921        }
922        None
923    }
924
925    /// Add a request to this workspace
926    pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
927        let id = request.id.clone();
928        self.requests.push(request);
929        self.updated_at = Utc::now();
930        Ok(id)
931    }
932
933    /// Get all routes from this workspace
934    pub fn get_routes(&self) -> Vec<Route> {
935        let mut routes = Vec::new();
936
937        // Add workspace-level requests
938        for request in &self.requests {
939            routes.push(
940                Route::new(request.method.clone(), request.path.clone())
941                    .with_priority(request.priority)
942                    .with_metadata("request_id".to_string(), serde_json::json!(request.id))
943                    .with_metadata("workspace_id".to_string(), serde_json::json!(self.id)),
944            );
945        }
946
947        // Add routes from folders
948        for folder in &self.folders {
949            routes.extend(folder.get_routes(&self.id));
950        }
951
952        routes
953    }
954
955    /// Get effective authentication for a request at the given path
956    pub fn get_effective_auth<'a>(&'a self, folder_path: &[&'a Folder]) -> Option<&'a AuthConfig> {
957        // Check folder inheritance (higher priority)
958        for folder in folder_path.iter().rev() {
959            if let Some(auth) = &folder.inheritance.auth {
960                return Some(auth);
961            }
962        }
963
964        // Fall back to workspace auth
965        self.config.auth.as_ref()
966    }
967
968    /// Get merged headers for a request at the given path
969    pub fn get_effective_headers(&self, folder_path: &[&Folder]) -> HashMap<String, String> {
970        let mut effective_headers = HashMap::new();
971
972        // Start with workspace headers (lowest priority)
973        for (key, value) in &self.config.default_headers {
974            effective_headers.insert(key.clone(), value.clone());
975        }
976
977        // Add folder headers (higher priority) in order from parent to child
978        for folder in folder_path {
979            for (key, value) in &folder.inheritance.headers {
980                effective_headers.insert(key.clone(), value.clone());
981            }
982        }
983
984        effective_headers
985    }
986}
987
988impl Folder {
989    /// Get the inheritance path from this folder to root
990    pub fn get_inheritance_path<'a>(&'a self, workspace: &'a Workspace) -> Vec<&'a Folder> {
991        let mut path = Vec::new();
992        let mut current = Some(self);
993
994        while let Some(folder) = current {
995            path.push(folder);
996            current =
997                folder.parent_id.as_ref().and_then(|parent_id| workspace.find_folder(parent_id));
998        }
999
1000        path.reverse(); // Root first
1001        path
1002    }
1003}
1004
1005impl MockRequest {
1006    /// Apply inheritance to this request, returning headers and auth from the hierarchy
1007    pub fn apply_inheritance(
1008        &mut self,
1009        effective_headers: HashMap<String, String>,
1010        effective_auth: Option<&AuthConfig>,
1011    ) {
1012        // Merge headers - request headers override inherited ones
1013        for (key, value) in effective_headers {
1014            self.headers.entry(key).or_insert(value);
1015        }
1016
1017        // For authentication - store it as a tag or custom field for use by the handler
1018        // This will be used by the request processing middleware
1019        if let Some(auth) = effective_auth {
1020            self.auth = Some(auth.clone());
1021        }
1022    }
1023
1024    /// Create inherited request with merged headers and auth
1025    pub fn create_inherited_request(
1026        mut self,
1027        workspace: &Workspace,
1028        folder_path: &[&Folder],
1029    ) -> Self {
1030        let effective_headers = workspace.get_effective_headers(folder_path);
1031        let effective_auth = workspace.get_effective_auth(folder_path);
1032
1033        self.apply_inheritance(effective_headers, effective_auth);
1034        self
1035    }
1036
1037    /// Create a new mock request
1038    pub fn new(method: HttpMethod, path: String, name: String) -> Self {
1039        let now = Utc::now();
1040        Self {
1041            id: Uuid::new_v4().to_string(),
1042            name,
1043            description: None,
1044            method,
1045            path,
1046            headers: HashMap::new(),
1047            query_params: HashMap::new(),
1048            body: None,
1049            response: MockResponse::default(),
1050            response_history: Vec::new(),
1051            created_at: now,
1052            updated_at: now,
1053            tags: Vec::new(),
1054            auth: None,
1055            priority: 0,
1056        }
1057    }
1058
1059    /// Set the response for this request
1060    pub fn with_response(mut self, response: MockResponse) -> Self {
1061        self.response = response;
1062        self
1063    }
1064
1065    /// Add a header
1066    pub fn with_header(mut self, key: String, value: String) -> Self {
1067        self.headers.insert(key, value);
1068        self
1069    }
1070
1071    /// Add a query parameter
1072    pub fn with_query_param(mut self, key: String, value: String) -> Self {
1073        self.query_params.insert(key, value);
1074        self
1075    }
1076
1077    /// Set request body
1078    pub fn with_body(mut self, body: String) -> Self {
1079        self.body = Some(body);
1080        self
1081    }
1082
1083    /// Add a tag
1084    pub fn with_tag(mut self, tag: String) -> Self {
1085        self.tags.push(tag);
1086        self
1087    }
1088
1089    /// Add a response history entry
1090    pub fn add_response_history(&mut self, entry: ResponseHistoryEntry) {
1091        self.response_history.push(entry);
1092        // Keep only last 100 history entries to prevent unbounded growth
1093        if self.response_history.len() > 100 {
1094            self.response_history.remove(0);
1095        }
1096        // Sort by execution time (newest first)
1097        self.response_history.sort_by(|a, b| b.executed_at.cmp(&a.executed_at));
1098    }
1099
1100    /// Get response history (sorted by execution time, newest first)
1101    pub fn get_response_history(&self) -> &[ResponseHistoryEntry] {
1102        &self.response_history
1103    }
1104}
1105
1106impl Default for MockResponse {
1107    fn default() -> Self {
1108        Self {
1109            status_code: 200,
1110            headers: HashMap::new(),
1111            body: Some("{}".to_string()),
1112            content_type: Some("application/json".to_string()),
1113            delay_ms: None,
1114        }
1115    }
1116}
1117
1118impl Environment {
1119    /// Create a new environment
1120    pub fn new(name: String) -> Self {
1121        let now = Utc::now();
1122        Self {
1123            id: Uuid::new_v4().to_string(),
1124            name,
1125            description: None,
1126            color: None,
1127            variables: HashMap::new(),
1128            created_at: now,
1129            updated_at: now,
1130            order: 0,        // Default order will be updated when added to workspace
1131            sharable: false, // Default to not sharable
1132        }
1133    }
1134
1135    /// Create a new global environment
1136    pub fn new_global() -> Self {
1137        let mut env = Self::new("Global".to_string());
1138        env.description =
1139            Some("Global environment variables available in all contexts".to_string());
1140        env
1141    }
1142
1143    /// Add or update a variable
1144    pub fn set_variable(&mut self, key: String, value: String) {
1145        self.variables.insert(key, value);
1146        self.updated_at = Utc::now();
1147    }
1148
1149    /// Remove a variable
1150    pub fn remove_variable(&mut self, key: &str) -> bool {
1151        let removed = self.variables.remove(key).is_some();
1152        if removed {
1153            self.updated_at = Utc::now();
1154        }
1155        removed
1156    }
1157
1158    /// Get a variable value
1159    pub fn get_variable(&self, key: &str) -> Option<&String> {
1160        self.variables.get(key)
1161    }
1162
1163    /// Set the environment color
1164    pub fn set_color(&mut self, color: EnvironmentColor) {
1165        self.color = Some(color);
1166        self.updated_at = Utc::now();
1167    }
1168}
1169
1170impl Default for SyncConfig {
1171    fn default() -> Self {
1172        Self {
1173            enabled: false,
1174            target_directory: None,
1175            directory_structure: SyncDirectoryStructure::Nested,
1176            sync_direction: SyncDirection::Manual,
1177            include_metadata: true,
1178            realtime_monitoring: false,
1179            filename_pattern: "{name}".to_string(),
1180            exclude_pattern: None,
1181            force_overwrite: false,
1182            last_sync: None,
1183        }
1184    }
1185}
1186
1187impl Default for WorkspaceConfig {
1188    fn default() -> Self {
1189        Self {
1190            base_url: None,
1191            default_headers: HashMap::new(),
1192            auth: None,
1193            global_environment: Environment::new_global(),
1194            environments: Vec::new(),
1195            active_environment_id: None,
1196            mock_environments: MockEnvironmentManager::default(),
1197            sync: SyncConfig::default(),
1198            auto_encryption: AutoEncryptionConfig::default(),
1199            reality_level: None,
1200            fidelity_score: None,
1201            ai_mode: None,
1202        }
1203    }
1204}
1205
1206impl WorkspaceRegistry {
1207    /// Create a new workspace registry
1208    pub fn new() -> Self {
1209        Self {
1210            workspaces: HashMap::new(),
1211            active_workspace: None,
1212        }
1213    }
1214
1215    /// Add a workspace
1216    pub fn add_workspace(&mut self, mut workspace: Workspace) -> Result<EntityId> {
1217        let id = workspace.id.clone();
1218
1219        // Set order to the end of the list if not already set
1220        if workspace.order == 0 && !self.workspaces.is_empty() {
1221            workspace.order = self.workspaces.len() as i32;
1222        }
1223
1224        self.workspaces.insert(id.clone(), workspace);
1225        Ok(id)
1226    }
1227
1228    /// Get a workspace by ID
1229    pub fn get_workspace(&self, id: &str) -> Option<&Workspace> {
1230        self.workspaces.get(id)
1231    }
1232
1233    /// Get a workspace by ID (mutable)
1234    pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut Workspace> {
1235        self.workspaces.get_mut(id)
1236    }
1237
1238    /// Remove a workspace
1239    pub fn remove_workspace(&mut self, id: &str) -> Result<()> {
1240        if self.workspaces.remove(id).is_some() {
1241            // Clear active workspace if it was removed
1242            if self.active_workspace.as_deref() == Some(id) {
1243                self.active_workspace = None;
1244            }
1245            Ok(())
1246        } else {
1247            Err(Error::not_found("Workspace", id))
1248        }
1249    }
1250
1251    /// Set the active workspace
1252    pub fn set_active_workspace(&mut self, id: Option<String>) -> Result<()> {
1253        if let Some(ref workspace_id) = id {
1254            if !self.workspaces.contains_key(workspace_id) {
1255                return Err(Error::not_found("Workspace", workspace_id));
1256            }
1257        }
1258        self.active_workspace = id;
1259        Ok(())
1260    }
1261
1262    /// Get the active workspace
1263    pub fn get_active_workspace(&self) -> Option<&Workspace> {
1264        self.active_workspace.as_ref().and_then(|id| self.workspaces.get(id))
1265    }
1266
1267    /// Get the active workspace ID
1268    pub fn get_active_workspace_id(&self) -> Option<&str> {
1269        self.active_workspace.as_deref()
1270    }
1271
1272    /// Get all workspaces
1273    pub fn get_workspaces(&self) -> Vec<&Workspace> {
1274        self.workspaces.values().collect()
1275    }
1276
1277    /// Get all workspaces sorted by order
1278    pub fn get_workspaces_ordered(&self) -> Vec<&Workspace> {
1279        let mut workspaces: Vec<&Workspace> = self.workspaces.values().collect();
1280        workspaces.sort_by_key(|w| w.order);
1281        workspaces
1282    }
1283
1284    /// Update the order of workspaces
1285    pub fn update_workspaces_order(&mut self, workspace_ids: Vec<String>) -> Result<()> {
1286        // Validate that all provided IDs exist
1287        for workspace_id in &workspace_ids {
1288            if !self.workspaces.contains_key(workspace_id) {
1289                return Err(Error::not_found("Workspace", workspace_id));
1290            }
1291        }
1292
1293        // Update order for each workspace
1294        for (index, workspace_id) in workspace_ids.iter().enumerate() {
1295            if let Some(workspace) = self.workspaces.get_mut(workspace_id) {
1296                workspace.order = index as i32;
1297                workspace.updated_at = Utc::now();
1298            }
1299        }
1300
1301        Ok(())
1302    }
1303
1304    /// Get all routes from all workspaces
1305    pub fn get_all_routes(&self) -> Vec<Route> {
1306        let mut all_routes = Vec::new();
1307        for workspace in self.workspaces.values() {
1308            all_routes.extend(workspace.get_routes());
1309        }
1310        all_routes
1311    }
1312
1313    /// Create a route registry from all workspaces
1314    pub fn create_route_registry(&self) -> Result<RouteRegistry> {
1315        let mut registry = RouteRegistry::new();
1316        let routes = self.get_all_routes();
1317
1318        for route in routes {
1319            registry.add_http_route(route)?;
1320        }
1321
1322        Ok(registry)
1323    }
1324}
1325
1326impl Default for WorkspaceRegistry {
1327    fn default() -> Self {
1328        Self::new()
1329    }
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334    use super::*;
1335
1336    use crate::ApiKeyConfig;
1337
1338    #[test]
1339    fn test_workspace_creation() {
1340        let workspace = Workspace::new("Test Workspace".to_string());
1341        assert_eq!(workspace.name, "Test Workspace");
1342        assert!(!workspace.id.is_empty());
1343        assert!(workspace.folders.is_empty());
1344        assert!(workspace.requests.is_empty());
1345    }
1346
1347    #[test]
1348    fn test_folder_creation() {
1349        let folder = Folder::new("Test Folder".to_string());
1350        assert_eq!(folder.name, "Test Folder");
1351        assert!(!folder.id.is_empty());
1352        assert!(folder.folders.is_empty());
1353        assert!(folder.requests.is_empty());
1354    }
1355
1356    #[test]
1357    fn test_request_creation() {
1358        let request =
1359            MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1360        assert_eq!(request.name, "Test Request");
1361        assert_eq!(request.method, HttpMethod::GET);
1362        assert_eq!(request.path, "/test");
1363        assert_eq!(request.response.status_code, 200);
1364    }
1365
1366    #[test]
1367    fn test_workspace_hierarchy() {
1368        let mut workspace = Workspace::new("Test Workspace".to_string());
1369
1370        // Add folder
1371        let folder_id = workspace.add_folder("Test Folder".to_string()).unwrap();
1372        assert_eq!(workspace.folders.len(), 1);
1373
1374        // Add request to workspace
1375        let request =
1376            MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1377        workspace.add_request(request).unwrap();
1378        assert_eq!(workspace.requests.len(), 1);
1379
1380        // Add request to folder
1381        let folder = workspace.find_folder_mut(&folder_id).unwrap();
1382        let folder_request = MockRequest::new(
1383            HttpMethod::POST,
1384            "/folder-test".to_string(),
1385            "Folder Request".to_string(),
1386        );
1387        folder.add_request(folder_request).unwrap();
1388        assert_eq!(folder.requests.len(), 1);
1389    }
1390
1391    #[test]
1392    fn test_workspace_registry() {
1393        let mut registry = WorkspaceRegistry::new();
1394
1395        let workspace = Workspace::new("Test Workspace".to_string());
1396        let workspace_id = registry.add_workspace(workspace).unwrap();
1397
1398        // Set as active
1399        registry.set_active_workspace(Some(workspace_id.clone())).unwrap();
1400        assert!(registry.get_active_workspace().is_some());
1401
1402        // Get workspace
1403        let retrieved = registry.get_workspace(&workspace_id).unwrap();
1404        assert_eq!(retrieved.name, "Test Workspace");
1405
1406        // Remove workspace
1407        registry.remove_workspace(&workspace_id).unwrap();
1408        assert!(registry.get_workspace(&workspace_id).is_none());
1409    }
1410
1411    #[test]
1412    fn test_inheritance_header_priority() {
1413        let mut workspace = Workspace::new("Test Workspace".to_string());
1414        workspace
1415            .config
1416            .default_headers
1417            .insert("X-Common".to_string(), "workspace-value".to_string());
1418        workspace
1419            .config
1420            .default_headers
1421            .insert("X-Workspace-Only".to_string(), "workspace-only-value".to_string());
1422
1423        // Add folder with inheritance
1424        let mut folder = Folder::new("Test Folder".to_string());
1425        folder
1426            .inheritance
1427            .headers
1428            .insert("X-Common".to_string(), "folder-value".to_string());
1429        folder
1430            .inheritance
1431            .headers
1432            .insert("X-Folder-Only".to_string(), "folder-only-value".to_string());
1433
1434        // Test single folder
1435        let folder_path = vec![&folder];
1436        let effective_headers = workspace.get_effective_headers(&folder_path);
1437
1438        assert_eq!(effective_headers.get("X-Common").unwrap(), "folder-value"); // Folder overrides workspace
1439        assert_eq!(effective_headers.get("X-Workspace-Only").unwrap(), "workspace-only-value"); // Workspace value preserved
1440        assert_eq!(effective_headers.get("X-Folder-Only").unwrap(), "folder-only-value");
1441        // Folder value added
1442    }
1443
1444    #[test]
1445    fn test_inheritance_request_headers_override() {
1446        let mut workspace = Workspace::new("Test Workspace".to_string());
1447        workspace
1448            .config
1449            .default_headers
1450            .insert("Authorization".to_string(), "Bearer workspace-token".to_string());
1451
1452        let folder_path = vec![];
1453        let effective_headers = workspace.get_effective_headers(&folder_path);
1454        let mut request =
1455            MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1456
1457        // Request headers should override inherited ones
1458        request
1459            .headers
1460            .insert("Authorization".to_string(), "Bearer request-token".to_string());
1461
1462        // Apply inheritance - request headers should take priority
1463        request.apply_inheritance(effective_headers, None);
1464
1465        assert_eq!(request.headers.get("Authorization").unwrap(), "Bearer request-token");
1466    }
1467
1468    #[test]
1469    fn test_inheritance_nested_folders() {
1470        let mut workspace = Workspace::new("Test Workspace".to_string());
1471        workspace
1472            .config
1473            .default_headers
1474            .insert("X-Level".to_string(), "workspace".to_string());
1475
1476        // Parent folder
1477        let mut parent_folder = Folder::new("Parent Folder".to_string());
1478        parent_folder
1479            .inheritance
1480            .headers
1481            .insert("X-Level".to_string(), "parent".to_string());
1482        parent_folder
1483            .inheritance
1484            .headers
1485            .insert("X-Parent-Only".to_string(), "parent-value".to_string());
1486
1487        // Child folder
1488        let mut child_folder = Folder::new("Child Folder".to_string());
1489        child_folder
1490            .inheritance
1491            .headers
1492            .insert("X-Level".to_string(), "child".to_string());
1493        child_folder
1494            .inheritance
1495            .headers
1496            .insert("X-Child-Only".to_string(), "child-value".to_string());
1497
1498        // Parent-to-child hierarchy
1499        let folder_path = vec![&parent_folder, &child_folder];
1500        let effective_headers = workspace.get_effective_headers(&folder_path);
1501
1502        // Child should override parent which overrides workspace
1503        assert_eq!(effective_headers.get("X-Level").unwrap(), "child");
1504        assert_eq!(effective_headers.get("X-Parent-Only").unwrap(), "parent-value");
1505        assert_eq!(effective_headers.get("X-Child-Only").unwrap(), "child-value");
1506    }
1507
1508    #[test]
1509    fn test_inheritance_auth_from_folder() {
1510        // Create workspace without auth
1511        let workspace = Workspace::new("Test Workspace".to_string());
1512
1513        // Create folder with auth
1514        let mut folder = Folder::new("Test Folder".to_string());
1515        let auth = AuthConfig {
1516            require_auth: true,
1517            api_key: Some(ApiKeyConfig {
1518                header_name: "X-API-Key".to_string(),
1519                query_name: Some("api_key".to_string()),
1520                keys: vec!["folder-key".to_string()],
1521            }),
1522            ..Default::default()
1523        };
1524        folder.inheritance.auth = Some(auth);
1525
1526        let folder_path = vec![&folder];
1527        let effective_auth = workspace.get_effective_auth(&folder_path);
1528
1529        assert!(effective_auth.is_some());
1530        let auth_config = effective_auth.unwrap();
1531        assert!(auth_config.require_auth);
1532        let api_key_config = auth_config.api_key.as_ref().unwrap();
1533        assert_eq!(api_key_config.keys, vec!["folder-key".to_string()]);
1534    }
1535
1536    #[test]
1537    fn test_folder_inheritance_config_default() {
1538        let config = FolderInheritanceConfig::default();
1539        assert!(config.headers.is_empty());
1540        assert!(config.auth.is_none());
1541    }
1542
1543    #[test]
1544    fn test_mock_response_default() {
1545        let response = MockResponse::default();
1546        assert_eq!(response.status_code, 200);
1547        assert!(response.headers.is_empty());
1548    }
1549
1550    #[test]
1551    fn test_mock_response_serialization() {
1552        let mut response = MockResponse {
1553            status_code: 404,
1554            body: Some("Not Found".to_string()),
1555            ..Default::default()
1556        };
1557        response
1558            .headers
1559            .insert("Content-Type".to_string(), "application/json".to_string());
1560
1561        let json = serde_json::to_string(&response).unwrap();
1562        assert!(json.contains("404"));
1563    }
1564
1565    #[test]
1566    fn test_response_history_entry_creation() {
1567        let entry = ResponseHistoryEntry {
1568            id: "exec-123".to_string(),
1569            executed_at: Utc::now(),
1570            request_method: HttpMethod::GET,
1571            request_path: "/api/test".to_string(),
1572            request_headers: HashMap::new(),
1573            request_body: None,
1574            response_status_code: 200,
1575            response_headers: HashMap::new(),
1576            response_body: Some("{}".to_string()),
1577            response_time_ms: 150,
1578            response_size_bytes: 2,
1579            error_message: None,
1580        };
1581
1582        assert_eq!(entry.response_status_code, 200);
1583        assert_eq!(entry.response_time_ms, 150);
1584        assert_eq!(entry.id, "exec-123");
1585    }
1586
1587    #[test]
1588    fn test_environment_color_creation() {
1589        let color = EnvironmentColor {
1590            hex: "#FF8040".to_string(),
1591            name: Some("Orange".to_string()),
1592        };
1593        assert_eq!(color.hex, "#FF8040");
1594        assert_eq!(color.name, Some("Orange".to_string()));
1595    }
1596
1597    #[test]
1598    fn test_environment_color_serialization() {
1599        let color = EnvironmentColor {
1600            hex: "#FF0000".to_string(),
1601            name: None,
1602        };
1603        let json = serde_json::to_string(&color).unwrap();
1604        assert!(json.contains("#FF0000"));
1605    }
1606
1607    #[test]
1608    fn test_sync_config_default() {
1609        // Use the SyncConfig from workspace.rs (not from sync module)
1610        // This is the one used in WorkspaceConfig
1611        let config = SyncConfig::default();
1612        assert!(!config.enabled);
1613        // Just verify it can be created
1614        let _ = config;
1615    }
1616
1617    #[test]
1618    fn test_sync_directory_structure_serialization() {
1619        let structures = vec![
1620            SyncDirectoryStructure::Flat,
1621            SyncDirectoryStructure::Nested,
1622            SyncDirectoryStructure::Grouped,
1623        ];
1624
1625        for structure in structures {
1626            let json = serde_json::to_string(&structure).unwrap();
1627            assert!(!json.is_empty());
1628            // Just verify it can be deserialized
1629            let _deserialized: SyncDirectoryStructure = serde_json::from_str(&json).unwrap();
1630        }
1631    }
1632
1633    #[test]
1634    fn test_sync_direction_serialization() {
1635        let directions = vec![
1636            SyncDirection::Manual,
1637            SyncDirection::WorkspaceToDirectory,
1638            SyncDirection::Bidirectional,
1639        ];
1640
1641        for direction in directions {
1642            let json = serde_json::to_string(&direction).unwrap();
1643            assert!(!json.is_empty());
1644            // Just verify it can be deserialized
1645            let _deserialized: SyncDirection = serde_json::from_str(&json).unwrap();
1646        }
1647    }
1648
1649    #[test]
1650    fn test_workspace_config_default() {
1651        let config = WorkspaceConfig::default();
1652        assert!(config.base_url.is_none());
1653        assert!(config.default_headers.is_empty());
1654        assert!(config.auth.is_none());
1655        assert!(config.environments.is_empty());
1656        assert!(config.active_environment_id.is_none());
1657    }
1658
1659    #[test]
1660    fn test_workspace_registry_new() {
1661        let registry = WorkspaceRegistry::new();
1662        assert!(registry.get_workspaces().is_empty());
1663        assert!(registry.get_active_workspace().is_none());
1664    }
1665
1666    #[test]
1667    fn test_workspace_registry_get_active_workspace_id() {
1668        let mut registry = WorkspaceRegistry::new();
1669        let workspace = Workspace::new("Test".to_string());
1670        let id = registry.add_workspace(workspace).unwrap();
1671        registry.set_active_workspace(Some(id.clone())).unwrap();
1672
1673        assert_eq!(registry.get_active_workspace_id(), Some(id.as_str()));
1674    }
1675
1676    #[test]
1677    fn test_workspace_registry_get_workspaces_ordered() {
1678        let mut registry = WorkspaceRegistry::new();
1679        let mut ws1 = Workspace::new("First".to_string());
1680        ws1.order = 2;
1681        let mut ws2 = Workspace::new("Second".to_string());
1682        ws2.order = 1;
1683
1684        registry.add_workspace(ws1).unwrap();
1685        registry.add_workspace(ws2).unwrap();
1686
1687        let ordered = registry.get_workspaces_ordered();
1688        assert_eq!(ordered.len(), 2);
1689        assert_eq!(ordered[0].name, "Second"); // Lower order first
1690        assert_eq!(ordered[1].name, "First");
1691    }
1692
1693    #[test]
1694    fn test_workspace_registry_update_workspaces_order() {
1695        let mut registry = WorkspaceRegistry::new();
1696        let id1 = registry.add_workspace(Workspace::new("First".to_string())).unwrap();
1697        let id2 = registry.add_workspace(Workspace::new("Second".to_string())).unwrap();
1698
1699        registry.update_workspaces_order(vec![id2.clone(), id1.clone()]).unwrap();
1700
1701        let ordered = registry.get_workspaces_ordered();
1702        assert_eq!(ordered[0].id, id2);
1703        assert_eq!(ordered[1].id, id1);
1704    }
1705
1706    #[test]
1707    fn test_workspace_registry_update_workspaces_order_invalid_id() {
1708        let mut registry = WorkspaceRegistry::new();
1709        let id1 = registry.add_workspace(Workspace::new("First".to_string())).unwrap();
1710
1711        let result = registry.update_workspaces_order(vec![id1, "invalid-id".to_string()]);
1712        assert!(result.is_err());
1713    }
1714
1715    #[test]
1716    fn test_workspace_registry_set_active_workspace_invalid() {
1717        let mut registry = WorkspaceRegistry::new();
1718        let result = registry.set_active_workspace(Some("invalid-id".to_string()));
1719        assert!(result.is_err());
1720    }
1721
1722    #[test]
1723    fn test_workspace_registry_remove_active_workspace() {
1724        let mut registry = WorkspaceRegistry::new();
1725        let id = registry.add_workspace(Workspace::new("Test".to_string())).unwrap();
1726        registry.set_active_workspace(Some(id.clone())).unwrap();
1727        registry.remove_workspace(&id).unwrap();
1728
1729        assert!(registry.get_active_workspace().is_none());
1730    }
1731
1732    #[test]
1733    fn test_workspace_clone() {
1734        let workspace1 = Workspace::new("Test Workspace".to_string());
1735        let workspace2 = workspace1.clone();
1736        assert_eq!(workspace1.name, workspace2.name);
1737        assert_eq!(workspace1.id, workspace2.id);
1738    }
1739
1740    #[test]
1741    fn test_workspace_debug() {
1742        let workspace = Workspace::new("Debug Test".to_string());
1743        let debug_str = format!("{:?}", workspace);
1744        assert!(debug_str.contains("Workspace"));
1745    }
1746
1747    #[test]
1748    fn test_workspace_serialization() {
1749        let workspace = Workspace::new("Serialization Test".to_string());
1750        let json = serde_json::to_string(&workspace).unwrap();
1751        assert!(json.contains("Serialization Test"));
1752    }
1753
1754    #[test]
1755    fn test_folder_clone() {
1756        let folder1 = Folder::new("Test Folder".to_string());
1757        let folder2 = folder1.clone();
1758        assert_eq!(folder1.name, folder2.name);
1759        assert_eq!(folder1.id, folder2.id);
1760    }
1761
1762    #[test]
1763    fn test_folder_debug() {
1764        let folder = Folder::new("Debug Folder".to_string());
1765        let debug_str = format!("{:?}", folder);
1766        assert!(debug_str.contains("Folder"));
1767    }
1768
1769    #[test]
1770    fn test_folder_serialization() {
1771        let folder = Folder::new("Serialization Folder".to_string());
1772        let json = serde_json::to_string(&folder).unwrap();
1773        assert!(json.contains("Serialization Folder"));
1774    }
1775
1776    #[test]
1777    fn test_folder_inheritance_config_clone() {
1778        let mut config1 = FolderInheritanceConfig::default();
1779        config1.headers.insert("X-Test".to_string(), "value".to_string());
1780        let config2 = config1.clone();
1781        assert_eq!(config1.headers, config2.headers);
1782    }
1783
1784    #[test]
1785    fn test_folder_inheritance_config_debug() {
1786        let config = FolderInheritanceConfig::default();
1787        let debug_str = format!("{:?}", config);
1788        assert!(debug_str.contains("FolderInheritanceConfig"));
1789    }
1790
1791    #[test]
1792    fn test_workspace_config_clone() {
1793        let config1 = WorkspaceConfig {
1794            base_url: Some("https://api.example.com".to_string()),
1795            ..Default::default()
1796        };
1797        let config2 = config1.clone();
1798        assert_eq!(config1.base_url, config2.base_url);
1799    }
1800
1801    #[test]
1802    fn test_workspace_config_debug() {
1803        let config = WorkspaceConfig::default();
1804        let debug_str = format!("{:?}", config);
1805        assert!(debug_str.contains("WorkspaceConfig"));
1806    }
1807
1808    #[test]
1809    fn test_workspace_registry_clone() {
1810        let mut registry1 = WorkspaceRegistry::new();
1811        let id = registry1.add_workspace(Workspace::new("Test".to_string())).unwrap();
1812        let registry2 = registry1.clone();
1813        assert!(registry2.get_workspace(&id).is_some());
1814    }
1815
1816    #[test]
1817    fn test_workspace_registry_debug() {
1818        let registry = WorkspaceRegistry::new();
1819        let debug_str = format!("{:?}", registry);
1820        assert!(debug_str.contains("WorkspaceRegistry"));
1821    }
1822}