Skip to main content

rch_common/
mock_worker.rs

1//! Mock worker server helper for tests.
2//!
3//! This helper configures the global mock SSH/rsync overrides and provides
4//! a mock:// URI for worker configs. It does not open network sockets; it is
5//! intended for CI and E2E tests where real SSH is unavailable.
6
7use crate::mock::{
8    MockConfig, MockRsyncConfig, clear_mock_overrides, set_mock_enabled_override,
9    set_mock_rsync_config_override, set_mock_ssh_config_override,
10};
11use std::sync::atomic::{AtomicUsize, Ordering};
12
13static MOCK_WORKER_COUNTER: AtomicUsize = AtomicUsize::new(0);
14
15#[derive(Debug, Clone)]
16pub struct MockWorkerServer {
17    uri: String,
18    ssh_config: MockConfig,
19    rsync_config: MockRsyncConfig,
20    started: bool,
21}
22
23impl MockWorkerServer {
24    pub fn builder() -> MockWorkerServerBuilder {
25        MockWorkerServerBuilder::default()
26    }
27
28    /// Return the mock:// URI to use in worker configs.
29    pub fn uri(&self) -> &str {
30        &self.uri
31    }
32
33    /// Enable mock transport globally with this server's config.
34    pub fn start(&mut self) {
35        if self.started {
36            return;
37        }
38        set_mock_enabled_override(Some(true));
39        set_mock_ssh_config_override(Some(self.ssh_config.clone()));
40        set_mock_rsync_config_override(Some(self.rsync_config.clone()));
41        self.started = true;
42    }
43
44    /// Disable mock transport overrides.
45    pub fn stop(&mut self) {
46        if !self.started {
47            return;
48        }
49        clear_mock_overrides();
50        self.started = false;
51    }
52}
53
54impl Drop for MockWorkerServer {
55    fn drop(&mut self) {
56        self.stop();
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct MockWorkerServerBuilder {
62    uri: Option<String>,
63    ssh_config: MockConfig,
64    rsync_config: MockRsyncConfig,
65}
66
67impl Default for MockWorkerServerBuilder {
68    fn default() -> Self {
69        Self {
70            uri: None,
71            ssh_config: MockConfig::success(),
72            rsync_config: MockRsyncConfig::success(),
73        }
74    }
75}
76
77impl MockWorkerServerBuilder {
78    /// Set the mock:// bind URI (e.g., mock://localhost:9900).
79    pub fn bind(mut self, uri: impl Into<String>) -> Self {
80        self.uri = Some(normalize_uri(uri.into()));
81        self
82    }
83
84    /// Override mock SSH behavior.
85    pub fn ssh_config(mut self, config: MockConfig) -> Self {
86        self.ssh_config = config;
87        self
88    }
89
90    /// Override mock rsync behavior.
91    pub fn rsync_config(mut self, config: MockRsyncConfig) -> Self {
92        self.rsync_config = config;
93        self
94    }
95
96    pub fn build(self) -> MockWorkerServer {
97        MockWorkerServer {
98            uri: self.uri.unwrap_or_else(default_uri),
99            ssh_config: self.ssh_config,
100            rsync_config: self.rsync_config,
101            started: false,
102        }
103    }
104}
105
106fn default_uri() -> String {
107    let id = MOCK_WORKER_COUNTER.fetch_add(1, Ordering::SeqCst) + 1;
108    format!("mock://worker-{}", id)
109}
110
111fn normalize_uri(uri: String) -> String {
112    if uri.starts_with("mock://") {
113        uri
114    } else {
115        format!("mock://{}", uri)
116    }
117}
118
119#[cfg(test)]
120#[allow(unsafe_code)]
121mod tests {
122    use super::*;
123    use crate::mock::{clear_mock_overrides, clear_thread_mock_override, is_mock_enabled};
124    use std::env;
125
126    fn clear_env() {
127        // SAFETY: Tests control env var lifecycle within the module.
128        unsafe { env::remove_var("RCH_MOCK_SSH") };
129        clear_thread_mock_override();
130    }
131
132    #[test]
133    fn test_default_uri_prefix_and_uniqueness() {
134        let server_a = MockWorkerServer::builder().build();
135        let server_b = MockWorkerServer::builder().build();
136
137        assert!(server_a.uri().starts_with("mock://worker-"));
138        assert!(server_b.uri().starts_with("mock://worker-"));
139        assert_ne!(server_a.uri(), server_b.uri());
140    }
141
142    #[test]
143    fn test_bind_normalizes_uri() {
144        let server = MockWorkerServer::builder().bind("localhost:9900").build();
145        assert_eq!(server.uri(), "mock://localhost:9900");
146    }
147
148    #[test]
149    fn test_bind_preserves_mock_prefix() {
150        let server = MockWorkerServer::builder()
151            .bind("mock://example:1234")
152            .build();
153        assert_eq!(server.uri(), "mock://example:1234");
154    }
155
156    #[test]
157    fn test_start_stop_toggles_mock_enabled() {
158        clear_env();
159        clear_mock_overrides();
160
161        let mut server = MockWorkerServer::builder().bind("worker-a").build();
162        assert!(!is_mock_enabled());
163
164        server.start();
165        assert!(is_mock_enabled());
166
167        server.stop();
168        assert!(!is_mock_enabled());
169
170        clear_mock_overrides();
171    }
172
173    // -------------------------------------------------------------------------
174    // MockWorkerServer trait tests
175    // -------------------------------------------------------------------------
176
177    #[test]
178    fn test_mock_worker_server_debug() {
179        let server = MockWorkerServer::builder().bind("debug-worker").build();
180        let debug_str = format!("{:?}", server);
181        assert!(debug_str.contains("MockWorkerServer"));
182        assert!(debug_str.contains("uri"));
183        assert!(debug_str.contains("mock://debug-worker"));
184    }
185
186    #[test]
187    fn test_mock_worker_server_clone() {
188        let server = MockWorkerServer::builder().bind("clone-worker").build();
189        let cloned = server.clone();
190        assert_eq!(cloned.uri(), "mock://clone-worker");
191    }
192
193    // -------------------------------------------------------------------------
194    // MockWorkerServerBuilder trait tests
195    // -------------------------------------------------------------------------
196
197    #[test]
198    fn test_builder_debug() {
199        let builder = MockWorkerServer::builder().bind("builder-debug");
200        let debug_str = format!("{:?}", builder);
201        assert!(debug_str.contains("MockWorkerServerBuilder"));
202    }
203
204    #[test]
205    fn test_builder_clone() {
206        let builder = MockWorkerServer::builder().bind("builder-clone");
207        let cloned = builder.clone();
208        let server = cloned.build();
209        assert_eq!(server.uri(), "mock://builder-clone");
210    }
211
212    #[test]
213    fn test_builder_default() {
214        let builder = MockWorkerServerBuilder::default();
215        let server = builder.build();
216        // Default should generate unique mock://worker-N URI
217        assert!(server.uri().starts_with("mock://worker-"));
218    }
219
220    // -------------------------------------------------------------------------
221    // Builder method tests
222    // -------------------------------------------------------------------------
223
224    #[test]
225    fn test_builder_ssh_config() {
226        let custom_config = MockConfig::connection_failure();
227        let server = MockWorkerServer::builder()
228            .ssh_config(custom_config.clone())
229            .build();
230        // Verify the config was set (we can't easily inspect it, but at least it compiles)
231        let debug_str = format!("{:?}", server);
232        assert!(debug_str.contains("ssh_config"));
233    }
234
235    #[test]
236    fn test_builder_rsync_config() {
237        let custom_config = MockRsyncConfig::sync_failure();
238        let server = MockWorkerServer::builder()
239            .rsync_config(custom_config.clone())
240            .build();
241        // Verify the config was set
242        let debug_str = format!("{:?}", server);
243        assert!(debug_str.contains("rsync_config"));
244    }
245
246    #[test]
247    fn test_builder_method_chaining() {
248        let server = MockWorkerServer::builder()
249            .bind("chained-worker")
250            .ssh_config(MockConfig::success())
251            .rsync_config(MockRsyncConfig::success())
252            .build();
253        assert_eq!(server.uri(), "mock://chained-worker");
254    }
255
256    // -------------------------------------------------------------------------
257    // Idempotent start/stop tests
258    // -------------------------------------------------------------------------
259
260    #[test]
261    fn test_start_is_idempotent() {
262        clear_env();
263        clear_mock_overrides();
264
265        let mut server = MockWorkerServer::builder().bind("idempotent-start").build();
266
267        // Start multiple times should be safe
268        server.start();
269        assert!(is_mock_enabled());
270        server.start(); // Second start should be no-op
271        assert!(is_mock_enabled());
272        server.start(); // Third start should be no-op
273        assert!(is_mock_enabled());
274
275        server.stop();
276        clear_mock_overrides();
277    }
278
279    #[test]
280    fn test_stop_is_idempotent() {
281        clear_env();
282        clear_mock_overrides();
283
284        let mut server = MockWorkerServer::builder().bind("idempotent-stop").build();
285
286        // Stop without start should be safe
287        server.stop();
288        assert!(!is_mock_enabled());
289        server.stop(); // Second stop should be no-op
290        assert!(!is_mock_enabled());
291
292        clear_mock_overrides();
293    }
294
295    #[test]
296    fn test_stop_without_start_is_safe() {
297        clear_env();
298        clear_mock_overrides();
299
300        let mut server = MockWorkerServer::builder().bind("no-start-stop").build();
301        // Never started, stop should be a no-op
302        server.stop();
303        assert!(!is_mock_enabled());
304
305        clear_mock_overrides();
306    }
307
308    // -------------------------------------------------------------------------
309    // Drop behavior tests
310    // -------------------------------------------------------------------------
311
312    #[test]
313    fn test_drop_clears_mock_state() {
314        clear_env();
315        clear_mock_overrides();
316
317        {
318            let mut server = MockWorkerServer::builder().bind("drop-test").build();
319            server.start();
320            assert!(is_mock_enabled());
321            // Server is dropped here
322        }
323
324        // After drop, mock should be disabled
325        assert!(!is_mock_enabled());
326
327        clear_mock_overrides();
328    }
329
330    // -------------------------------------------------------------------------
331    // URI normalization tests
332    // -------------------------------------------------------------------------
333
334    #[test]
335    fn test_normalize_uri_without_prefix() {
336        assert_eq!(
337            normalize_uri("localhost:9900".to_string()),
338            "mock://localhost:9900"
339        );
340    }
341
342    #[test]
343    fn test_normalize_uri_with_prefix() {
344        assert_eq!(
345            normalize_uri("mock://already-prefixed".to_string()),
346            "mock://already-prefixed"
347        );
348    }
349
350    #[test]
351    fn test_normalize_uri_empty() {
352        assert_eq!(normalize_uri("".to_string()), "mock://");
353    }
354}