nvpn 4.1.6

CLI and daemon for Nostr VPN private mesh networks
Documentation
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use nostr_vpn_core::config::AppConfig;

#[test]
fn status_commands_do_not_migrate_legacy_config_or_create_sidecars() {
    let dir = TestDir::new();
    let config_path = dir.path().join("legacy.toml");
    let config = AppConfig::generated();
    fs::write(
        &config_path,
        config.plaintext_toml().expect("encode legacy config"),
    )
    .expect("write legacy config");
    assert!(
        AppConfig::config_file_needs_secret_migration(&config_path).expect("inspect legacy config")
    );
    let before = directory_snapshot(dir.path());
    let config_arg = config_path.to_str().expect("utf8 config path");

    for args in [
        vec![
            "status",
            "--config",
            config_arg,
            "--discover-secs",
            "0",
            "--json",
        ],
        vec!["paid-exit", "status", "--config", config_arg, "--json"],
    ] {
        let output = Command::new(env!("CARGO_BIN_EXE_nvpn"))
            .args(&args)
            .output()
            .expect("run nvpn status command");

        assert!(
            output.status.success(),
            "nvpn {args:?} failed\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
        assert_eq!(directory_snapshot(dir.path()), before, "nvpn {args:?}");
    }
}

fn directory_snapshot(path: &Path) -> BTreeMap<OsString, Vec<u8>> {
    fs::read_dir(path)
        .expect("read test directory")
        .map(|entry| {
            let entry = entry.expect("read directory entry");
            let name = entry.file_name();
            let contents = fs::read(entry.path()).expect("read directory entry contents");
            (name, contents)
        })
        .collect()
}

struct TestDir {
    path: PathBuf,
}

impl TestDir {
    fn new() -> Self {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("clock is after epoch")
            .as_nanos();
        let path = std::env::temp_dir().join(format!(
            "nvpn-status-readonly-{}-{nonce}",
            std::process::id()
        ));
        fs::create_dir_all(&path).expect("create test directory");
        Self { path }
    }

    fn path(&self) -> &Path {
        &self.path
    }
}

impl Drop for TestDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}