pg-embed-setup-unpriv 0.5.2

Initializes postgresql_embedded clusters with platform-appropriate setup
Documentation
//! Unit tests for `pg_worker` data directory recovery and argument parsing.

use std::{
    ffi::{OsStr, OsString},
    fs,
    os::unix::ffi::OsStrExt,
};

use pg_embedded_setup_unpriv::test_support::create_partial_data_dir;
use rstest::{fixture, rstest};
use tempfile::{TempDir, tempdir};

use super::*;

type R<T = ()> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
type TempDataDirResult = R<(TempDir, Utf8PathBuf)>;

fn ensure(is_valid: bool, msg: &str) -> R { if is_valid { Ok(()) } else { Err(msg.into()) } }

#[fixture]
fn temp_data_dir() -> TempDataDirResult {
    let temp = tempdir()?;
    let p = Utf8PathBuf::from_path_buf(temp.path().join("data"))
        .map_err(|p| format!("not UTF-8: {}", p.display()))?;
    Ok((temp, p))
}

#[test]
fn rejects_extra_argument() -> R {
    let args = ["pg_worker", "setup", "/tmp/config.json", "unexpected"].map(OsString::from);
    let err = run_worker(args.into_iter()).err().ok_or("expected error")?;
    ensure(
        err.to_string().contains("unexpected extra argument"),
        "wrong err",
    )
}

#[test]
fn parse_args_rejects_non_utf8_config_path() -> R {
    let args = [
        OsString::from("pg_worker"),
        OsString::from("setup"),
        OsStr::from_bytes(&[0x80]).to_os_string(),
    ];
    match parse_args(args.into_iter()) {
        Err(WorkerError::InvalidArgs(m)) => ensure(
            m.to_lowercase().contains("utf-8") && m.contains("config"),
            "bad msg",
        ),
        o => Err(format!("expected InvalidArgs: {o:?}").into()),
    }
}

#[rstest]
fn valid_data_dir_detected(temp_data_dir: TempDataDirResult) -> R {
    let (_, p) = temp_data_dir?;
    fs::create_dir_all(p.join("global"))?;
    fs::write(p.join(PG_FILENODE_MAP_MARKER), "")?;
    ensure(has_valid_data_dir(&p)?, "should be valid")
}

#[rstest]
fn missing_dir_is_invalid(temp_data_dir: TempDataDirResult) -> R {
    ensure(!has_valid_data_dir(&temp_data_dir?.1)?, "should be invalid")
}

#[rstest]
fn dir_without_marker_is_invalid(temp_data_dir: TempDataDirResult) -> R {
    let (_, p) = temp_data_dir?;
    fs::create_dir_all(&p)?;
    ensure(!has_valid_data_dir(&p)?, "should be invalid")
}

#[rstest]
fn reset_removes_partial(temp_data_dir: TempDataDirResult) -> R {
    let (_, p) = temp_data_dir?;
    fs::create_dir_all(p.join("x"))?;
    reset_data_dir(&p)?;
    ensure(!p.exists(), "should be removed")
}

#[rstest]
fn reset_ok_for_missing(temp_data_dir: TempDataDirResult) -> R {
    let (_temp_dir, data_dir) = temp_data_dir?;
    reset_data_dir(&data_dir)
}

#[test]
fn reset_errors_on_root() -> R {
    let e = reset_data_dir(&Utf8PathBuf::from("/"))
        .err()
        .ok_or("expected err")?;
    ensure(
        e.to_string().to_lowercase().contains("root"),
        "should mention root",
    )
}

#[rstest]
fn recover_skips_nonexistent(temp_data_dir: TempDataDirResult) -> R {
    let (_, p) = temp_data_dir?;
    recover_invalid_data_dir(&p)?;
    ensure(!p.exists(), "should not exist")
}

#[rstest]
fn recover_skips_empty_dir(temp_data_dir: TempDataDirResult) -> R {
    let (_, p) = temp_data_dir?;
    fs::create_dir_all(&p)?;
    recover_invalid_data_dir(&p)?;
    ensure(p.exists(), "empty dir should remain")
}

/// Validates the recovery scenario from issue #80: a partial data directory
/// (missing `global/pg_filenode.map`) is detected as invalid and removed,
/// allowing fresh initialization to proceed.
#[rstest]
fn recover_removes_partial_initialization(temp_data_dir: TempDataDirResult) -> R {
    let (_, p) = temp_data_dir?;
    create_partial_data_dir(p.as_std_path())?;
    ensure(!has_valid_data_dir(&p)?, "partial dir should be invalid")?;
    recover_invalid_data_dir(&p)?;
    ensure(!p.exists(), "partial dir should be removed by recovery")
}

#[rstest]
#[case("setup", "Setup")]
#[case("start", "Start")]
#[case("stop", "Stop")]
#[case("cleanup", "Cleanup")]
#[case("cleanup-full", "CleanupFull")]
fn operation_parses_known_verbs(#[case] verb: &str, #[case] expected: &str) -> R {
    // `Operation` derives only `Debug`, so compare the variant's debug name to
    // ensure the verb maps to the correct operation, not merely that it parses.
    let op =
        Operation::parse(OsStr::new(verb)).map_err(|e| format!("expected {verb} to parse: {e}"))?;
    ensure(
        format!("{op:?}") == expected,
        "verb mapped to the wrong operation",
    )
}

#[test]
fn operation_parse_rejects_unknown_verb() -> R {
    let err = Operation::parse(OsStr::new("frobnicate"))
        .err()
        .ok_or("expected error")?;
    ensure(err.to_string().contains("unknown operation"), "wrong err")
}

#[test]
fn parse_args_requires_operation_and_config() -> R {
    let missing_op = parse_args(["pg_worker"].map(OsString::from).into_iter())
        .err()
        .ok_or("expected missing operation error")?;
    ensure(
        missing_op.to_string().contains("missing operation"),
        "wrong missing-op msg",
    )?;

    let missing_cfg = parse_args(["pg_worker", "setup"].map(OsString::from).into_iter())
        .err()
        .ok_or("expected missing config error")?;
    ensure(
        missing_cfg.to_string().contains("missing config path"),
        "wrong missing-cfg msg",
    )
}