Skip to main content

mneme/sync/transport/
http.rs

1use std::time::Duration;
2
3use crate::error::{MnemeError, Result};
4use crate::sync::protocol::{SyncHello, SyncRequest, SyncResponse};
5
6/// Transporte HTTP para sincronizacion.
7pub struct HttpTransport {
8    client: reqwest::Client,
9    base_url: String,
10}
11
12impl HttpTransport {
13    /// Crea un nuevo HttpTransport.
14    pub fn new(base_url: String) -> Result<Self> {
15        let client = reqwest::Client::builder()
16            .timeout(Duration::from_secs(30))
17            .build()
18            .map_err(|e| MnemeError::Http(e.to_string()))?;
19        Ok(Self { client, base_url })
20    }
21
22    /// Envia saludo inicial al peer.
23    pub async fn hello(&self, hello: &SyncHello) -> Result<SyncHello> {
24        let url = format!("{}/api/v1/sync/hello", self.base_url);
25        let res = self
26            .client
27            .post(&url)
28            .json(hello)
29            .send()
30            .await
31            .map_err(|e| MnemeError::Http(e.to_string()))?;
32        if !res.status().is_success() {
33            return Err(MnemeError::Http(format!("hello failed: {}", res.status())));
34        }
35        res.json()
36            .await
37            .map_err(|e| MnemeError::Http(e.to_string()))
38    }
39
40    /// Solicita cambios al peer (pull).
41    pub async fn pull(&self, request: &SyncRequest) -> Result<SyncResponse> {
42        let url = format!("{}/api/v1/sync/pull", self.base_url);
43        let res = self
44            .client
45            .post(&url)
46            .json(request)
47            .send()
48            .await
49            .map_err(|e| MnemeError::Http(e.to_string()))?;
50        if !res.status().is_success() {
51            return Err(MnemeError::Http(format!("pull failed: {}", res.status())));
52        }
53        res.json()
54            .await
55            .map_err(|e| MnemeError::Http(e.to_string()))
56    }
57
58    /// Envia cambios al peer (push).
59    pub async fn push(&self, response: &SyncResponse) -> Result<()> {
60        let url = format!("{}/api/v1/sync/push", self.base_url);
61        let res = self
62            .client
63            .post(&url)
64            .json(response)
65            .send()
66            .await
67            .map_err(|e| MnemeError::Http(e.to_string()))?;
68        if !res.status().is_success() {
69            return Err(MnemeError::Http(format!("push failed: {}", res.status())));
70        }
71        Ok(())
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[tokio::test]
80    async fn test_http_transport_hello_unreachable() {
81        // Use invalid URL that fails connection immediately (no DNS resolution needed)
82        let transport = HttpTransport::new("http://255.255.255.255:1".to_string()).unwrap();
83        let hello = SyncHello {
84            project: "test".to_string(),
85            peer_id: uuid::Uuid::nil(),
86            peer_name: "test".to_string(),
87            mneme_version: "0.1.0".to_string(),
88            memory_count: 0,
89            heads: std::collections::HashMap::new(),
90        };
91        let result = tokio::time::timeout(
92            std::time::Duration::from_secs(3),
93            transport.hello(&hello),
94        )
95        .await;
96        assert!(result.is_ok(), "hello should complete within timeout");
97        assert!(result.unwrap().is_err(), "hello to unreachable should error");
98    }
99
100    #[tokio::test]
101    async fn test_http_transport_pull_unreachable() {
102        let transport = HttpTransport::new("http://255.255.255.255:1".to_string()).unwrap();
103        let request = SyncRequest {
104            project: "test".to_string(),
105            have: std::collections::HashMap::new(),
106        };
107        let result = tokio::time::timeout(
108            std::time::Duration::from_secs(3),
109            transport.pull(&request),
110        )
111        .await;
112        assert!(result.is_ok(), "pull should complete within timeout");
113        assert!(result.unwrap().is_err(), "pull from unreachable should error");
114    }
115
116    #[tokio::test]
117    async fn test_http_transport_push_unreachable() {
118        let transport = HttpTransport::new("http://255.255.255.255:1".to_string()).unwrap();
119        let response = SyncResponse {
120            project: "test".to_string(),
121            changes: vec![],
122            tombstones: vec![],
123        };
124        let result = tokio::time::timeout(
125            std::time::Duration::from_secs(3),
126            transport.push(&response),
127        )
128        .await;
129        assert!(result.is_ok(), "push should complete within timeout");
130        assert!(result.unwrap().is_err(), "push to unreachable should error");
131    }
132
133    #[test]
134    fn test_http_transport_invalid_base_url() {
135        let result = HttpTransport::new(String::new());
136        // Empty base URL is technically valid for the client, just won't work for requests
137        assert!(result.is_ok());
138    }
139}