From f9114426cc9af57978628dffd8fa4b377ddb4c9d Mon Sep 17 00:00:00 2001 From: jasonwitty Date: Thu, 21 Aug 2025 14:42:15 -0700 Subject: [PATCH] add unit tests for profile creation and update readme --- Cargo.lock | 1 + README.md | 25 ++------ socktop/Cargo.toml | 1 + socktop/src/main.rs | 23 ++++++-- socktop/tests/profiles.rs | 118 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 socktop/tests/profiles.rs diff --git a/Cargo.lock b/Cargo.lock index e5aa16f..79207ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2237,6 +2237,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", + "tempfile", "tokio", "tokio-tungstenite", "url", diff --git a/README.md b/README.md index be2004a..e34a2e4 100644 --- a/README.md +++ b/README.md @@ -94,31 +94,18 @@ cargo build --release ./target/release/socktop ws://REMOTE_HOST:3000/ws ``` -Tip: Add ?token=... if you enable auth (see Security). +### Quick demo (no agent setup) -TLS quick start (optional, recommended on untrusted networks): - -- Start the agent with TLS enabled (default TLS port 8443). On first run it will generate a self‑signed certificate and key under your config directory. +Spin up a temporary local agent on port 3231 and connect automatically: ```bash -./target/release/socktop_agent --enableSSL --port 8443 # or: -p 8443 -# First run prints the cert and key paths, e.g.: -# socktop_agent: generated self-signed TLS certificate at /home/you/.config/socktop_agent/tls/cert.pem -# socktop_agent: private key at /home/you/.config/socktop_agent/tls/key.pem +socktop --demo ``` -- Copy the certificate file to the client machine (keep the key private on the server): +Or just run `socktop` with no arguments and pick the built‑in `demo` entry from the interactive profile list (if you have saved profiles, `demo` is appended). The demo agent: -```bash -scp /home/you/.config/socktop_agent/tls/cert.pem you@client:/tmp/socktop-agent-ca.pem -``` - -- Connect with the TUI, pinning the server cert: - -```bash -./target/release/socktop --tls-ca /tmp/socktop-agent-ca.pem wss://REMOTE_HOST:8443/ws -# Note: if you pass --tls-ca but use ws://, the client auto-upgrades to wss:// -``` +- Runs locally (`ws://127.0.0.1:3231/ws`) +- Stops automatically (you'll see "Stopped demo agent on port 3231") when you quit the TUI or press Ctrl-C --- diff --git a/socktop/Cargo.toml b/socktop/Cargo.toml index bdbcb92..1fe44fb 100644 --- a/socktop/Cargo.toml +++ b/socktop/Cargo.toml @@ -27,6 +27,7 @@ bytes = { workspace = true } [dev-dependencies] assert_cmd = "2.0" +tempfile = "3" [build-dependencies] prost-build = "0.13" diff --git a/socktop/src/main.rs b/socktop/src/main.rs index 0d3b71b..c9204b2 100644 --- a/socktop/src/main.rs +++ b/socktop/src/main.rs @@ -12,15 +12,16 @@ use profiles::{load_profiles, save_profiles, ProfileEntry, ProfileRequest, Resol use std::env; use std::io::{self, Write}; -struct ParsedArgs { +pub(crate) struct ParsedArgs { url: Option, tls_ca: Option, profile: Option, save: bool, demo: bool, + dry_run: bool, // hidden test helper: skip connecting } -fn parse_args>(args: I) -> Result { +pub(crate) fn parse_args>(args: I) -> Result { let mut it = args.into_iter(); let prog = it.next().unwrap_or_else(|| "socktop".into()); let mut url: Option = None; @@ -28,10 +29,11 @@ fn parse_args>(args: I) -> Result = None; let mut save = false; let mut demo = false; + let mut dry_run = false; while let Some(arg) = it.next() { match arg.as_str() { "-h" | "--help" => { - return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]")); + return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]\n")); } "--tls-ca" | "-t" => { tls_ca = it.next(); @@ -45,6 +47,10 @@ fn parse_args>(args: I) -> Result { demo = true; } + "--dry-run" => { + // intentionally undocumented + dry_run = true; + } _ if arg.starts_with("--tls-ca=") => { if let Some((_, v)) = arg.split_once('=') { if !v.is_empty() { @@ -74,6 +80,7 @@ fn parse_args>(args: I) -> Result Result<(), Box> { } }; let mut app = App::new(); + if parsed.dry_run { + return Ok(()); + } app.run(&url, tls_ca.as_deref()).await } @@ -246,8 +256,11 @@ fn spawn_demo_agent(port: u16) -> Result> let mut cmd = std::process::Command::new(candidate); cmd.arg("--port").arg(port.to_string()); cmd.env("SOCKTOP_ENABLE_SSL", "0"); - cmd.env("SOCKTOP_AGENT_GPU", "0"); - cmd.env("SOCKTOP_AGENT_TEMP", "0"); + + //JW: do not disable GPU and TEMP in demo mode + //cmd.env("SOCKTOP_AGENT_GPU", "0"); + //cmd.env("SOCKTOP_AGENT_TEMP", "0"); + let child = cmd.spawn()?; std::thread::sleep(std::time::Duration::from_millis(300)); Ok(DemoGuard { diff --git a/socktop/tests/profiles.rs b/socktop/tests/profiles.rs new file mode 100644 index 0000000..c1aa446 --- /dev/null +++ b/socktop/tests/profiles.rs @@ -0,0 +1,118 @@ +//! Tests for profile load/save and resolution logic (non-interactive paths only) +use std::fs; +use std::sync::Mutex; + +// Global lock to serialize tests that mutate process-wide environment variables. +static ENV_LOCK: Mutex<()> = Mutex::new(()); + +#[allow(dead_code)] // touch crate +fn touch() { + let _ = socktop::types::Metrics { + cpu_total: 0.0, + cpu_per_core: vec![], + mem_total: 0, + mem_used: 0, + swap_total: 0, + swap_used: 0, + process_count: None, + hostname: String::new(), + cpu_temp_c: None, + disks: vec![], + networks: vec![], + top_processes: vec![], + gpus: None, + }; +} + +// We re-import internal modules by copying minimal logic here because profiles.rs isn't public. +// Instead of exposing internals, we simulate profile saving through CLI invocations. + +use std::process::Command; + +fn run_socktop(args: &[&str]) -> (bool, String) { + let exe = env!("CARGO_BIN_EXE_socktop"); + let output = Command::new(exe).args(args).output().expect("run socktop"); + let ok = output.status.success(); + let text = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + (ok, text) +} + +fn config_dir() -> std::path::PathBuf { + if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") { + std::path::PathBuf::from(xdg).join("socktop") + } else { + dirs_next::config_dir() + .unwrap_or_else(|| std::path::PathBuf::from(".")) + .join("socktop") + } +} + +fn profiles_path() -> std::path::PathBuf { + config_dir().join("profiles.json") +} + +#[test] +fn test_profile_created_on_first_use() { + let _guard = ENV_LOCK.lock().unwrap(); + // Isolate config in a temp dir + let td = tempfile::tempdir().unwrap(); + std::env::set_var("XDG_CONFIG_HOME", td.path()); + // Ensure directory exists fresh + std::fs::create_dir_all(td.path().join("socktop")).unwrap(); + let _ = fs::remove_file(profiles_path()); + // Provide profile + url => should create profiles.json + let (_ok, _out) = run_socktop(&["--profile", "unittest", "ws://example:1/ws", "--dry-run"]); + // We pass --help to exit early after parsing (no network attempt) + let data = fs::read_to_string(profiles_path()).expect("profiles.json created"); + assert!( + data.contains("unittest"), + "profiles.json missing profile entry: {data}" + ); +} + +#[test] +fn test_profile_overwrite_only_when_changed() { + let _guard = ENV_LOCK.lock().unwrap(); + let td = tempfile::tempdir().unwrap(); + std::env::set_var("XDG_CONFIG_HOME", td.path()); + std::fs::create_dir_all(td.path().join("socktop")).unwrap(); + let _ = fs::remove_file(profiles_path()); + // Initial create + let (_ok, _out) = run_socktop(&["--profile", "prod", "ws://one/ws", "--dry-run"]); // create + let first = fs::read_to_string(profiles_path()).unwrap(); + // Re-run identical (should not duplicate or corrupt) + let (_ok2, _out2) = run_socktop(&["--profile", "prod", "ws://one/ws", "--dry-run"]); // identical + let second = fs::read_to_string(profiles_path()).unwrap(); + assert_eq!( + first, second, + "Profile file changed despite identical input" + ); + // Overwrite with different URL using --save (no prompt path) + let (_ok3, _out3) = run_socktop(&["--profile", "prod", "--save", "ws://two/ws", "--dry-run"]); + let third = fs::read_to_string(profiles_path()).unwrap(); + assert!(third.contains("two"), "Updated URL not written: {third}"); +} + +#[test] +fn test_profile_tls_ca_persisted() { + let _guard = ENV_LOCK.lock().unwrap(); + let td = tempfile::tempdir().unwrap(); + std::env::set_var("XDG_CONFIG_HOME", td.path()); + std::fs::create_dir_all(td.path().join("socktop")).unwrap(); + let _ = fs::remove_file(profiles_path()); + let (_ok, _out) = run_socktop(&[ + "--profile", + "secureX", + "--tls-ca", + "/tmp/cert.pem", + "wss://host/ws", + "--dry-run", + ]); + let data = fs::read_to_string(profiles_path()).unwrap(); + assert!(data.contains("secureX")); + assert!(data.contains("cert.pem")); +}