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::generic(format!("Environment with name '{}' already exists", name)));
411        }
412
413        let mut environment = Environment::new(name);
414        environment.description = description;
415
416        // Set order to the end of the list
417        environment.order = self.config.environments.len() as i32;
418
419        let id = environment.id.clone();
420
421        self.config.environments.push(environment);
422        self.updated_at = Utc::now();
423        Ok(id)
424    }
425
426    /// Get all environments (global + sub-environments)
427    pub fn get_environments(&self) -> Vec<&Environment> {
428        let mut all_envs = vec![&self.config.global_environment];
429        all_envs.extend(self.config.environments.iter());
430        all_envs
431    }
432
433    /// Get environment by ID
434    pub fn get_environment(&self, id: &str) -> Option<&Environment> {
435        if self.config.global_environment.id == id {
436            Some(&self.config.global_environment)
437        } else {
438            self.config.environments.iter().find(|env| env.id == id)
439        }
440    }
441
442    /// Get environment by ID (mutable)
443    pub fn get_environment_mut(&mut self, id: &str) -> Option<&mut Environment> {
444        if self.config.global_environment.id == id {
445            Some(&mut self.config.global_environment)
446        } else {
447            self.config.environments.iter_mut().find(|env| env.id == id)
448        }
449    }
450
451    /// Set active environment
452    pub fn set_active_environment(&mut self, environment_id: Option<String>) -> Result<()> {
453        if let Some(ref id) = environment_id {
454            if self.get_environment(id).is_none() {
455                return Err(Error::generic(format!("Environment with ID '{}' not found", id)));
456            }
457        }
458        self.config.active_environment_id = environment_id;
459        self.updated_at = Utc::now();
460        Ok(())
461    }
462
463    /// Get active environment (returns global if no sub-environment is active)
464    pub fn get_active_environment(&self) -> &Environment {
465        if let Some(ref active_id) = self.config.active_environment_id {
466            self.get_environment(active_id).unwrap_or(&self.config.global_environment)
467        } else {
468            &self.config.global_environment
469        }
470    }
471
472    /// Get active environment ID
473    pub fn get_active_environment_id(&self) -> Option<&str> {
474        self.config.active_environment_id.as_deref()
475    }
476
477    /// Get variable value from current active environment
478    pub fn get_variable(&self, key: &str) -> Option<&String> {
479        // First check active environment, then global environment
480        let active_env = self.get_active_environment();
481        active_env.get_variable(key).or_else(|| {
482            if active_env.id != self.config.global_environment.id {
483                self.config.global_environment.get_variable(key)
484            } else {
485                None
486            }
487        })
488    }
489
490    /// Get all variables from current active environment context
491    pub fn get_all_variables(&self) -> HashMap<String, String> {
492        let mut variables = HashMap::new();
493
494        // Start with global environment variables
495        variables.extend(self.config.global_environment.variables.clone());
496
497        // Override with active environment variables if different from global
498        let active_env = self.get_active_environment();
499        if active_env.id != self.config.global_environment.id {
500            variables.extend(active_env.variables.clone());
501        }
502
503        variables
504    }
505
506    /// Delete an environment
507    pub fn delete_environment(&mut self, id: &str) -> Result<()> {
508        if id == self.config.global_environment.id {
509            return Err(Error::generic("Cannot delete global environment".to_string()));
510        }
511
512        let position = self.config.environments.iter().position(|env| env.id == id);
513        if let Some(pos) = position {
514            self.config.environments.remove(pos);
515
516            // Clear active environment if it was deleted
517            if self.config.active_environment_id.as_deref() == Some(id) {
518                self.config.active_environment_id = None;
519            }
520
521            self.updated_at = Utc::now();
522            Ok(())
523        } else {
524            Err(Error::generic(format!("Environment with ID '{}' not found", id)))
525        }
526    }
527
528    /// Update the order of environments
529    pub fn update_environments_order(&mut self, environment_ids: Vec<String>) -> Result<()> {
530        // Validate that all provided IDs exist
531        for env_id in &environment_ids {
532            if !self.config.environments.iter().any(|env| env.id == *env_id) {
533                return Err(Error::generic(format!("Environment with ID '{}' not found", env_id)));
534            }
535        }
536
537        // Update order for each environment
538        for (index, env_id) in environment_ids.iter().enumerate() {
539            if let Some(env) = self.config.environments.iter_mut().find(|env| env.id == *env_id) {
540                env.order = index as i32;
541                env.updated_at = Utc::now();
542            }
543        }
544
545        self.updated_at = Utc::now();
546        Ok(())
547    }
548
549    /// Get environments sorted by order
550    pub fn get_environments_ordered(&self) -> Vec<&Environment> {
551        let mut all_envs = vec![&self.config.global_environment];
552        all_envs.extend(self.config.environments.iter());
553        all_envs.sort_by_key(|env| env.order);
554        all_envs
555    }
556
557    /// Get the mock environment manager
558    pub fn get_mock_environments(&self) -> &MockEnvironmentManager {
559        &self.config.mock_environments
560    }
561
562    /// Get mutable access to the mock environment manager
563    pub fn get_mock_environments_mut(&mut self) -> &mut MockEnvironmentManager {
564        &mut self.config.mock_environments
565    }
566
567    /// Get a specific mock environment by name
568    pub fn get_mock_environment(&self, name: MockEnvironmentName) -> Option<&MockEnvironment> {
569        self.config.mock_environments.get_environment(name)
570    }
571
572    /// Get the active mock environment
573    pub fn get_active_mock_environment(&self) -> Option<&MockEnvironment> {
574        self.config.mock_environments.get_active_environment()
575    }
576
577    /// Set the active mock environment
578    pub fn set_active_mock_environment(&mut self, name: MockEnvironmentName) -> Result<()> {
579        self.config.mock_environments.set_active_environment(name)?;
580        self.updated_at = Utc::now();
581        Ok(())
582    }
583
584    /// List all mock environments
585    pub fn list_mock_environments(&self) -> Vec<&MockEnvironment> {
586        self.config.mock_environments.list_environments()
587    }
588
589    /// Add or update a mock environment configuration
590    pub fn set_mock_environment_config(
591        &mut self,
592        name: MockEnvironmentName,
593        reality_config: Option<crate::reality::RealityConfig>,
594        chaos_config: Option<crate::chaos_utilities::ChaosConfig>,
595        drift_budget_config: Option<crate::contract_drift::DriftBudgetConfig>,
596    ) -> Result<()> {
597        // Get or create the environment
598        let env = if let Some(existing) = self.config.mock_environments.get_environment(name) {
599            MockEnvironment::with_configs(
600                existing.workspace_id.clone(),
601                name,
602                reality_config,
603                chaos_config,
604                drift_budget_config,
605            )
606        } else {
607            MockEnvironment::with_configs(
608                self.id.clone(),
609                name,
610                reality_config,
611                chaos_config,
612                drift_budget_config,
613            )
614        };
615
616        self.config.mock_environments.add_environment(env);
617        self.updated_at = Utc::now();
618        Ok(())
619    }
620
621    /// Configure directory sync for this workspace
622    pub fn configure_sync(&mut self, config: SyncConfig) -> Result<()> {
623        self.config.sync = config;
624        self.updated_at = Utc::now();
625        Ok(())
626    }
627
628    /// Enable directory sync with default settings
629    pub fn enable_sync(&mut self, target_directory: String) -> Result<()> {
630        self.config.sync.enabled = true;
631        self.config.sync.target_directory = Some(target_directory);
632        self.config.sync.realtime_monitoring = true; // Enable realtime monitoring by default
633        self.updated_at = Utc::now();
634        Ok(())
635    }
636
637    /// Disable directory sync
638    pub fn disable_sync(&mut self) -> Result<()> {
639        self.config.sync.enabled = false;
640        self.updated_at = Utc::now();
641        Ok(())
642    }
643
644    /// Get sync configuration
645    pub fn get_sync_config(&self) -> &SyncConfig {
646        &self.config.sync
647    }
648
649    /// Check if sync is enabled
650    pub fn is_sync_enabled(&self) -> bool {
651        self.config.sync.enabled
652    }
653
654    /// Get the target sync directory
655    pub fn get_sync_directory(&self) -> Option<&str> {
656        self.config.sync.target_directory.as_deref()
657    }
658
659    /// Set sync directory
660    pub fn set_sync_directory(&mut self, directory: Option<String>) -> Result<()> {
661        self.config.sync.target_directory = directory;
662        self.updated_at = Utc::now();
663        Ok(())
664    }
665
666    /// Set sync direction
667    pub fn set_sync_direction(&mut self, direction: SyncDirection) -> Result<()> {
668        self.config.sync.sync_direction = direction;
669        self.updated_at = Utc::now();
670        Ok(())
671    }
672
673    /// Get sync direction
674    pub fn get_sync_direction(&self) -> &SyncDirection {
675        &self.config.sync.sync_direction
676    }
677
678    /// Enable/disable real-time monitoring
679    pub fn set_realtime_monitoring(&mut self, enabled: bool) -> Result<()> {
680        self.config.sync.realtime_monitoring = enabled;
681        self.updated_at = Utc::now();
682        Ok(())
683    }
684
685    /// Check if real-time monitoring is enabled
686    pub fn is_realtime_monitoring_enabled(&self) -> bool {
687        self.config.sync.realtime_monitoring && self.config.sync.enabled
688    }
689
690    /// Create filtered copy for directory sync (removes sensitive environments and non-sharable environments)
691    pub fn to_filtered_for_sync(&self) -> Workspace {
692        let mut filtered = self.clone();
693
694        // Remove sensitive environment variables before sync
695        // This implementation filters out common sensitive keys
696        filtered.config.global_environment.variables =
697            self.filter_sensitive_variables(&self.config.global_environment.variables);
698
699        // Filter out non-sharable environments
700        filtered.config.environments = filtered
701            .config
702            .environments
703            .into_iter()
704            .filter(|env| env.sharable)
705            .map(|mut env| {
706                env.variables = self.filter_sensitive_variables(&env.variables);
707                env
708            })
709            .collect();
710
711        filtered
712    }
713
714    /// Filter out sensitive environment variables
715    fn filter_sensitive_variables(
716        &self,
717        variables: &HashMap<String, String>,
718    ) -> HashMap<String, String> {
719        let sensitive_keys = [
720            // Common sensitive keys that should not be synced
721            "password",
722            "secret",
723            "key",
724            "token",
725            "credential",
726            "api_key",
727            "apikey",
728            "api_secret",
729            "db_password",
730            "database_password",
731            "aws_secret_key",
732            "aws_session_token",
733            "private_key",
734            "authorization",
735            "auth_token",
736            "access_token",
737            "refresh_token",
738            "cookie",
739            "session",
740            "csrf",
741            "jwt",
742            "bearer",
743        ];
744
745        variables
746            .iter()
747            .filter(|(key, _)| {
748                let key_lower = key.to_lowercase();
749                !sensitive_keys.iter().any(|sensitive| key_lower.contains(sensitive))
750            })
751            .map(|(k, v)| (k.clone(), v.clone()))
752            .collect()
753    }
754
755    /// Check if this workspace should be included in directory sync
756    pub fn should_sync(&self) -> bool {
757        self.config.sync.enabled && self.config.sync.target_directory.is_some()
758    }
759
760    /// Get the filename for this workspace in directory sync
761    pub fn get_sync_filename(&self) -> String {
762        // Apply filename pattern, default to {name}
763        let pattern = &self.config.sync.filename_pattern;
764
765        // Simple pattern replacement - {name} → workspace name, {id} → workspace id
766        let filename = pattern
767            .replace("{name}", &sanitize_filename(&self.name))
768            .replace("{id}", &self.id);
769
770        if filename.ends_with(".yaml") || filename.ends_with(".yml") {
771            filename
772        } else {
773            format!("{}.yaml", filename)
774        }
775    }
776}
777
778/// Helper function to sanitize filenames for cross-platform compatibility
779fn sanitize_filename(name: &str) -> String {
780    name.chars()
781        .map(|c| match c {
782            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
783            c if c.is_control() => '_',
784            c if c.is_whitespace() => '-',
785            c => c,
786        })
787        .collect::<String>()
788        .to_lowercase()
789}
790
791impl Folder {
792    /// Create a new folder
793    pub fn new(name: String) -> Self {
794        let now = Utc::now();
795        Self {
796            id: Uuid::new_v4().to_string(),
797            name,
798            description: None,
799            parent_id: None,
800            created_at: now,
801            updated_at: now,
802            tags: Vec::new(),
803            inheritance: FolderInheritanceConfig::default(),
804            folders: Vec::new(),
805            requests: Vec::new(),
806        }
807    }
808
809    /// Add a subfolder
810    pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
811        let mut folder = Folder::new(name);
812        folder.parent_id = Some(self.id.clone());
813        let id = folder.id.clone();
814        self.folders.push(folder);
815        self.updated_at = Utc::now();
816        Ok(id)
817    }
818
819    /// Add a request to this folder
820    pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
821        let id = request.id.clone();
822        self.requests.push(request);
823        self.updated_at = Utc::now();
824        Ok(id)
825    }
826
827    /// Find a folder by ID recursively
828    pub fn find_folder(&self, id: &str) -> Option<&Folder> {
829        for folder in &self.folders {
830            if folder.id == id {
831                return Some(folder);
832            }
833            if let Some(found) = folder.find_folder(id) {
834                return Some(found);
835            }
836        }
837        None
838    }
839
840    /// Find a folder by ID recursively (mutable)
841    pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
842        for folder in &mut self.folders {
843            if folder.id == id {
844                return Some(folder);
845            }
846            if let Some(found) = folder.find_folder_mut(id) {
847                return Some(found);
848            }
849        }
850        None
851    }
852
853    /// Find a request by ID recursively
854    pub fn find_request(&self, id: &str) -> Option<&MockRequest> {
855        // Check this folder's requests
856        for request in &self.requests {
857            if request.id == id {
858                return Some(request);
859            }
860        }
861
862        // Check subfolders
863        for folder in &self.folders {
864            if let Some(found) = folder.find_request(id) {
865                return Some(found);
866            }
867        }
868        None
869    }
870
871    /// Get all routes from this folder and subfolders
872    pub fn get_routes(&self, workspace_id: &str) -> Vec<Route> {
873        let mut routes = Vec::new();
874
875        // Add this folder's requests
876        for request in &self.requests {
877            routes.push(
878                Route::new(request.method.clone(), request.path.clone())
879                    .with_priority(request.priority)
880                    .with_metadata("request_id".to_string(), serde_json::json!(request.id))
881                    .with_metadata("folder_id".to_string(), serde_json::json!(self.id))
882                    .with_metadata("workspace_id".to_string(), serde_json::json!(workspace_id)),
883            );
884        }
885
886        // Add routes from subfolders
887        for folder in &self.folders {
888            routes.extend(folder.get_routes(workspace_id));
889        }
890
891        routes
892    }
893}
894
895impl Workspace {
896    /// Find a folder by ID recursively
897    pub fn find_folder(&self, id: &str) -> Option<&Folder> {
898        for folder in &self.folders {
899            if folder.id == id {
900                return Some(folder);
901            }
902            if let Some(found) = folder.find_folder(id) {
903                return Some(found);
904            }
905        }
906        None
907    }
908
909    /// Find a folder by ID recursively (mutable)
910    pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
911        for folder in &mut self.folders {
912            if folder.id == id {
913                return Some(folder);
914            }
915            if let Some(found) = folder.find_folder_mut(id) {
916                return Some(found);
917            }
918        }
919        None
920    }
921
922    /// Add a request to this workspace
923    pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
924        let id = request.id.clone();
925        self.requests.push(request);
926        self.updated_at = Utc::now();
927        Ok(id)
928    }
929
930    /// Get all routes from this workspace
931    pub fn get_routes(&self) -> Vec<Route> {
932        let mut routes = Vec::new();
933
934        // Add workspace-level requests
935        for request in &self.requests {
936            routes.push(
937                Route::new(request.method.clone(), request.path.clone())
938                    .with_priority(request.priority)
939                    .with_metadata("request_id".to_string(), serde_json::json!(request.id))
940                    .with_metadata("workspace_id".to_string(), serde_json::json!(self.id)),
941            );
942        }
943
944        // Add routes from folders
945        for folder in &self.folders {
946            routes.extend(folder.get_routes(&self.id));
947        }
948
949        routes
950    }
951
952    /// Get effective authentication for a request at the given path
953    pub fn get_effective_auth<'a>(&'a self, folder_path: &[&'a Folder]) -> Option<&'a AuthConfig> {
954        // Check folder inheritance (higher priority)
955        for folder in folder_path.iter().rev() {
956            if let Some(auth) = &folder.inheritance.auth {
957                return Some(auth);
958            }
959        }
960
961        // Fall back to workspace auth
962        self.config.auth.as_ref()
963    }
964
965    /// Get merged headers for a request at the given path
966    pub fn get_effective_headers(&self, folder_path: &[&Folder]) -> HashMap<String, String> {
967        let mut effective_headers = HashMap::new();
968
969        // Start with workspace headers (lowest priority)
970        for (key, value) in &self.config.default_headers {
971            effective_headers.insert(key.clone(), value.clone());
972        }
973
974        // Add folder headers (higher priority) in order from parent to child
975        for folder in folder_path {
976            for (key, value) in &folder.inheritance.headers {
977                effective_headers.insert(key.clone(), value.clone());
978            }
979        }
980
981        effective_headers
982    }
983}
984
985impl Folder {
986    /// Get the inheritance path from this folder to root
987    pub fn get_inheritance_path<'a>(&'a self, workspace: &'a Workspace) -> Vec<&'a Folder> {
988        let mut path = Vec::new();
989        let mut current = Some(self);
990
991        while let Some(folder) = current {
992            path.push(folder);
993            current =
994                folder.parent_id.as_ref().and_then(|parent_id| workspace.find_folder(parent_id));
995        }
996
997        path.reverse(); // Root first
998        path
999    }
1000}
1001
1002impl MockRequest {
1003    /// Apply inheritance to this request, returning headers and auth from the hierarchy
1004    pub fn apply_inheritance(
1005        &mut self,
1006        effective_headers: HashMap<String, String>,
1007        effective_auth: Option<&AuthConfig>,
1008    ) {
1009        // Merge headers - request headers override inherited ones
1010        for (key, value) in effective_headers {
1011            self.headers.entry(key).or_insert(value);
1012        }
1013
1014        // For authentication - store it as a tag or custom field for use by the handler
1015        // This will be used by the request processing middleware
1016        if let Some(auth) = effective_auth {
1017            self.auth = Some(auth.clone());
1018        }
1019    }
1020
1021    /// Create inherited request with merged headers and auth
1022    pub fn create_inherited_request(
1023        mut self,
1024        workspace: &Workspace,
1025        folder_path: &[&Folder],
1026    ) -> Self {
1027        let effective_headers = workspace.get_effective_headers(folder_path);
1028        let effective_auth = workspace.get_effective_auth(folder_path);
1029
1030        self.apply_inheritance(effective_headers, effective_auth);
1031        self
1032    }
1033
1034    /// Create a new mock request
1035    pub fn new(method: HttpMethod, path: String, name: String) -> Self {
1036        let now = Utc::now();
1037        Self {
1038            id: Uuid::new_v4().to_string(),
1039            name,
1040            description: None,
1041            method,
1042            path,
1043            headers: HashMap::new(),
1044            query_params: HashMap::new(),
1045            body: None,
1046            response: MockResponse::default(),
1047            response_history: Vec::new(),
1048            created_at: now,
1049            updated_at: now,
1050            tags: Vec::new(),
1051            auth: None,
1052            priority: 0,
1053        }
1054    }
1055
1056    /// Set the response for this request
1057    pub fn with_response(mut self, response: MockResponse) -> Self {
1058        self.response = response;
1059        self
1060    }
1061
1062    /// Add a header
1063    pub fn with_header(mut self, key: String, value: String) -> Self {
1064        self.headers.insert(key, value);
1065        self
1066    }
1067
1068    /// Add a query parameter
1069    pub fn with_query_param(mut self, key: String, value: String) -> Self {
1070        self.query_params.insert(key, value);
1071        self
1072    }
1073
1074    /// Set request body
1075    pub fn with_body(mut self, body: String) -> Self {
1076        self.body = Some(body);
1077        self
1078    }
1079
1080    /// Add a tag
1081    pub fn with_tag(mut self, tag: String) -> Self {
1082        self.tags.push(tag);
1083        self
1084    }
1085
1086    /// Add a response history entry
1087    pub fn add_response_history(&mut self, entry: ResponseHistoryEntry) {
1088        self.response_history.push(entry);
1089        // Keep only last 100 history entries to prevent unbounded growth
1090        if self.response_history.len() > 100 {
1091            self.response_history.remove(0);
1092        }
1093        // Sort by execution time (newest first)
1094        self.response_history.sort_by(|a, b| b.executed_at.cmp(&a.executed_at));
1095    }
1096
1097    /// Get response history (sorted by execution time, newest first)
1098    pub fn get_response_history(&self) -> &[ResponseHistoryEntry] {
1099        &self.response_history
1100    }
1101}
1102
1103impl Default for MockResponse {
1104    fn default() -> Self {
1105        Self {
1106            status_code: 200,
1107            headers: HashMap::new(),
1108            body: Some("{}".to_string()),
1109            content_type: Some("application/json".to_string()),
1110            delay_ms: None,
1111        }
1112    }
1113}
1114
1115impl Environment {
1116    /// Create a new environment
1117    pub fn new(name: String) -> Self {
1118        let now = Utc::now();
1119        Self {
1120            id: Uuid::new_v4().to_string(),
1121            name,
1122            description: None,
1123            color: None,
1124            variables: HashMap::new(),
1125            created_at: now,
1126            updated_at: now,
1127            order: 0,        // Default order will be updated when added to workspace
1128            sharable: false, // Default to not sharable
1129        }
1130    }
1131
1132    /// Create a new global environment
1133    pub fn new_global() -> Self {
1134        let mut env = Self::new("Global".to_string());
1135        env.description =
1136            Some("Global environment variables available in all contexts".to_string());
1137        env
1138    }
1139
1140    /// Add or update a variable
1141    pub fn set_variable(&mut self, key: String, value: String) {
1142        self.variables.insert(key, value);
1143        self.updated_at = Utc::now();
1144    }
1145
1146    /// Remove a variable
1147    pub fn remove_variable(&mut self, key: &str) -> bool {
1148        let removed = self.variables.remove(key).is_some();
1149        if removed {
1150            self.updated_at = Utc::now();
1151        }
1152        removed
1153    }
1154
1155    /// Get a variable value
1156    pub fn get_variable(&self, key: &str) -> Option<&String> {
1157        self.variables.get(key)
1158    }
1159
1160    /// Set the environment color
1161    pub fn set_color(&mut self, color: EnvironmentColor) {
1162        self.color = Some(color);
1163        self.updated_at = Utc::now();
1164    }
1165}
1166
1167impl Default for SyncConfig {
1168    fn default() -> Self {
1169        Self {
1170            enabled: false,
1171            target_directory: None,
1172            directory_structure: SyncDirectoryStructure::Nested,
1173            sync_direction: SyncDirection::Manual,
1174            include_metadata: true,
1175            realtime_monitoring: false,
1176            filename_pattern: "{name}".to_string(),
1177            exclude_pattern: None,
1178            force_overwrite: false,
1179            last_sync: None,
1180        }
1181    }
1182}
1183
1184impl Default for WorkspaceConfig {
1185    fn default() -> Self {
1186        Self {
1187            base_url: None,
1188            default_headers: HashMap::new(),
1189            auth: None,
1190            global_environment: Environment::new_global(),
1191            environments: Vec::new(),
1192            active_environment_id: None,
1193            mock_environments: MockEnvironmentManager::default(),
1194            sync: SyncConfig::default(),
1195            auto_encryption: AutoEncryptionConfig::default(),
1196            reality_level: None,
1197            fidelity_score: None,
1198            ai_mode: None,
1199        }
1200    }
1201}
1202
1203impl WorkspaceRegistry {
1204    /// Create a new workspace registry
1205    pub fn new() -> Self {
1206        Self {
1207            workspaces: HashMap::new(),
1208            active_workspace: None,
1209        }
1210    }
1211
1212    /// Add a workspace
1213    pub fn add_workspace(&mut self, mut workspace: Workspace) -> Result<EntityId> {
1214        let id = workspace.id.clone();
1215
1216        // Set order to the end of the list if not already set
1217        if workspace.order == 0 && !self.workspaces.is_empty() {
1218            workspace.order = self.workspaces.len() as i32;
1219        }
1220
1221        self.workspaces.insert(id.clone(), workspace);
1222        Ok(id)
1223    }
1224
1225    /// Get a workspace by ID
1226    pub fn get_workspace(&self, id: &str) -> Option<&Workspace> {
1227        self.workspaces.get(id)
1228    }
1229
1230    /// Get a workspace by ID (mutable)
1231    pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut Workspace> {
1232        self.workspaces.get_mut(id)
1233    }
1234
1235    /// Remove a workspace
1236    pub fn remove_workspace(&mut self, id: &str) -> Result<()> {
1237        if self.workspaces.remove(id).is_some() {
1238            // Clear active workspace if it was removed
1239            if self.active_workspace.as_deref() == Some(id) {
1240                self.active_workspace = None;
1241            }
1242            Ok(())
1243        } else {
1244            Err(Error::generic(format!("Workspace with ID '{}' not found", id)))
1245        }
1246    }
1247
1248    /// Set the active workspace
1249    pub fn set_active_workspace(&mut self, id: Option<String>) -> Result<()> {
1250        if let Some(ref workspace_id) = id {
1251            if !self.workspaces.contains_key(workspace_id) {
1252                return Err(Error::generic(format!(
1253                    "Workspace with ID '{}' not found",
1254                    workspace_id
1255                )));
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::generic(format!(
1290                    "Workspace with ID '{}' not found",
1291                    workspace_id
1292                )));
1293            }
1294        }
1295
1296        // Update order for each workspace
1297        for (index, workspace_id) in workspace_ids.iter().enumerate() {
1298            if let Some(workspace) = self.workspaces.get_mut(workspace_id) {
1299                workspace.order = index as i32;
1300                workspace.updated_at = Utc::now();
1301            }
1302        }
1303
1304        Ok(())
1305    }
1306
1307    /// Get all routes from all workspaces
1308    pub fn get_all_routes(&self) -> Vec<Route> {
1309        let mut all_routes = Vec::new();
1310        for workspace in self.workspaces.values() {
1311            all_routes.extend(workspace.get_routes());
1312        }
1313        all_routes
1314    }
1315
1316    /// Create a route registry from all workspaces
1317    pub fn create_route_registry(&self) -> Result<RouteRegistry> {
1318        let mut registry = RouteRegistry::new();
1319        let routes = self.get_all_routes();
1320
1321        for route in routes {
1322            registry.add_http_route(route)?;
1323        }
1324
1325        Ok(registry)
1326    }
1327}
1328
1329impl Default for WorkspaceRegistry {
1330    fn default() -> Self {
1331        Self::new()
1332    }
1333}
1334
1335#[cfg(test)]
1336mod tests {
1337    use super::*;
1338
1339    use crate::ApiKeyConfig;
1340
1341    #[test]
1342    fn test_workspace_creation() {
1343        let workspace = Workspace::new("Test Workspace".to_string());
1344        assert_eq!(workspace.name, "Test Workspace");
1345        assert!(!workspace.id.is_empty());
1346        assert!(workspace.folders.is_empty());
1347        assert!(workspace.requests.is_empty());
1348    }
1349
1350    #[test]
1351    fn test_folder_creation() {
1352        let folder = Folder::new("Test Folder".to_string());
1353        assert_eq!(folder.name, "Test Folder");
1354        assert!(!folder.id.is_empty());
1355        assert!(folder.folders.is_empty());
1356        assert!(folder.requests.is_empty());
1357    }
1358
1359    #[test]
1360    fn test_request_creation() {
1361        let request =
1362            MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1363        assert_eq!(request.name, "Test Request");
1364        assert_eq!(request.method, HttpMethod::GET);
1365        assert_eq!(request.path, "/test");
1366        assert_eq!(request.response.status_code, 200);
1367    }
1368
1369    #[test]
1370    fn test_workspace_hierarchy() {
1371        let mut workspace = Workspace::new("Test Workspace".to_string());
1372
1373        // Add folder
1374        let folder_id = workspace.add_folder("Test Folder".to_string()).unwrap();
1375        assert_eq!(workspace.folders.len(), 1);
1376
1377        // Add request to workspace
1378        let request =
1379            MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1380        workspace.add_request(request).unwrap();
1381        assert_eq!(workspace.requests.len(), 1);
1382
1383        // Add request to folder
1384        let folder = workspace.find_folder_mut(&folder_id).unwrap();
1385        let folder_request = MockRequest::new(
1386            HttpMethod::POST,
1387            "/folder-test".to_string(),
1388            "Folder Request".to_string(),
1389        );
1390        folder.add_request(folder_request).unwrap();
1391        assert_eq!(folder.requests.len(), 1);
1392    }
1393
1394    #[test]
1395    fn test_workspace_registry() {
1396        let mut registry = WorkspaceRegistry::new();
1397
1398        let workspace = Workspace::new("Test Workspace".to_string());
1399        let workspace_id = registry.add_workspace(workspace).unwrap();
1400
1401        // Set as active
1402        registry.set_active_workspace(Some(workspace_id.clone())).unwrap();
1403        assert!(registry.get_active_workspace().is_some());
1404
1405        // Get workspace
1406        let retrieved = registry.get_workspace(&workspace_id).unwrap();
1407        assert_eq!(retrieved.name, "Test Workspace");
1408
1409        // Remove workspace
1410        registry.remove_workspace(&workspace_id).unwrap();
1411        assert!(registry.get_workspace(&workspace_id).is_none());
1412    }
1413
1414    #[test]
1415    fn test_inheritance_header_priority() {
1416        let mut workspace = Workspace::new("Test Workspace".to_string());
1417        workspace
1418            .config
1419            .default_headers
1420            .insert("X-Common".to_string(), "workspace-value".to_string());
1421        workspace
1422            .config
1423            .default_headers
1424            .insert("X-Workspace-Only".to_string(), "workspace-only-value".to_string());
1425
1426        // Add folder with inheritance
1427        let mut folder = Folder::new("Test Folder".to_string());
1428        folder
1429            .inheritance
1430            .headers
1431            .insert("X-Common".to_string(), "folder-value".to_string());
1432        folder
1433            .inheritance
1434            .headers
1435            .insert("X-Folder-Only".to_string(), "folder-only-value".to_string());
1436
1437        // Test single folder
1438        let folder_path = vec![&folder];
1439        let effective_headers = workspace.get_effective_headers(&folder_path);
1440
1441        assert_eq!(effective_headers.get("X-Common").unwrap(), "folder-value"); // Folder overrides workspace
1442        assert_eq!(effective_headers.get("X-Workspace-Only").unwrap(), "workspace-only-value"); // Workspace value preserved
1443        assert_eq!(effective_headers.get("X-Folder-Only").unwrap(), "folder-only-value");
1444        // Folder value added
1445    }
1446
1447    #[test]
1448    fn test_inheritance_request_headers_override() {
1449        let mut workspace = Workspace::new("Test Workspace".to_string());
1450        workspace
1451            .config
1452            .default_headers
1453            .insert("Authorization".to_string(), "Bearer workspace-token".to_string());
1454
1455        let folder_path = vec![];
1456        let effective_headers = workspace.get_effective_headers(&folder_path);
1457        let mut request = MockRequest::new(
1458            crate::routing::HttpMethod::GET,
1459            "/test".to_string(),
1460            "Test Request".to_string(),
1461        );
1462
1463        // Request headers should override inherited ones
1464        request
1465            .headers
1466            .insert("Authorization".to_string(), "Bearer request-token".to_string());
1467
1468        // Apply inheritance - request headers should take priority
1469        request.apply_inheritance(effective_headers, None);
1470
1471        assert_eq!(request.headers.get("Authorization").unwrap(), "Bearer request-token");
1472    }
1473
1474    #[test]
1475    fn test_inheritance_nested_folders() {
1476        let mut workspace = Workspace::new("Test Workspace".to_string());
1477        workspace
1478            .config
1479            .default_headers
1480            .insert("X-Level".to_string(), "workspace".to_string());
1481
1482        // Parent folder
1483        let mut parent_folder = Folder::new("Parent Folder".to_string());
1484        parent_folder
1485            .inheritance
1486            .headers
1487            .insert("X-Level".to_string(), "parent".to_string());
1488        parent_folder
1489            .inheritance
1490            .headers
1491            .insert("X-Parent-Only".to_string(), "parent-value".to_string());
1492
1493        // Child folder
1494        let mut child_folder = Folder::new("Child Folder".to_string());
1495        child_folder
1496            .inheritance
1497            .headers
1498            .insert("X-Level".to_string(), "child".to_string());
1499        child_folder
1500            .inheritance
1501            .headers
1502            .insert("X-Child-Only".to_string(), "child-value".to_string());
1503
1504        // Parent-to-child hierarchy
1505        let folder_path = vec![&parent_folder, &child_folder];
1506        let effective_headers = workspace.get_effective_headers(&folder_path);
1507
1508        // Child should override parent which overrides workspace
1509        assert_eq!(effective_headers.get("X-Level").unwrap(), "child");
1510        assert_eq!(effective_headers.get("X-Parent-Only").unwrap(), "parent-value");
1511        assert_eq!(effective_headers.get("X-Child-Only").unwrap(), "child-value");
1512    }
1513
1514    #[test]
1515    fn test_inheritance_auth_from_folder() {
1516        // Create workspace without auth
1517        let workspace = Workspace::new("Test Workspace".to_string());
1518
1519        // Create folder with auth
1520        let mut folder = Folder::new("Test Folder".to_string());
1521        let auth = AuthConfig {
1522            require_auth: true,
1523            api_key: Some(ApiKeyConfig {
1524                header_name: "X-API-Key".to_string(),
1525                query_name: Some("api_key".to_string()),
1526                keys: vec!["folder-key".to_string()],
1527            }),
1528            ..Default::default()
1529        };
1530        folder.inheritance.auth = Some(auth);
1531
1532        let folder_path = vec![&folder];
1533        let effective_auth = workspace.get_effective_auth(&folder_path);
1534
1535        assert!(effective_auth.is_some());
1536        let auth_config = effective_auth.unwrap();
1537        assert!(auth_config.require_auth);
1538        let api_key_config = auth_config.api_key.as_ref().unwrap();
1539        assert_eq!(api_key_config.keys, vec!["folder-key".to_string()]);
1540    }
1541}