ruzor 0.1.2

Ruzor, a 1:1-compatible Rust port of the Pyzor UDP client and server
Documentation
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use ruzor::config::{
    expand_homefile, load_access_file, load_local_whitelist, load_passwd_file, load_servers,
    read_ini_section,
};

fn temp_dir(name: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let path = std::env::temp_dir().join(format!("pyzor-config-{name}-{nanos}"));
    std::fs::create_dir_all(&path).unwrap();
    path
}

#[test]
fn passwd_loading_matches_reference_cases() {
    let dir = temp_dir("passwd");
    let passwd = dir.join("ruzord.passwd");

    assert!(load_passwd_file(dir.join("missing")).is_empty());

    std::fs::write(
        &passwd,
        "alice : alice_key\nbob : bob_key\ninvalid ; key\n# carol : carol_key\n",
    )
    .unwrap();
    assert_eq!(
        load_passwd_file(&passwd),
        HashMap::from([
            ("alice".to_string(), "alice_key".to_string()),
            ("bob".to_string(), "bob_key".to_string()),
        ])
    );

    let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn access_loading_matches_reference_rule_ordering() {
    let dir = temp_dir("access");
    let access = dir.join("ruzord.access");
    let accounts = HashMap::from([
        ("alice".to_string(), "alice_key".to_string()),
        ("bob".to_string(), "bob_key".to_string()),
    ]);
    let all = HashSet::from([
        "check".to_string(),
        "report".to_string(),
        "ping".to_string(),
        "pong".to_string(),
        "info".to_string(),
        "whitelist".to_string(),
    ]);

    let default_acl = load_access_file(dir.join("missing"), &accounts);
    assert_eq!(
        default_acl["anonymous"],
        HashSet::from([
            "check".to_string(),
            "report".to_string(),
            "ping".to_string(),
            "pong".to_string(),
            "info".to_string(),
        ])
    );

    std::fs::write(
        &access,
        "all : all : allow\nping : bob : deny\ninvalid line\nall : alice : not-allowed\n",
    )
    .unwrap();
    let acl = load_access_file(&access, &accounts);
    assert_eq!(acl["alice"], all);
    let mut bob = all.clone();
    bob.remove("ping");
    assert_eq!(acl["bob"], bob);

    let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn access_loading_matches_reference_empty_and_multi_field_cases() {
    let dir = temp_dir("access-extra");
    let access = dir.join("ruzord.access");
    let accounts = HashMap::from([
        ("alice".to_string(), "alice_key".to_string()),
        ("bob".to_string(), "bob_key".to_string()),
    ]);
    let all = HashSet::from([
        "check".to_string(),
        "report".to_string(),
        "ping".to_string(),
        "pong".to_string(),
        "info".to_string(),
        "whitelist".to_string(),
    ]);

    std::fs::write(&access, "").unwrap();
    assert!(load_access_file(&access, &accounts).is_empty());

    std::fs::write(&access, "all : alice bob: allow\n").unwrap();
    let acl = load_access_file(&access, &accounts);
    assert_eq!(acl["alice"], all);
    assert_eq!(acl["bob"], all);

    std::fs::write(&access, "ping pong : alice: allow\n").unwrap();
    let acl = load_access_file(&access, &accounts);
    assert_eq!(
        acl["alice"],
        HashSet::from(["ping".to_string(), "pong".to_string()])
    );
    assert!(!acl.contains_key("bob"));

    std::fs::write(&access, "all: alice: allow\n# all: bob : allow\n").unwrap();
    let acl = load_access_file(&access, &accounts);
    assert_eq!(acl["alice"], all);
    assert!(!acl.contains_key("bob"));

    let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn servers_loading_matches_reference_defaults_and_comments() {
    let dir = temp_dir("servers");
    let servers = dir.join("servers");

    assert_eq!(
        load_servers(dir.join("missing")),
        vec![("public.pyzor.org".to_string(), 24441)]
    );

    std::fs::write(
        &servers,
        "#ignored.pyzor.org:24441\nrandom.pyzor.org:33544\n127.1.2.45:13587\n",
    )
    .unwrap();
    assert_eq!(
        load_servers(&servers),
        vec![
            ("random.pyzor.org".to_string(), 33544),
            ("127.1.2.45".to_string(), 13587),
        ]
    );

    let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn local_whitelist_comment_stripping_matches_reference_regex() {
    let dir = temp_dir("local-whitelist");
    let whitelist = dir.join("whitelist");
    std::fs::write(
        &whitelist,
        "# leading comment\nabc # trailing comment\nabc\\# literal\n",
    )
    .unwrap();

    assert_eq!(
        load_local_whitelist(&whitelist),
        HashSet::from([
            "# leading comment".to_string(),
            "abc".to_string(),
            "abc\\# literal".to_string(),
        ])
    );

    let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn ini_option_names_are_loaded_case_insensitively_like_python_configparser() {
    let dir = temp_dir("ini-case");
    let config = dir.join("config");
    std::fs::write(
        &config,
        "[client]
ServersFile = mixed_servers
REPORTTHRESHOLD = 7
",
    )
    .unwrap();

    let values = read_ini_section(&config, "client");

    assert_eq!(
        values.get("serversfile"),
        Some(&"mixed_servers".to_string())
    );
    assert_eq!(values.get("reportthreshold"), Some(&"7".to_string()));
    assert!(!values.contains_key("ServersFile"));
    let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn expand_homefile_matches_reference_homedir_rules() {
    let home = PathBuf::from("/home/user/pyzor");

    assert_eq!(
        expand_homefile(&home, "my.file"),
        "/home/user/pyzor/my.file"
    );
    assert_eq!(expand_homefile(&home, ""), "");
    assert_eq!(
        expand_homefile(&home, "/home/user2/pyzor"),
        "/home/user2/pyzor"
    );
    let expected_home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
    assert_eq!(expand_homefile(&home, "~"), expected_home);
}