Release v2.1.0: backend test seam, captive-portal detection, JSON status, robustness
Features: - Introduce a Backend trait + System impl so flow/status/watch can be unit tested against a fake; add 11 connect-state-machine tests. - Captive-portal detection: status::connectivity returns Online/Portal/Offline; surfaced in status, JSON, connect notes, and a dedicated watch state. - `status --json` for bars/scripts; `profile add`/`profile remove`; detect now scores by number of in-range markers. Robustness: - Pin LC_ALL=C/LANG=C on child processes for locale-independent parsing. - Atomic config/state writes (temp + rename); 0600 config never world-readable. - Transient PSK file written to $XDG_RUNTIME_DIR when available. Fixes (from prior audit): - Feed Wi-Fi PSK to nmcli via stdin/passwd-file, never argv. - mask() no longer panics on multi-byte passwords. - Connectivity check requires HTTP 204 (no captive-portal false positives). - nmcli NAME,TYPE parsing handles escaped colons. - Strip CIDR suffix from displayed IP; PKGBUILD/Cargo version aligned (2.1.0).
This commit is contained in:
parent
8aceab7857
commit
d3c1e19ba3
17 changed files with 1662 additions and 217 deletions
110
src/tailscale.rs
110
src/tailscale.rs
|
|
@ -313,6 +313,54 @@ mod tests {
|
|||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn ts_health_is_ok_only_for_ok_variant() {
|
||||
assert!(TsHealth::Ok.is_ok());
|
||||
assert!(!TsHealth::NotInstalled.is_ok());
|
||||
assert!(!TsHealth::NeedsLogin.is_ok());
|
||||
assert!(!TsHealth::Stopped.is_ok());
|
||||
assert!(!TsHealth::ExitNodeMissing.is_ok());
|
||||
assert!(!TsHealth::ExitNodeOffline.is_ok());
|
||||
assert!(!TsHealth::Error("x".into()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ts_health_describe_covers_all_variants() {
|
||||
assert_eq!(TsHealth::Ok.describe(), "ok");
|
||||
assert!(TsHealth::NotInstalled.describe().contains("not installed"));
|
||||
assert!(TsHealth::NeedsLogin.describe().contains("not logged in"));
|
||||
assert!(TsHealth::Stopped.describe().contains("stopped"));
|
||||
assert!(TsHealth::ExitNodeMissing.describe().contains("not found"));
|
||||
assert!(TsHealth::ExitNodeOffline.describe().contains("offline"));
|
||||
let msg = TsHealth::Error("boom".into()).describe();
|
||||
assert!(msg.contains("boom"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_url_finds_https_url() {
|
||||
assert_eq!(
|
||||
extract_url("To authenticate, visit https://login.tailscale.com/a/xxx"),
|
||||
Some("https://login.tailscale.com/a/xxx".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_url_returns_none_when_no_url() {
|
||||
assert_eq!(extract_url("Waiting for login..."), None);
|
||||
assert_eq!(extract_url(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_url_picks_first_https_token() {
|
||||
let line = "Try https://first.example.com https://second.example.com";
|
||||
assert_eq!(extract_url(line), Some("https://first.example.com".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_url_does_not_match_plain_http() {
|
||||
assert_eq!(extract_url("see http://example.com for info"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_state_extraction() {
|
||||
assert_eq!(
|
||||
|
|
@ -322,6 +370,13 @@ mod tests {
|
|||
assert_eq!(backend_state(&json!({})), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_state_all_known_values() {
|
||||
for state in ["Running", "NeedsLogin", "NoState", "Stopped"] {
|
||||
assert_eq!(backend_state(&json!({"BackendState": state})), state);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_node_healthy_and_selected() {
|
||||
let v = json!({
|
||||
|
|
@ -381,4 +436,59 @@ mod tests {
|
|||
});
|
||||
assert_eq!(exit_node_state(&v, "exitnode"), (true, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_node_state_empty_peer_map() {
|
||||
let v = json!({ "BackendState": "Running", "Peer": {} });
|
||||
assert_eq!(exit_node_state(&v, "anynode"), (false, false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_node_state_no_peer_field() {
|
||||
let v = json!({ "BackendState": "Running" });
|
||||
assert_eq!(exit_node_state(&v, "anynode"), (false, false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_node_state_case_insensitive_hostname() {
|
||||
let v = json!({
|
||||
"Peer": {
|
||||
"k1": { "HostName": "MYNODE", "DNSName": "mynode.ts.net.",
|
||||
"Online": true, "ExitNode": true, "ExitNodeOption": true }
|
||||
}
|
||||
});
|
||||
let (exists, online, selected) = exit_node_state(&v, "mynode");
|
||||
assert!(exists && online && selected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_node_status_overrides_peer_online_when_selected() {
|
||||
// ExitNodeStatus.Online=false should override the peer's Online=true
|
||||
// when the peer is the currently-selected exit node.
|
||||
let v = json!({
|
||||
"ExitNodeStatus": { "Online": false },
|
||||
"Peer": {
|
||||
"k1": { "HostName": "exitnode", "DNSName": "exitnode.ts.net.",
|
||||
"Online": true, "ExitNode": true, "ExitNodeOption": true }
|
||||
}
|
||||
});
|
||||
let (exists, online, selected) = exit_node_state(&v, "exitnode");
|
||||
assert!(exists);
|
||||
assert!(
|
||||
!online,
|
||||
"ExitNodeStatus.Online=false should override peer Online"
|
||||
);
|
||||
assert!(selected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_node_state_wrong_node_name_not_matched() {
|
||||
let v = json!({
|
||||
"Peer": {
|
||||
"k1": { "HostName": "othernode", "DNSName": "othernode.ts.net.",
|
||||
"Online": true, "ExitNode": true, "ExitNodeOption": true }
|
||||
}
|
||||
});
|
||||
assert_eq!(exit_node_state(&v, "exitnode"), (false, false, false));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue