ddns_a/network/
fetcher.rs

1//! Address fetching trait and error types.
2
3use super::AdapterSnapshot;
4use thiserror::Error;
5
6/// Error type for address fetching operations.
7///
8/// Describes what went wrong without dictating recovery strategy.
9/// Callers decide how to handle each error variant.
10#[derive(Debug, Error)]
11pub enum FetchError {
12    /// Windows API call failed.
13    #[cfg(windows)]
14    #[error("Windows API error: {0}")]
15    WindowsApi(#[from] windows::core::Error),
16
17    /// Permission denied to access network information.
18    #[error("Permission denied: {context}")]
19    PermissionDenied {
20        /// Additional context about what permission was denied.
21        context: String,
22    },
23
24    /// Platform-specific error with a generic message.
25    #[error("Platform error: {message}")]
26    Platform {
27        /// Error message describing the platform-specific failure.
28        message: String,
29    },
30}
31
32/// Trait for fetching network adapter address information.
33///
34/// # Design
35///
36/// - All external dependencies should implement this trait
37/// - Enables dependency injection for testing with mock implementations
38/// - Platform-specific implementations provided in submodules
39///
40/// # Example
41///
42/// ```ignore
43/// use ddns_a::network::{AddressFetcher, AdapterSnapshot};
44///
45/// struct MockFetcher {
46///     snapshots: Vec<Vec<AdapterSnapshot>>,
47///     call_count: std::sync::atomic::AtomicUsize,
48/// }
49///
50/// impl AddressFetcher for MockFetcher {
51///     fn fetch(&self) -> Result<Vec<AdapterSnapshot>, FetchError> {
52///         let idx = self.call_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
53///         Ok(self.snapshots.get(idx).cloned().unwrap_or_default())
54///     }
55/// }
56/// ```
57pub trait AddressFetcher: Send + Sync {
58    /// Fetches the current state of all network adapters.
59    ///
60    /// # Returns
61    ///
62    /// A vector of [`AdapterSnapshot`] representing all adapters on the system,
63    /// or a [`FetchError`] if the operation fails.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`FetchError`] when:
68    /// - Platform API calls fail (e.g., `FetchError::WindowsApi` on Windows)
69    /// - Insufficient permissions to access network information (`FetchError::PermissionDenied`)
70    /// - Other platform-specific failures (`FetchError::Platform`)
71    ///
72    /// # Implementation Notes
73    ///
74    /// - Implementations should return ALL adapters; filtering is done by the caller
75    /// - Address order within each adapter should be stable across calls
76    /// - This is a synchronous operation; async wrappers can be added if needed
77    fn fetch(&self) -> Result<Vec<AdapterSnapshot>, FetchError>;
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::network::{AdapterKind, AdapterSnapshot};
84    use std::sync::Mutex;
85
86    /// A mock fetcher for testing that returns predefined snapshots.
87    ///
88    /// Uses `Mutex<VecDeque>` to avoid requiring `Clone` on `FetchError`.
89    struct MockFetcher {
90        results: Mutex<std::collections::VecDeque<Result<Vec<AdapterSnapshot>, FetchError>>>,
91    }
92
93    impl MockFetcher {
94        fn new(results: Vec<Result<Vec<AdapterSnapshot>, FetchError>>) -> Self {
95            Self {
96                results: Mutex::new(results.into()),
97            }
98        }
99
100        fn returning_snapshots(snapshots: Vec<Vec<AdapterSnapshot>>) -> Self {
101            Self::new(snapshots.into_iter().map(Ok).collect())
102        }
103    }
104
105    impl AddressFetcher for MockFetcher {
106        fn fetch(&self) -> Result<Vec<AdapterSnapshot>, FetchError> {
107            self.results
108                .lock()
109                .unwrap()
110                .pop_front()
111                .unwrap_or_else(|| Ok(vec![]))
112        }
113    }
114
115    #[test]
116    fn mock_fetcher_returns_predefined_snapshots() {
117        let snapshot = AdapterSnapshot::new(
118            "eth0",
119            AdapterKind::Ethernet,
120            vec!["192.168.1.1".parse().unwrap()],
121            vec![],
122        );
123        let fetcher = MockFetcher::returning_snapshots(vec![vec![snapshot.clone()]]);
124
125        let result = fetcher.fetch().unwrap();
126
127        assert_eq!(result.len(), 1);
128        assert_eq!(result[0], snapshot);
129    }
130
131    #[test]
132    fn mock_fetcher_returns_different_results_on_each_call() {
133        let snapshot1 = AdapterSnapshot::new("eth0", AdapterKind::Ethernet, vec![], vec![]);
134        let snapshot2 = AdapterSnapshot::new("eth1", AdapterKind::Wireless, vec![], vec![]);
135
136        let fetcher = MockFetcher::returning_snapshots(vec![vec![snapshot1], vec![snapshot2]]);
137
138        let result1 = fetcher.fetch().unwrap();
139        let result2 = fetcher.fetch().unwrap();
140
141        assert_eq!(result1[0].name, "eth0");
142        assert_eq!(result2[0].name, "eth1");
143    }
144
145    #[test]
146    fn mock_fetcher_returns_empty_after_exhausting_results() {
147        let fetcher = MockFetcher::returning_snapshots(vec![vec![]]);
148
149        let _ = fetcher.fetch(); // First call
150        let result = fetcher.fetch().unwrap(); // Second call - should return empty
151
152        assert!(result.is_empty());
153    }
154
155    #[test]
156    fn mock_fetcher_can_return_errors() {
157        let fetcher = MockFetcher::new(vec![Err(FetchError::Platform {
158            message: "test error".to_string(),
159        })]);
160
161        let result = fetcher.fetch();
162
163        assert!(result.is_err());
164        let error = result.unwrap_err();
165        assert!(error.to_string().contains("test error"));
166    }
167
168    #[test]
169    fn fetch_error_permission_denied_displays_context() {
170        let error = FetchError::PermissionDenied {
171            context: "elevated privileges required".to_string(),
172        };
173        assert!(error.to_string().contains("elevated privileges required"));
174    }
175
176    #[test]
177    fn fetch_error_platform_displays_message() {
178        let error = FetchError::Platform {
179            message: "unsupported operation".to_string(),
180        };
181        assert!(error.to_string().contains("unsupported operation"));
182    }
183}