Skip to main content

trusty_console/detect/
review.rs

1//! `ServiceConnector` implementation for `trusty-review`.
2//!
3//! Why: trusty-review writes its bound address to `~/.trusty-review/http_addr`
4//! on successful bind. This connector reads that file and probes the TCP port.
5//! The console previously excluded trusty-review per decision #1069; issue #1163
6//! lifts that exclusion now that the Review dashboard tab is implemented.
7//! What: `ReviewConnector` implements `ServiceConnector::detect()` using
8//! `~/.trusty-review/http_addr` as the discovery file and `trusty-review` as
9//! the binary name. Falls back to probing the default port (7880) when no
10//! discovery file is found, matching the AnalyzeConnector pattern.
11//! Test: `test_review_connector_with_stale_addr_file` and
12//! `test_review_connector_no_addr_file` below.
13
14use std::path::PathBuf;
15
16use crate::connector::{ServiceConnector, ServiceInfo, ServiceStatus};
17
18use super::helpers::{binary_on_path, fetch_health_version, read_addr_file, tcp_probe};
19
20/// ServiceConnector for `trusty-review`.
21///
22/// Why: trusty-review stores its data under `~/.trusty-review/`. An `http_addr`
23/// file there (if written by the running daemon) gives the exact address;
24/// otherwise we probe the default port 7880.
25/// What: Implements `detect()` checking `~/.trusty-review/http_addr`, then
26/// falling back to `http://127.0.0.1:7880` TCP probe when the file is absent.
27/// Test: `test_review_connector_with_stale_addr_file`,
28/// `test_review_connector_no_addr_file` below.
29pub struct ReviewConnector {
30    /// Override for the home directory (used in tests).
31    home_dir: Option<PathBuf>,
32}
33
34impl ReviewConnector {
35    /// Create a new `ReviewConnector`.
36    ///
37    /// Why: Production callers use `new()`; tests use `with_home()`.
38    /// What: Stores no state except the optional home override.
39    /// Test: Created in `all_connectors()` and in unit tests.
40    pub fn new() -> Self {
41        Self { home_dir: None }
42    }
43
44    /// Create a connector that uses `home_dir` instead of the real home.
45    ///
46    /// Why: Unit tests must not read or write the real user's `~/.trusty-*`
47    /// directories. Injecting a temp dir keeps tests hermetic.
48    /// What: Stores `home_dir` for use in `addr_file_path()`.
49    /// Test: `test_review_connector_with_stale_addr_file`,
50    /// `test_review_connector_no_addr_file`.
51    #[cfg(test)]
52    pub fn with_home(home_dir: PathBuf) -> Self {
53        Self {
54            home_dir: Some(home_dir),
55        }
56    }
57
58    fn addr_file_path(&self) -> PathBuf {
59        let home = self
60            .home_dir
61            .clone()
62            .or_else(dirs::home_dir)
63            .unwrap_or_else(|| PathBuf::from("/tmp"));
64        home.join(".trusty-review").join("http_addr")
65    }
66
67    /// Default address string for trusty-review.
68    ///
69    /// Why: trusty-review's fixed default port is 7880. Used as a fallback
70    /// when no http_addr file is found.
71    /// What: Returns the address string `"127.0.0.1:7880"`.
72    /// Test: Covered by the detect() fallback path.
73    pub(crate) fn default_addr() -> &'static str {
74        "127.0.0.1:7880"
75    }
76}
77
78impl Default for ReviewConnector {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl ServiceConnector for ReviewConnector {
85    fn id(&self) -> &'static str {
86        "trusty-review"
87    }
88
89    fn display_name(&self) -> &'static str {
90        "Trusty Review"
91    }
92
93    /// Detect trusty-review status.
94    ///
95    /// Why: Reads `~/.trusty-review/http_addr` — the file the daemon writes
96    /// immediately after successfully binding its port. Falls back to probing
97    /// the default port 7880 when no discovery file is present (matches the
98    /// AnalyzeConnector pattern).
99    /// What: Binary check → addr file + TCP probe → fallback default-port TCP → status.
100    /// Test: `test_review_connector_with_stale_addr_file`,
101    /// `test_review_connector_no_addr_file`.
102    fn detect(&self) -> ServiceInfo {
103        if !binary_on_path("trusty-review") {
104            return ServiceInfo {
105                id: self.id().to_string(),
106                display_name: self.display_name().to_string(),
107                status: ServiceStatus::Absent,
108                version: None,
109                url: None,
110                hint: None,
111            };
112        }
113
114        // Try the discovery file first.
115        if let Some(addr) = read_addr_file(&self.addr_file_path())
116            && tcp_probe(&addr)
117        {
118            let base_url = format!("http://{addr}");
119            let version = fetch_health_version(&addr);
120            return ServiceInfo {
121                id: self.id().to_string(),
122                display_name: self.display_name().to_string(),
123                status: ServiceStatus::Running,
124                version,
125                url: Some(base_url),
126                hint: None,
127            };
128        }
129
130        // Fallback: probe the well-known default port.
131        let default_addr = Self::default_addr();
132        if tcp_probe(default_addr) {
133            let base_url = format!("http://{default_addr}");
134            let version = fetch_health_version(default_addr);
135            return ServiceInfo {
136                id: self.id().to_string(),
137                display_name: self.display_name().to_string(),
138                status: ServiceStatus::Running,
139                version,
140                url: Some(base_url),
141                hint: None,
142            };
143        }
144
145        ServiceInfo {
146            id: self.id().to_string(),
147            display_name: self.display_name().to_string(),
148            status: ServiceStatus::Available,
149            version: None,
150            url: None,
151            hint: None,
152        }
153    }
154}
155
156// ─── tests ────────────────────────────────────────────────────────────────────
157
158#[cfg(test)]
159mod tests {
160    use super::super::helpers::tcp_probe;
161    use super::*;
162    use std::fs;
163    use tempfile::TempDir;
164
165    fn make_home_with_addr(rel_path: &str, content: &str) -> TempDir {
166        let tmp = TempDir::new().expect("tempdir");
167        let path = tmp.path().join(rel_path);
168        fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
169        fs::write(&path, content).expect("write addr file");
170        tmp
171    }
172
173    /// Why: stale addr file must yield Available (not Running) because TCP on
174    /// port 14997 fails. However, the ReviewConnector also probes the default
175    /// port 7880 as a fallback. If `trusty-review` is running on 7880 in the
176    /// test environment this test returns Running — which is correct behaviour.
177    /// What: creates `.trusty-review/http_addr = 127.0.0.1:14997` and calls
178    /// detect(); branches on whether the binary is present and the default port
179    /// is reachable, producing a deterministic assertion regardless of environment.
180    /// Test: this test itself.
181    #[test]
182    fn test_review_connector_with_stale_addr_file() {
183        let tmp = make_home_with_addr(".trusty-review/http_addr", "127.0.0.1:14997");
184        let connector = ReviewConnector::with_home(tmp.path().to_path_buf());
185        let info = connector.detect();
186        let binary_present = which::which("trusty-review").is_ok();
187        let default_running = tcp_probe(ReviewConnector::default_addr());
188        if !binary_present {
189            assert_eq!(info.status, ServiceStatus::Absent, "binary absent → Absent");
190        } else if default_running {
191            assert_eq!(
192                info.status,
193                ServiceStatus::Running,
194                "binary present, default port running → Running"
195            );
196        } else {
197            assert_eq!(
198                info.status,
199                ServiceStatus::Available,
200                "binary present, no running daemon → Available"
201            );
202        }
203        assert_eq!(info.id, "trusty-review");
204        assert_eq!(info.display_name, "Trusty Review");
205    }
206
207    /// Why: no addr file; result depends on whether the binary is present and
208    /// the daemon is running on the default port.
209    /// What: empty temp HOME; branches deterministically on
210    /// `which::which("trusty-review")` and a probe of the default port.
211    /// Test: this test itself.
212    #[test]
213    fn test_review_connector_no_addr_file() {
214        let tmp = TempDir::new().expect("tempdir");
215        let connector = ReviewConnector::with_home(tmp.path().to_path_buf());
216        let info = connector.detect();
217        let binary_present = which::which("trusty-review").is_ok();
218        let default_running = tcp_probe(ReviewConnector::default_addr());
219        if !binary_present {
220            assert_eq!(info.status, ServiceStatus::Absent, "binary absent → Absent");
221        } else if default_running {
222            assert_eq!(
223                info.status,
224                ServiceStatus::Running,
225                "binary present, default port running → Running"
226            );
227        } else {
228            assert_eq!(
229                info.status,
230                ServiceStatus::Available,
231                "binary present, no running daemon → Available"
232            );
233        }
234        assert!(info.url.is_none() || info.status == ServiceStatus::Running);
235    }
236}