Skip to main content

mock_igd/server/
mod.rs

1//! Mock IGD server implementation.
2
3mod http;
4mod ssdp;
5
6use crate::action::Action;
7use crate::mock::{Mock, MockRegistry, ReceivedRequest, ReceivedSsdpRequest};
8use crate::responder::Responder;
9use crate::Result;
10use std::net::SocketAddr;
11use std::sync::Arc;
12use tokio::sync::oneshot;
13
14/// A mock UPnP IGD server for testing.
15pub struct MockIgdServer {
16    /// HTTP server address.
17    http_addr: SocketAddr,
18    /// SSDP server address (if enabled).
19    ssdp_addr: Option<SocketAddr>,
20    /// Mock registry.
21    registry: Arc<MockRegistry>,
22    /// Shutdown signal sender.
23    shutdown_tx: Option<oneshot::Sender<()>>,
24}
25
26impl MockIgdServer {
27    /// Start a new mock IGD server on a random available port.
28    pub async fn start() -> Result<Self> {
29        Self::builder().start().await
30    }
31
32    /// Create a builder for configuring the server.
33    pub fn builder() -> MockIgdServerBuilder {
34        MockIgdServerBuilder::default()
35    }
36
37    /// Get the URL of the HTTP server (for SOAP requests).
38    pub fn url(&self) -> String {
39        format!("http://{}", self.http_addr)
40    }
41
42    /// Get the control URL for SOAP actions.
43    pub fn control_url(&self) -> String {
44        format!("http://{}/ctl/IPConn", self.http_addr)
45    }
46
47    /// Get the device description URL.
48    pub fn description_url(&self) -> String {
49        format!("http://{}/rootDesc.xml", self.http_addr)
50    }
51
52    /// Get the HTTP server address.
53    pub fn http_addr(&self) -> SocketAddr {
54        self.http_addr
55    }
56
57    /// Get the SSDP server address (if enabled).
58    pub fn ssdp_addr(&self) -> Option<SocketAddr> {
59        self.ssdp_addr
60    }
61
62    /// Register a mock for the given action.
63    pub async fn mock(&self, action: impl Into<Action>, responder: impl Into<Responder>) {
64        let mock = Mock::new(action, responder);
65        self.registry.register(mock).await;
66    }
67
68    /// Register a mock with a specific priority (higher = checked first).
69    pub async fn mock_with_priority(
70        &self,
71        action: impl Into<Action>,
72        responder: impl Into<Responder>,
73        priority: u32,
74    ) {
75        let mock = Mock::new(action, responder).with_priority(priority);
76        self.registry.register(mock).await;
77    }
78
79    /// Register a mock that only matches a limited number of times.
80    pub async fn mock_with_times(
81        &self,
82        action: impl Into<Action>,
83        responder: impl Into<Responder>,
84        times: u32,
85    ) {
86        let mock = Mock::new(action, responder).times(times);
87        self.registry.register(mock).await;
88    }
89
90    /// Clear all registered mocks.
91    pub async fn clear_mocks(&self) {
92        self.registry.clear().await;
93    }
94
95    /// Get all received requests.
96    ///
97    /// Returns a list of all SOAP requests received by the server.
98    /// Useful for verifying that expected requests were made.
99    ///
100    /// # Example
101    ///
102    /// ```ignore
103    /// let requests = server.received_requests().await;
104    /// assert_eq!(requests.len(), 1);
105    /// assert_eq!(requests[0].action_name, "GetExternalIPAddress");
106    /// ```
107    pub async fn received_requests(&self) -> Vec<ReceivedRequest> {
108        self.registry.received_requests().await
109    }
110
111    /// Clear all received requests.
112    pub async fn clear_received_requests(&self) {
113        self.registry.clear_received_requests().await;
114    }
115
116    /// Get all received SSDP requests (M-SEARCH).
117    ///
118    /// Returns a list of all SSDP M-SEARCH requests received by the server.
119    /// Useful for verifying device discovery behavior.
120    ///
121    /// # Example
122    ///
123    /// ```ignore
124    /// let requests = server.received_ssdp_requests().await;
125    /// assert_eq!(requests.len(), 1);
126    /// assert_eq!(requests[0].search_target, "ssdp:all");
127    /// ```
128    pub async fn received_ssdp_requests(&self) -> Vec<ReceivedSsdpRequest> {
129        self.registry.received_ssdp_requests().await
130    }
131
132    /// Clear all received SSDP requests.
133    pub async fn clear_received_ssdp_requests(&self) {
134        self.registry.clear_received_ssdp_requests().await;
135    }
136
137    /// Shutdown the server.
138    pub fn shutdown(mut self) {
139        if let Some(tx) = self.shutdown_tx.take() {
140            let _ = tx.send(());
141        }
142    }
143}
144
145impl Drop for MockIgdServer {
146    fn drop(&mut self) {
147        if let Some(tx) = self.shutdown_tx.take() {
148            let _ = tx.send(());
149        }
150    }
151}
152
153/// Builder for configuring a mock IGD server.
154#[derive(Default)]
155pub struct MockIgdServerBuilder {
156    http_port: Option<u16>,
157    enable_ssdp: bool,
158    ssdp_port: Option<u16>,
159}
160
161impl MockIgdServerBuilder {
162    /// Set a specific port for the HTTP server.
163    pub fn http_port(mut self, port: u16) -> Self {
164        self.http_port = Some(port);
165        self
166    }
167
168    /// Enable SSDP discovery responses.
169    pub fn with_ssdp(mut self) -> Self {
170        self.enable_ssdp = true;
171        self
172    }
173
174    /// Set a specific port for SSDP (default: 1900).
175    pub fn ssdp_port(mut self, port: u16) -> Self {
176        self.ssdp_port = Some(port);
177        self.enable_ssdp = true;
178        self
179    }
180
181    /// Start the server with the configured options.
182    pub async fn start(self) -> Result<MockIgdServer> {
183        let registry = Arc::new(MockRegistry::new());
184        let (shutdown_tx, shutdown_rx) = oneshot::channel();
185
186        // Start HTTP server
187        let http_addr = format!("127.0.0.1:{}", self.http_port.unwrap_or(0));
188        let listener = tokio::net::TcpListener::bind(&http_addr).await?;
189        let http_addr = listener.local_addr()?;
190
191        let http_registry = registry.clone();
192        tokio::spawn(async move {
193            http::run_http_server(listener, http_registry, shutdown_rx).await;
194        });
195
196        // Start SSDP server if enabled
197        let ssdp_addr = if self.enable_ssdp {
198            let port = self.ssdp_port.unwrap_or(1900);
199            match ssdp::start_ssdp_server(http_addr, port, registry.clone()).await {
200                Ok(addr) => Some(addr),
201                Err(e) => {
202                    tracing::warn!("Failed to start SSDP server: {}", e);
203                    None
204                }
205            }
206        } else {
207            None
208        };
209
210        Ok(MockIgdServer {
211            http_addr,
212            ssdp_addr,
213            registry,
214            shutdown_tx: Some(shutdown_tx),
215        })
216    }
217}