Skip to main content

trusty_console/detect/
search.rs

1//! `ServiceConnector` implementation for `trusty-search`.
2//!
3//! Why: trusty-search writes its bound address to `~/.trusty-search/http_addr`
4//! on successful bind. This connector reads that file and probes the TCP port.
5//! What: `SearchConnector` implements `ServiceConnector::detect()` using
6//! `~/.trusty-search/http_addr` as the discovery file and `trusty-search` as
7//! the binary name.
8//! Test: `test_search_connector_*` in the module below. Run with
9//! `cargo test -p trusty-console`.
10
11use std::path::PathBuf;
12
13use crate::connector::{ServiceConnector, ServiceInfo};
14
15use super::helpers::detect_service;
16
17/// ServiceConnector for `trusty-search`.
18///
19/// Why: trusty-search writes its bound address to `~/.trusty-search/http_addr`
20/// on successful bind. This connector reads that file and probes the TCP port.
21/// What: Implements `detect()` using `~/.trusty-search/http_addr` as the
22/// discovery file and `trusty-search` as the binary name.
23/// Test: `test_search_connector_with_stale_addr_file` and
24/// `test_search_connector_no_addr_file` below.
25pub struct SearchConnector {
26    /// Override for the home directory (used in tests).
27    home_dir: Option<PathBuf>,
28}
29
30impl SearchConnector {
31    /// Create a new `SearchConnector`.
32    ///
33    /// Why: Production callers use `new()`; tests use `with_home()`.
34    /// What: Stores no state except the optional home override.
35    /// Test: Created in `all_connectors()` and in unit tests.
36    pub fn new() -> Self {
37        Self { home_dir: None }
38    }
39
40    /// Create a connector that uses `home_dir` instead of the real home.
41    ///
42    /// Why: Unit tests must not read or write the real user's `~/.trusty-*`
43    /// directories. Injecting a temp dir keeps tests hermetic.
44    /// What: Stores `home_dir` for use in `addr_file_path()`.
45    /// Test: `test_search_connector_with_stale_addr_file`,
46    /// `test_search_connector_no_addr_file`.
47    #[cfg(test)]
48    pub fn with_home(home_dir: PathBuf) -> Self {
49        Self {
50            home_dir: Some(home_dir),
51        }
52    }
53
54    fn addr_file_path(&self) -> PathBuf {
55        let home = self
56            .home_dir
57            .clone()
58            .or_else(dirs::home_dir)
59            .unwrap_or_else(|| PathBuf::from("/tmp"));
60        home.join(".trusty-search").join("http_addr")
61    }
62}
63
64impl Default for SearchConnector {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl ServiceConnector for SearchConnector {
71    fn id(&self) -> &'static str {
72        "trusty-search"
73    }
74
75    fn display_name(&self) -> &'static str {
76        "Trusty Search"
77    }
78
79    /// Detect trusty-search status.
80    ///
81    /// Why: Reads `~/.trusty-search/http_addr` — the file the daemon writes
82    /// immediately after successfully binding its port.
83    /// What: Three-step sequence: binary check → addr file + TCP probe → status.
84    /// Test: `test_search_connector_with_stale_addr_file`,
85    /// `test_search_connector_no_addr_file`.
86    fn detect(&self) -> ServiceInfo {
87        detect_service(
88            self.id(),
89            self.display_name(),
90            "trusty-search",
91            self.addr_file_path(),
92        )
93    }
94}
95
96// ─── tests ────────────────────────────────────────────────────────────────────
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::connector::ServiceStatus;
102    use std::fs;
103    use tempfile::TempDir;
104
105    fn make_home_with_addr(rel_path: &str, content: &str) -> TempDir {
106        let tmp = TempDir::new().expect("tempdir");
107        let path = tmp.path().join(rel_path);
108        fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
109        fs::write(&path, content).expect("write addr file");
110        tmp
111    }
112
113    /// Why: when http_addr contains a valid but unreachable address, the
114    /// connector must return Available (binary present, file present, TCP failed).
115    /// This test requires `trusty-search` to NOT be on PATH — it short-circuits
116    /// to Absent if it is, which is the correct behaviour in that environment.
117    /// What: creates a fake HOME with `.trusty-search/http_addr = 127.0.0.1:14998`
118    /// and calls detect(); expects either Absent (binary not on PATH in CI) or
119    /// Available (binary on PATH, TCP fails on 14998).
120    /// Test: this test itself.
121    #[test]
122    fn test_search_connector_with_stale_addr_file() {
123        let tmp = make_home_with_addr(".trusty-search/http_addr", "127.0.0.1:14998");
124        let connector = SearchConnector::with_home(tmp.path().to_path_buf());
125        let info = connector.detect();
126        // Either Absent (no binary) or Available (binary present, TCP stale).
127        assert!(
128            info.status == ServiceStatus::Absent || info.status == ServiceStatus::Available,
129            "expected Absent or Available, got {:?}",
130            info.status
131        );
132        assert_eq!(info.id, "trusty-search");
133        assert_eq!(info.display_name, "Trusty Search");
134    }
135
136    /// Why: when no http_addr file exists, detect() must return Absent or
137    /// Available depending on whether the binary is on PATH.
138    /// What: temp HOME with no trusty-search dir.
139    /// Test: this test itself.
140    #[test]
141    fn test_search_connector_no_addr_file() {
142        let tmp = TempDir::new().expect("tempdir");
143        let connector = SearchConnector::with_home(tmp.path().to_path_buf());
144        let info = connector.detect();
145        assert!(
146            info.status == ServiceStatus::Absent || info.status == ServiceStatus::Available,
147            "expected Absent or Available, got {:?}",
148            info.status
149        );
150        assert!(info.url.is_none());
151    }
152}