pg-embed-setup-unpriv 0.5.1

Initialises postgresql_embedded clusters as root while handing off filesystem work to nobody
Documentation
//! Unit tests for `pg_worker` data directory recovery and argument parsing.

use super::*;
use pg_embedded_setup_unpriv::test_support::create_partial_data_dir;
use rstest::{fixture, rstest};
use std::{
    ffi::{OsStr, OsString},
    fs,
    os::unix::ffi::OsStrExt,
};
use tempfile::{TempDir, tempdir};

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 {
    reset_data_dir(&temp_data_dir?.1)
}

#[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 initialisation to proceed.
#[rstest]
fn recover_removes_partial_initialisation(temp_data_dir: TempDataDirResult) -> R {
    let (_, p) = temp_data_dir?;
    // Create a partial data directory using the shared helper
    create_partial_data_dir(p.as_std_path())?;

    // Verify the directory is detected as invalid (missing marker)
    ensure(!has_valid_data_dir(&p)?, "partial dir should be invalid")?;

    // Recovery should remove the partial directory
    recover_invalid_data_dir(&p)?;

    // After recovery, the directory should be gone
    ensure(!p.exists(), "partial dir should be removed by recovery")
}