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}