mockforge_tunnel/
manager.rs

1//! Tunnel manager for creating and managing tunnels
2
3use crate::{config::TunnelConfig, provider::*, Result, TunnelError, TunnelStatus};
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7/// Tunnel manager for creating and managing tunnels
8pub struct TunnelManager {
9    provider: Arc<dyn TunnelProvider>,
10    active_tunnel: Arc<RwLock<Option<TunnelStatus>>>,
11}
12
13impl TunnelManager {
14    /// Create a new tunnel manager
15    pub fn new(config: &TunnelConfig) -> Result<Self> {
16        let provider: Arc<dyn TunnelProvider> = match config.provider {
17            crate::config::TunnelProvider::SelfHosted => {
18                let server_url = config.server_url.clone().ok_or_else(|| {
19                    TunnelError::ConfigError(
20                        "server_url required for self-hosted provider".to_string(),
21                    )
22                })?;
23                Arc::new(SelfHostedProvider::new(server_url, config.auth_token.clone()))
24            }
25            crate::config::TunnelProvider::Cloud => {
26                // For now, fallback to self-hosted behavior
27                // In the future, this could connect to MockForge Cloud
28                let server_url = config
29                    .server_url
30                    .clone()
31                    .unwrap_or_else(|| "https://tunnel.mockforge.dev".to_string());
32                Arc::new(SelfHostedProvider::new(server_url, config.auth_token.clone()))
33            }
34            crate::config::TunnelProvider::Cloudflare => {
35                return Err(TunnelError::ProviderError(
36                    "Cloudflare tunnel support coming soon".to_string(),
37                ));
38            }
39            crate::config::TunnelProvider::Ngrok => {
40                return Err(TunnelError::ProviderError(
41                    "ngrok tunnel support coming soon".to_string(),
42                ));
43            }
44            crate::config::TunnelProvider::Localtunnel => {
45                return Err(TunnelError::ProviderError(
46                    "localtunnel support coming soon".to_string(),
47                ));
48            }
49        };
50
51        Ok(Self {
52            provider,
53            active_tunnel: Arc::new(RwLock::new(None)),
54        })
55    }
56
57    /// Create and start a tunnel
58    pub async fn create_tunnel(&self, config: &TunnelConfig) -> Result<TunnelStatus> {
59        // Check if tunnel already exists
60        {
61            let tunnel = self.active_tunnel.read().await;
62            if tunnel.is_some() {
63                return Err(TunnelError::AlreadyExists("Tunnel already active".to_string()));
64            }
65        }
66
67        // Create tunnel via provider
68        let status = self.provider.create_tunnel(config).await?;
69
70        // Store active tunnel
71        {
72            let mut tunnel = self.active_tunnel.write().await;
73            *tunnel = Some(status.clone());
74        }
75
76        Ok(status)
77    }
78
79    /// Get the current tunnel status
80    pub async fn get_status(&self) -> Result<Option<TunnelStatus>> {
81        let tunnel = self.active_tunnel.read().await;
82        Ok(tunnel.clone())
83    }
84
85    /// Refresh tunnel status from provider
86    pub async fn refresh_status(&self) -> Result<TunnelStatus> {
87        let tunnel_id = {
88            let tunnel = self.active_tunnel.read().await;
89            tunnel
90                .as_ref()
91                .map(|t| t.tunnel_id.clone())
92                .ok_or_else(|| TunnelError::NotFound("No active tunnel".to_string()))?
93        };
94
95        let status = self.provider.get_tunnel_status(&tunnel_id).await?;
96
97        // Update stored tunnel
98        {
99            let mut tunnel = self.active_tunnel.write().await;
100            *tunnel = Some(status.clone());
101        }
102
103        Ok(status)
104    }
105
106    /// Stop and delete the tunnel
107    pub async fn stop_tunnel(&self) -> Result<()> {
108        let tunnel_id = {
109            let tunnel = self.active_tunnel.read().await;
110            tunnel
111                .as_ref()
112                .map(|t| t.tunnel_id.clone())
113                .ok_or_else(|| TunnelError::NotFound("No active tunnel".to_string()))?
114        };
115
116        // Delete tunnel via provider
117        self.provider.delete_tunnel(&tunnel_id).await?;
118
119        // Clear active tunnel
120        {
121            let mut tunnel = self.active_tunnel.write().await;
122            *tunnel = None;
123        }
124
125        Ok(())
126    }
127
128    /// Stop and delete a tunnel by ID
129    pub async fn stop_tunnel_by_id(&self, tunnel_id: &str) -> Result<()> {
130        // Delete tunnel via provider
131        self.provider.delete_tunnel(tunnel_id).await?;
132
133        // Clear active tunnel if it matches
134        {
135            let mut tunnel = self.active_tunnel.write().await;
136            if tunnel.as_ref().map(|t| t.tunnel_id.as_str()) == Some(tunnel_id) {
137                *tunnel = None;
138            }
139        }
140
141        Ok(())
142    }
143
144    /// List all tunnels
145    pub async fn list_tunnels(&self) -> Result<Vec<TunnelStatus>> {
146        self.provider.list_tunnels().await
147    }
148
149    /// Check if provider is available
150    pub async fn is_available(&self) -> bool {
151        self.provider.is_available().await
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::config::TunnelConfig;
159
160    fn create_test_config() -> TunnelConfig {
161        TunnelConfig {
162            provider: crate::config::TunnelProvider::SelfHosted,
163            server_url: Some("https://tunnel.example.com".to_string()),
164            auth_token: Some("test-token".to_string()),
165            subdomain: Some("test".to_string()),
166            local_url: "http://localhost:3000".to_string(),
167            protocol: "http".to_string(),
168            region: None,
169            custom_domain: None,
170            websocket_enabled: true,
171            http2_enabled: true,
172        }
173    }
174
175    #[test]
176    fn test_tunnel_manager_new() {
177        let config = create_test_config();
178        let manager = TunnelManager::new(&config);
179        assert!(manager.is_ok());
180    }
181
182    #[test]
183    fn test_tunnel_manager_new_without_server_url() {
184        let mut config = create_test_config();
185        config.server_url = None;
186
187        let result = TunnelManager::new(&config);
188        assert!(result.is_err());
189
190        if let Err(e) = result {
191            match e {
192                TunnelError::ConfigError(msg) => {
193                    assert!(msg.contains("server_url"));
194                }
195                _ => panic!("Expected ConfigError"),
196            }
197        }
198    }
199
200    #[test]
201    fn test_tunnel_manager_new_cloud_provider() {
202        let mut config = create_test_config();
203        config.provider = crate::config::TunnelProvider::Cloud;
204        config.server_url = None; // Cloud should use default
205
206        let result = TunnelManager::new(&config);
207        assert!(result.is_ok());
208    }
209
210    #[test]
211    fn test_tunnel_manager_new_cloudflare_provider() {
212        let mut config = create_test_config();
213        config.provider = crate::config::TunnelProvider::Cloudflare;
214
215        let result = TunnelManager::new(&config);
216        assert!(result.is_err());
217
218        if let Err(e) = result {
219            match e {
220                TunnelError::ProviderError(msg) => {
221                    assert!(msg.contains("Cloudflare"));
222                    assert!(msg.contains("coming soon"));
223                }
224                _ => panic!("Expected ProviderError"),
225            }
226        }
227    }
228
229    #[test]
230    fn test_tunnel_manager_new_ngrok_provider() {
231        let mut config = create_test_config();
232        config.provider = crate::config::TunnelProvider::Ngrok;
233
234        let result = TunnelManager::new(&config);
235        assert!(result.is_err());
236
237        if let Err(e) = result {
238            match e {
239                TunnelError::ProviderError(msg) => {
240                    assert!(msg.contains("ngrok"));
241                    assert!(msg.contains("coming soon"));
242                }
243                _ => panic!("Expected ProviderError"),
244            }
245        }
246    }
247
248    #[test]
249    fn test_tunnel_manager_new_localtunnel_provider() {
250        let mut config = create_test_config();
251        config.provider = crate::config::TunnelProvider::Localtunnel;
252
253        let result = TunnelManager::new(&config);
254        assert!(result.is_err());
255
256        if let Err(e) = result {
257            match e {
258                TunnelError::ProviderError(msg) => {
259                    assert!(msg.contains("localtunnel"));
260                    assert!(msg.contains("coming soon"));
261                }
262                _ => panic!("Expected ProviderError"),
263            }
264        }
265    }
266
267    #[tokio::test]
268    async fn test_get_status_no_tunnel() {
269        let config = create_test_config();
270        let manager = TunnelManager::new(&config).unwrap();
271
272        let status = manager.get_status().await;
273        assert!(status.is_ok());
274        assert!(status.unwrap().is_none());
275    }
276
277    #[tokio::test]
278    async fn test_refresh_status_no_tunnel() {
279        let config = create_test_config();
280        let manager = TunnelManager::new(&config).unwrap();
281
282        let result = manager.refresh_status().await;
283        assert!(result.is_err());
284
285        if let Err(e) = result {
286            match e {
287                TunnelError::NotFound(msg) => {
288                    assert!(msg.contains("No active tunnel"));
289                }
290                _ => panic!("Expected NotFound error"),
291            }
292        }
293    }
294
295    #[tokio::test]
296    async fn test_stop_tunnel_no_tunnel() {
297        let config = create_test_config();
298        let manager = TunnelManager::new(&config).unwrap();
299
300        let result = manager.stop_tunnel().await;
301        assert!(result.is_err());
302
303        if let Err(e) = result {
304            match e {
305                TunnelError::NotFound(msg) => {
306                    assert!(msg.contains("No active tunnel"));
307                }
308                _ => panic!("Expected NotFound error"),
309            }
310        }
311    }
312
313    #[test]
314    fn test_tunnel_manager_with_different_protocols() {
315        let protocols = vec!["http", "https", "ws", "wss"];
316
317        for protocol in protocols {
318            let mut config = create_test_config();
319            config.protocol = protocol.to_string();
320
321            let result = TunnelManager::new(&config);
322            assert!(result.is_ok(), "Failed to create manager with protocol: {}", protocol);
323        }
324    }
325
326    #[test]
327    fn test_tunnel_manager_with_websocket_disabled() {
328        let mut config = create_test_config();
329        config.websocket_enabled = false;
330
331        let result = TunnelManager::new(&config);
332        assert!(result.is_ok());
333    }
334
335    #[test]
336    fn test_tunnel_manager_with_http2_disabled() {
337        let mut config = create_test_config();
338        config.http2_enabled = false;
339
340        let result = TunnelManager::new(&config);
341        assert!(result.is_ok());
342    }
343
344    #[test]
345    fn test_tunnel_manager_with_custom_domain() {
346        let mut config = create_test_config();
347        config.custom_domain = Some("api.example.com".to_string());
348
349        let result = TunnelManager::new(&config);
350        assert!(result.is_ok());
351    }
352
353    #[test]
354    fn test_tunnel_manager_with_region() {
355        let mut config = create_test_config();
356        config.region = Some("us-west".to_string());
357
358        let result = TunnelManager::new(&config);
359        assert!(result.is_ok());
360    }
361
362    #[test]
363    fn test_tunnel_manager_without_auth_token() {
364        let mut config = create_test_config();
365        config.auth_token = None;
366
367        let result = TunnelManager::new(&config);
368        assert!(result.is_ok());
369    }
370
371    #[test]
372    fn test_tunnel_manager_with_different_local_urls() {
373        let urls = vec![
374            "http://localhost:3000",
375            "http://127.0.0.1:8080",
376            "http://0.0.0.0:5000",
377            "https://internal-api:443",
378        ];
379
380        for url in urls {
381            let mut config = create_test_config();
382            config.local_url = url.to_string();
383
384            let result = TunnelManager::new(&config);
385            assert!(result.is_ok(), "Failed to create manager with local_url: {}", url);
386        }
387    }
388
389    #[test]
390    fn test_tunnel_manager_builder_pattern() {
391        let config = TunnelConfig::new("http://localhost:3000")
392            .with_provider(crate::config::TunnelProvider::SelfHosted)
393            .with_auth_token("token123")
394            .with_subdomain("myapp");
395
396        let mut config_with_server = config;
397        config_with_server.server_url = Some("https://tunnel.example.com".to_string());
398
399        let result = TunnelManager::new(&config_with_server);
400        assert!(result.is_ok());
401    }
402
403    #[test]
404    fn test_tunnel_manager_clone_config() {
405        let config = create_test_config();
406        let cloned = config.clone();
407
408        let manager1 = TunnelManager::new(&config);
409        let manager2 = TunnelManager::new(&cloned);
410
411        assert!(manager1.is_ok());
412        assert!(manager2.is_ok());
413    }
414}