mockforge_tunnel/
provider.rs

1//! Tunnel provider traits and implementations
2
3use crate::{Result, TunnelConfig};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Tunnel status information
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TunnelStatus {
11    /// Public URL of the tunnel
12    pub public_url: String,
13
14    /// Tunnel ID
15    pub tunnel_id: String,
16
17    /// Whether the tunnel is active
18    pub active: bool,
19
20    /// Request count
21    pub request_count: u64,
22
23    /// Bytes transferred
24    pub bytes_transferred: u64,
25
26    /// Created timestamp
27    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
28
29    /// Expires at (if applicable)
30    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
31
32    /// Local URL (for testing/info purposes)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub local_url: Option<String>,
35}
36
37/// Tunnel provider trait
38#[async_trait]
39pub trait TunnelProvider: Send + Sync {
40    /// Create a new tunnel
41    async fn create_tunnel(&self, config: &TunnelConfig) -> Result<TunnelStatus>;
42
43    /// Get tunnel status
44    async fn get_tunnel_status(&self, tunnel_id: &str) -> Result<TunnelStatus>;
45
46    /// Delete/stop a tunnel
47    async fn delete_tunnel(&self, tunnel_id: &str) -> Result<()>;
48
49    /// List all active tunnels
50    async fn list_tunnels(&self) -> Result<Vec<TunnelStatus>>;
51
52    /// Check if provider is available
53    async fn is_available(&self) -> bool;
54}
55
56/// Self-hosted tunnel provider
57pub struct SelfHostedProvider {
58    server_url: String,
59    auth_token: Option<String>,
60    client: reqwest::Client,
61}
62
63impl SelfHostedProvider {
64    /// Create a new self-hosted provider
65    pub fn new(server_url: impl Into<String>, auth_token: Option<String>) -> Self {
66        Self {
67            server_url: server_url.into(),
68            auth_token,
69            client: reqwest::Client::new(),
70        }
71    }
72}
73
74#[async_trait]
75impl TunnelProvider for SelfHostedProvider {
76    async fn create_tunnel(&self, config: &TunnelConfig) -> Result<TunnelStatus> {
77        let url = format!("{}/api/tunnels", self.server_url);
78        let mut request = self.client.post(&url);
79
80        // Add auth header if token is provided
81        if let Some(token) = &self.auth_token {
82            request = request.header("Authorization", format!("Bearer {}", token));
83        }
84
85        let payload = serde_json::json!({
86            "local_url": config.local_url,
87            "subdomain": config.subdomain,
88            "custom_domain": config.custom_domain,
89            "protocol": config.protocol,
90            "websocket_enabled": config.websocket_enabled,
91            "http2_enabled": config.http2_enabled,
92        });
93
94        let response = request
95            .json(&payload)
96            .send()
97            .await
98            .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
99
100        if !response.status().is_success() {
101            let error_text = response.text().await.unwrap_or_default();
102            return Err(crate::TunnelError::ProviderError(format!(
103                "Failed to create tunnel: {}",
104                error_text
105            )));
106        }
107
108        let status: TunnelStatus = response.json().await?;
109        Ok(status)
110    }
111
112    async fn get_tunnel_status(&self, tunnel_id: &str) -> Result<TunnelStatus> {
113        let url = format!("{}/api/tunnels/{}", self.server_url, tunnel_id);
114        let mut request = self.client.get(&url);
115
116        if let Some(token) = &self.auth_token {
117            request = request.header("Authorization", format!("Bearer {}", token));
118        }
119
120        let response = request
121            .send()
122            .await
123            .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
124
125        if !response.status().is_success() {
126            return Err(crate::TunnelError::NotFound(tunnel_id.to_string()));
127        }
128
129        let status: TunnelStatus = response.json().await?;
130        Ok(status)
131    }
132
133    async fn delete_tunnel(&self, tunnel_id: &str) -> Result<()> {
134        let url = format!("{}/api/tunnels/{}", self.server_url, tunnel_id);
135        let mut request = self.client.delete(&url);
136
137        if let Some(token) = &self.auth_token {
138            request = request.header("Authorization", format!("Bearer {}", token));
139        }
140
141        let response = request
142            .send()
143            .await
144            .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
145
146        if !response.status().is_success() {
147            return Err(crate::TunnelError::ProviderError(format!(
148                "Failed to delete tunnel: {}",
149                response.status()
150            )));
151        }
152
153        Ok(())
154    }
155
156    async fn list_tunnels(&self) -> Result<Vec<TunnelStatus>> {
157        let url = format!("{}/api/tunnels", self.server_url);
158        let mut request = self.client.get(&url);
159
160        if let Some(token) = &self.auth_token {
161            request = request.header("Authorization", format!("Bearer {}", token));
162        }
163
164        let response = request
165            .send()
166            .await
167            .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
168
169        if !response.status().is_success() {
170            return Err(crate::TunnelError::ProviderError(format!(
171                "Failed to list tunnels: {}",
172                response.status()
173            )));
174        }
175
176        let tunnels: Vec<TunnelStatus> = response.json().await?;
177        Ok(tunnels)
178    }
179
180    async fn is_available(&self) -> bool {
181        let url = format!("{}/health", self.server_url);
182        self.client.get(&url).timeout(Duration::from_secs(5)).send().await.is_ok()
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use chrono::Utc;
190
191    fn create_test_status() -> TunnelStatus {
192        TunnelStatus {
193            public_url: "https://test.tunnel.dev".to_string(),
194            tunnel_id: "tunnel-123".to_string(),
195            active: true,
196            request_count: 100,
197            bytes_transferred: 5000,
198            created_at: Some(Utc::now()),
199            expires_at: None,
200            local_url: Some("http://localhost:3000".to_string()),
201        }
202    }
203
204    #[test]
205    fn test_tunnel_status_clone() {
206        let status = create_test_status();
207        let cloned = status.clone();
208        assert_eq!(status.public_url, cloned.public_url);
209        assert_eq!(status.tunnel_id, cloned.tunnel_id);
210        assert_eq!(status.active, cloned.active);
211        assert_eq!(status.request_count, cloned.request_count);
212        assert_eq!(status.bytes_transferred, cloned.bytes_transferred);
213    }
214
215    #[test]
216    fn test_tunnel_status_debug() {
217        let status = create_test_status();
218        let debug = format!("{:?}", status);
219        assert!(debug.contains("TunnelStatus"));
220        assert!(debug.contains("tunnel-123"));
221    }
222
223    #[test]
224    fn test_tunnel_status_serialize() {
225        let status = create_test_status();
226        let json = serde_json::to_string(&status).unwrap();
227        assert!(json.contains("\"public_url\":\"https://test.tunnel.dev\""));
228        assert!(json.contains("\"tunnel_id\":\"tunnel-123\""));
229        assert!(json.contains("\"active\":true"));
230        assert!(json.contains("\"request_count\":100"));
231        assert!(json.contains("\"bytes_transferred\":5000"));
232    }
233
234    #[test]
235    fn test_tunnel_status_deserialize() {
236        let json = r#"{
237            "public_url": "https://example.tunnel.dev",
238            "tunnel_id": "tun-456",
239            "active": false,
240            "request_count": 50,
241            "bytes_transferred": 2500,
242            "created_at": null,
243            "expires_at": null
244        }"#;
245
246        let status: TunnelStatus = serde_json::from_str(json).unwrap();
247        assert_eq!(status.public_url, "https://example.tunnel.dev");
248        assert_eq!(status.tunnel_id, "tun-456");
249        assert!(!status.active);
250        assert_eq!(status.request_count, 50);
251        assert_eq!(status.bytes_transferred, 2500);
252        assert!(status.created_at.is_none());
253        assert!(status.expires_at.is_none());
254        assert!(status.local_url.is_none());
255    }
256
257    #[test]
258    fn test_tunnel_status_serialize_skip_none_local_url() {
259        let status = TunnelStatus {
260            public_url: "https://test.tunnel.dev".to_string(),
261            tunnel_id: "tunnel-123".to_string(),
262            active: true,
263            request_count: 0,
264            bytes_transferred: 0,
265            created_at: None,
266            expires_at: None,
267            local_url: None,
268        };
269        let json = serde_json::to_string(&status).unwrap();
270        // local_url should be skipped when None
271        assert!(!json.contains("local_url"));
272    }
273
274    #[test]
275    fn test_tunnel_status_with_local_url() {
276        let status = TunnelStatus {
277            public_url: "https://test.tunnel.dev".to_string(),
278            tunnel_id: "tunnel-123".to_string(),
279            active: true,
280            request_count: 0,
281            bytes_transferred: 0,
282            created_at: None,
283            expires_at: None,
284            local_url: Some("http://localhost:8080".to_string()),
285        };
286        let json = serde_json::to_string(&status).unwrap();
287        assert!(json.contains("\"local_url\":\"http://localhost:8080\""));
288    }
289
290    #[test]
291    fn test_tunnel_status_with_timestamps() {
292        let now = Utc::now();
293        let expires = now + chrono::Duration::hours(24);
294
295        let status = TunnelStatus {
296            public_url: "https://test.tunnel.dev".to_string(),
297            tunnel_id: "tunnel-123".to_string(),
298            active: true,
299            request_count: 0,
300            bytes_transferred: 0,
301            created_at: Some(now),
302            expires_at: Some(expires),
303            local_url: None,
304        };
305
306        assert!(status.created_at.is_some());
307        assert!(status.expires_at.is_some());
308        assert!(status.expires_at.unwrap() > status.created_at.unwrap());
309    }
310
311    #[test]
312    fn test_self_hosted_provider_new() {
313        let provider = SelfHostedProvider::new("https://tunnel.example.com", None);
314        assert_eq!(provider.server_url, "https://tunnel.example.com");
315        assert!(provider.auth_token.is_none());
316    }
317
318    #[test]
319    fn test_self_hosted_provider_new_with_token() {
320        let provider = SelfHostedProvider::new(
321            "https://tunnel.example.com",
322            Some("my-secret-token".to_string()),
323        );
324        assert_eq!(provider.server_url, "https://tunnel.example.com");
325        assert_eq!(provider.auth_token, Some("my-secret-token".to_string()));
326    }
327
328    #[test]
329    fn test_self_hosted_provider_new_with_string_conversion() {
330        let provider = SelfHostedProvider::new(String::from("https://api.tunnel.dev"), None);
331        assert_eq!(provider.server_url, "https://api.tunnel.dev");
332    }
333
334    #[test]
335    fn test_tunnel_status_roundtrip_serialization() {
336        let status = create_test_status();
337        let json = serde_json::to_string(&status).unwrap();
338        let deserialized: TunnelStatus = serde_json::from_str(&json).unwrap();
339
340        assert_eq!(status.public_url, deserialized.public_url);
341        assert_eq!(status.tunnel_id, deserialized.tunnel_id);
342        assert_eq!(status.active, deserialized.active);
343        assert_eq!(status.request_count, deserialized.request_count);
344        assert_eq!(status.bytes_transferred, deserialized.bytes_transferred);
345        assert_eq!(status.local_url, deserialized.local_url);
346    }
347
348    #[test]
349    fn test_tunnel_status_inactive() {
350        let status = TunnelStatus {
351            public_url: String::new(),
352            tunnel_id: "inactive-tunnel".to_string(),
353            active: false,
354            request_count: 0,
355            bytes_transferred: 0,
356            created_at: None,
357            expires_at: None,
358            local_url: None,
359        };
360
361        assert!(!status.active);
362        assert!(status.public_url.is_empty());
363    }
364
365    #[test]
366    fn test_tunnel_status_high_traffic() {
367        let status = TunnelStatus {
368            public_url: "https://high-traffic.tunnel.dev".to_string(),
369            tunnel_id: "high-traffic-1".to_string(),
370            active: true,
371            request_count: u64::MAX,
372            bytes_transferred: u64::MAX,
373            created_at: Some(Utc::now()),
374            expires_at: None,
375            local_url: None,
376        };
377
378        assert_eq!(status.request_count, u64::MAX);
379        assert_eq!(status.bytes_transferred, u64::MAX);
380    }
381}