add unit tests for profile creation and update readme
This commit is contained in:
parent
8ee2a03a2c
commit
f9114426cc
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2237,6 +2237,7 @@ dependencies = [
|
|||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"url",
|
"url",
|
||||||
|
|||||||
25
README.md
25
README.md
@ -94,31 +94,18 @@ cargo build --release
|
|||||||
./target/release/socktop ws://REMOTE_HOST:3000/ws
|
./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):
|
Spin up a temporary local agent on port 3231 and connect automatically:
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./target/release/socktop_agent --enableSSL --port 8443 # or: -p 8443
|
socktop --demo
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- 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
|
- Runs locally (`ws://127.0.0.1:3231/ws`)
|
||||||
scp /home/you/.config/socktop_agent/tls/cert.pem you@client:/tmp/socktop-agent-ca.pem
|
- Stops automatically (you'll see "Stopped demo agent on port 3231") when you quit the TUI or press Ctrl-C
|
||||||
```
|
|
||||||
|
|
||||||
- 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://
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ bytes = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
prost-build = "0.13"
|
prost-build = "0.13"
|
||||||
|
|||||||
@ -12,15 +12,16 @@ use profiles::{load_profiles, save_profiles, ProfileEntry, ProfileRequest, Resol
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
struct ParsedArgs {
|
pub(crate) struct ParsedArgs {
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
tls_ca: Option<String>,
|
tls_ca: Option<String>,
|
||||||
profile: Option<String>,
|
profile: Option<String>,
|
||||||
save: bool,
|
save: bool,
|
||||||
demo: bool,
|
demo: bool,
|
||||||
|
dry_run: bool, // hidden test helper: skip connecting
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
||||||
let mut it = args.into_iter();
|
let mut it = args.into_iter();
|
||||||
let prog = it.next().unwrap_or_else(|| "socktop".into());
|
let prog = it.next().unwrap_or_else(|| "socktop".into());
|
||||||
let mut url: Option<String> = None;
|
let mut url: Option<String> = None;
|
||||||
@ -28,10 +29,11 @@ fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, Str
|
|||||||
let mut profile: Option<String> = None;
|
let mut profile: Option<String> = None;
|
||||||
let mut save = false;
|
let mut save = false;
|
||||||
let mut demo = false;
|
let mut demo = false;
|
||||||
|
let mut dry_run = false;
|
||||||
while let Some(arg) = it.next() {
|
while let Some(arg) = it.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-h" | "--help" => {
|
"-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" | "-t" => {
|
||||||
tls_ca = it.next();
|
tls_ca = it.next();
|
||||||
@ -45,6 +47,10 @@ fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, Str
|
|||||||
"--demo" => {
|
"--demo" => {
|
||||||
demo = true;
|
demo = true;
|
||||||
}
|
}
|
||||||
|
"--dry-run" => {
|
||||||
|
// intentionally undocumented
|
||||||
|
dry_run = true;
|
||||||
|
}
|
||||||
_ if arg.starts_with("--tls-ca=") => {
|
_ if arg.starts_with("--tls-ca=") => {
|
||||||
if let Some((_, v)) = arg.split_once('=') {
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
if !v.is_empty() {
|
if !v.is_empty() {
|
||||||
@ -74,6 +80,7 @@ fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, Str
|
|||||||
profile,
|
profile,
|
||||||
save,
|
save,
|
||||||
demo,
|
demo,
|
||||||
|
dry_run,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,6 +207,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
if parsed.dry_run {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
app.run(&url, tls_ca.as_deref()).await
|
app.run(&url, tls_ca.as_deref()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,8 +256,11 @@ fn spawn_demo_agent(port: u16) -> Result<DemoGuard, Box<dyn std::error::Error>>
|
|||||||
let mut cmd = std::process::Command::new(candidate);
|
let mut cmd = std::process::Command::new(candidate);
|
||||||
cmd.arg("--port").arg(port.to_string());
|
cmd.arg("--port").arg(port.to_string());
|
||||||
cmd.env("SOCKTOP_ENABLE_SSL", "0");
|
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()?;
|
let child = cmd.spawn()?;
|
||||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||||
Ok(DemoGuard {
|
Ok(DemoGuard {
|
||||||
|
|||||||
118
socktop/tests/profiles.rs
Normal file
118
socktop/tests/profiles.rs
Normal file
@ -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"));
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user