quinjet 0.0.52

A fast, live, keyboard-first Git source-control interface for the terminal
use std::cell::{Cell, RefCell};
use std::fs;
use std::path::PathBuf;

use anyhow::{Result, ensure};

use super::*;

#[test]
fn assets_match_the_release_matrix() -> Result<()> {
    for (os, arch, translated, expected) in [
        ("linux", "x86_64", false, "quinjet-linux-x86_64"),
        ("linux", "aarch64", false, "quinjet-linux-aarch64"),
        ("macos", "x86_64", false, "quinjet-macos-x86_64"),
        ("macos", "x86_64", true, "quinjet-macos-aarch64"),
        ("macos", "aarch64", false, "quinjet-macos-aarch64"),
        ("windows", "x86_64", false, "quinjet-windows-x86_64.exe"),
        ("windows", "aarch64", false, "quinjet-windows-x86_64.exe"),
    ] {
        ensure!(asset_for(os, arch, translated)? == expected);
    }
    ensure!(asset_for("linux", "riscv64", false).is_err());
    Ok(())
}

#[test]
fn release_tags_must_be_stable_semantic_versions() -> Result<()> {
    ensure!(parse_release(br#"{"tag_name":"v1.2.3"}"#)?.version == Version::new(1, 2, 3));
    ensure!(parse_release(br#"{"tag_name":"1.2.3"}"#).is_err());
    ensure!(parse_release(br#"{"tag_name":"v1.2.3-beta.1"}"#).is_err());
    ensure!(parse_release(br#"{"tag_name":"v1/2/3"}"#).is_err());
    Ok(())
}

#[test]
fn checksum_selection_is_exact_and_unique() -> Result<()> {
    let digest = "a".repeat(64);
    let document = format!(
        "{}  quinjet-linux-aarch64\n{} *dist/quinjet-linux-x86_64\n",
        "b".repeat(64),
        digest
    );
    ensure!(release_checksum(&document, "quinjet-linux-x86_64")? == digest);
    ensure!(release_checksum(&document, "quinjet-macos-aarch64").is_err());
    let duplicate = format!("{document}{}  quinjet-linux-x86_64\n", "c".repeat(64));
    ensure!(release_checksum(&duplicate, "quinjet-linux-x86_64").is_err());
    ensure!(release_checksum("nope  quinjet-linux-x86_64\n", "quinjet-linux-x86_64").is_err());
    Ok(())
}

#[test]
fn equal_or_older_release_does_not_download_or_replace() -> Result<()> {
    for tag in ["v1.2.3", "v1.2.2"] {
        let context = context("1.2.3");
        let requests = RefCell::new(Vec::new());
        let replaced = Cell::new(false);
        let result = perform_update(
            &context,
            false,
            |url, _limit| {
                requests.borrow_mut().push(url.to_owned());
                Ok(format!(r#"{{"tag_name":"{tag}"}}"#).into_bytes())
            },
            |_staged| {
                replaced.set(true);
                Ok(())
            },
        )?;
        ensure!(result.status == UpdateStatus::UpToDate);
        ensure!(requests.borrow().as_slice() == [context.api_url]);
        ensure!(!replaced.get());
    }
    Ok(())
}

#[test]
fn check_reports_available_release_without_downloading_it() -> Result<()> {
    let context = context("1.2.3");
    let requests = RefCell::new(Vec::new());
    let result = perform_update(
        &context,
        true,
        |url, _limit| {
            requests.borrow_mut().push(url.to_owned());
            Ok(br#"{"tag_name":"v1.3.0"}"#.to_vec())
        },
        |_staged| bail!("check-only mode tried to replace the executable"),
    )?;
    ensure!(result.status == UpdateStatus::Available);
    ensure!(requests.borrow().as_slice() == [context.api_url]);
    Ok(())
}

#[test]
fn update_pins_downloads_and_verifies_the_staged_bytes() -> Result<()> {
    let context = context("1.2.3");
    let binary = b"new release binary";
    let checksum = sha256(binary);
    let requests = RefCell::new(Vec::new());
    let replaced = Cell::new(false);
    let result = perform_update(
        &context,
        false,
        |url, _limit| {
            requests.borrow_mut().push(url.to_owned());
            if url == context.api_url {
                Ok(br#"{"tag_name":"v1.3.0"}"#.to_vec())
            } else if url.ends_with("/SHA256SUMS") {
                Ok(format!("{checksum}  quinjet-linux-x86_64\n").into_bytes())
            } else {
                Ok(binary.to_vec())
            }
        },
        |staged| {
            ensure!(fs::read(staged)? == binary);
            replaced.set(true);
            Ok(())
        },
    )?;
    ensure!(result.status == UpdateStatus::Updated);
    ensure!(replaced.get());
    ensure!(
        requests.borrow().as_slice()
            == [
                context.api_url.to_owned(),
                "https://example.invalid/releases/download/v1.3.0/SHA256SUMS".to_owned(),
                "https://example.invalid/releases/download/v1.3.0/quinjet-linux-x86_64".to_owned(),
            ]
    );
    Ok(())
}

#[test]
fn checksum_failure_never_invokes_the_replacer() -> Result<()> {
    let context = context("1.2.3");
    let replaced = Cell::new(false);
    let result = perform_update(
        &context,
        false,
        |url, _limit| {
            if url == context.api_url {
                Ok(br#"{"tag_name":"v1.3.0"}"#.to_vec())
            } else if url.ends_with("/SHA256SUMS") {
                Ok(format!("{}  quinjet-linux-x86_64\n", "a".repeat(64)).into_bytes())
            } else {
                Ok(b"wrong bytes".to_vec())
            }
        },
        |_staged| {
            replaced.set(true);
            Ok(())
        },
    );
    ensure!(result.is_err());
    ensure!(!replaced.get());
    Ok(())
}

#[test]
fn replacement_failure_removes_the_stage() -> Result<()> {
    let context = context("1.2.3");
    let binary = b"new release binary";
    let checksum = sha256(binary);
    let staged_path = RefCell::new(None::<PathBuf>);
    let result = perform_update(
        &context,
        false,
        |url, _limit| {
            if url == context.api_url {
                Ok(br#"{"tag_name":"v1.3.0"}"#.to_vec())
            } else if url.ends_with("/SHA256SUMS") {
                Ok(format!("{checksum}  quinjet-linux-x86_64\n").into_bytes())
            } else {
                Ok(binary.to_vec())
            }
        },
        |staged| {
            drop(staged_path.replace(Some(staged.to_path_buf())));
            bail!("simulated replacement failure")
        },
    );
    ensure!(result.is_err());
    let staged_path = staged_path
        .borrow()
        .clone()
        .context("the replacer did not receive a staged path")?;
    ensure!(!staged_path.exists());
    Ok(())
}

fn context(current_version: &str) -> UpdateContext<'_> {
    UpdateContext {
        current_version,
        os: "linux",
        arch: "x86_64",
        translated: false,
        api_url: "https://example.invalid/latest",
        releases_url: "https://example.invalid/releases",
    }
}

#[cfg(unix)]
#[test]
fn relative_q_shortcut_replaces_the_resolved_binary() -> Result<()> {
    use std::os::unix::fs::{PermissionsExt, symlink};

    let directory = tempfile::tempdir()?;
    let executable = directory.path().join("quinjet");
    let shortcut = directory.path().join("q");
    let staged = directory.path().join("staged");
    write_unix_executable(&executable, b"old")?;
    symlink("quinjet", &shortcut)?;
    fs::write(&staged, b"new")?;
    replace_executable(&shortcut, &staged)?;
    ensure!(fs::read(&executable)? == b"new");
    ensure!(fs::symlink_metadata(&shortcut)?.file_type().is_symlink());
    ensure!(fs::read_link(&shortcut)?.as_os_str() == "quinjet");
    ensure!(fs::metadata(&executable)?.permissions().mode() & 0o777 == 0o755);
    Ok(())
}

#[cfg(unix)]
#[test]
fn chained_q_shortcut_replaces_the_canonical_binary() -> Result<()> {
    use std::os::unix::fs::symlink;

    let cargo = tempfile::tempdir()?;
    let local = tempfile::tempdir()?;
    let canonical = cargo.path().join("quinjet");
    let linked = local.path().join("quinjet");
    let shortcut = local.path().join("q");
    let staged = local.path().join("staged");
    write_unix_executable(&canonical, b"old")?;
    symlink(&canonical, &linked)?;
    symlink("quinjet", &shortcut)?;
    fs::write(&staged, b"new")?;
    replace_executable(&shortcut, &staged)?;
    ensure!(fs::read(&canonical)? == b"new");
    ensure!(fs::symlink_metadata(&linked)?.file_type().is_symlink());
    ensure!(fs::symlink_metadata(&shortcut)?.file_type().is_symlink());
    ensure!(fs::read_link(&shortcut)?.as_os_str() == "quinjet");
    ensure!(fs::canonicalize(&shortcut)? == fs::canonicalize(&canonical)?);
    Ok(())
}

#[cfg(unix)]
fn write_unix_executable(path: &Path, contents: &[u8]) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    fs::write(path, contents)?;
    let mut permissions = fs::metadata(path)?.permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(path, permissions)?;
    Ok(())
}