cargo-rahti 0.0.3

Create and maintain Rahti projects: cargo rahti new, cargo rahti upgrade.
//! Reading a project back, and deciding what may be written to.

use super::*;

// ---------------------------------------------------------------- Cargo.toml

/// The package name lives in `Cargo.toml` and nowhere else, so the templates
/// have to read it from there rather than from a second copy that could
/// disagree.
#[test]
fn the_package_name_is_found() {
    let manifest = "[package]\nname = \"demo-app\"\nversion = \"0.1.0\"\n\n[dependencies]\n";
    assert_eq!(package_name(manifest), Some("demo-app"));
}

/// `name` appears under `[dependencies]` entries too, and the one that
/// matters is the one under `[package]`.
#[test]
fn a_name_in_another_table_is_not_the_package() {
    let manifest = "[dependencies]\nname = \"wrong\"\n\n[package]\nname = \"right\"\n";
    assert_eq!(package_name(manifest), Some("right"));
}

#[test]
fn a_manifest_without_a_name_reads_as_none() {
    assert_eq!(package_name("[package]\nversion = \"0.1.0\"\n"), None);
}

/// A project created with `--local` has to stay pointed at the checkout: an
/// upgrade that replaced it with a published version would break a project
/// whose whole purpose is testing an unpublished one.
#[test]
fn a_local_checkout_is_recovered() {
    let manifest = "[dependencies]\nrahti = { path = \"C:/src/rahti/crates/rahti\" }\n";
    assert_eq!(local_checkout(manifest).as_deref(), Some("C:/src/rahti"));
}

#[test]
fn a_published_dependency_is_not_a_checkout() {
    assert_eq!(local_checkout("[dependencies]\nrahti = \"0.0.3\"\n"), None);
}

// -------------------------------------------------------------------- policy

/// Rewriting `Cargo.toml` would undo whatever the author added to it, and
/// would replace a working path dependency with a version that may not be
/// published at all.
#[test]
fn the_manifest_is_never_rewritten() {
    assert!(NEVER_REWRITTEN.contains(&"Cargo.toml"));
}

/// Everything else the scaffold owns has to be reachable, or a framework fix
/// never lands in an existing project.
#[test]
fn the_framework_files_are_rewritable() {
    for path in [
        "src/app/layout.rs",
        "build.rs",
        "public/js/pp-reactive-v2.min.js",
    ] {
        assert!(
            !NEVER_REWRITTEN.contains(&path),
            "{path} must be upgradable"
        );
    }
}

// ------------------------------------------------------------------ database

/// The scaffold's own config, read back the way an upgrade reads it. Written
/// as a file rather than parsed from a literal because that is the round trip
/// that actually has to hold: what `new` writes, `upgrade` must understand.
fn config_of(db: Option<Backend>) -> Result<Config, String> {
    // Uniqueness from a counter, not from the arguments: two tests may ask
    // about the same backend at the same time, and sharing a directory means
    // one of them deletes it out from under the other.
    static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
    let root = std::env::temp_dir().join(format!(
        "cargo-rahti-upgrade-db-{}-{}",
        std::process::id(),
        NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
    ));
    let _ = fs::remove_dir_all(&root);
    fs::create_dir_all(&root).expect("a temporary directory");

    let mut ledger = Ledger::new();
    ledger.insert("build.rs".to_string(), sha256::hex(b""));
    crate::new::write_config(&root, true, db, false, &ledger).expect("a written config");

    let manifest = match db {
        Some(_) => "[package]\nname = \"demo\"\n\n[dependencies]\nsea-orm = \"1\"\n",
        None => "[package]\nname = \"demo\"\n",
    };
    fs::write(root.join("Cargo.toml"), manifest).expect("a written manifest");

    let read = Config::read(&root);
    let _ = fs::remove_dir_all(&root);
    read
}

/// What `new` records, `upgrade` has to read back — otherwise an upgrade
/// regenerates a default project over somebody's database.
#[test]
fn the_backend_survives_the_round_trip() {
    for backend in [Backend::Sqlite, Backend::Postgres, Backend::MySql] {
        let config = config_of(Some(backend)).expect("a readable project");
        assert_eq!(config.db, Some(backend));
    }

    assert_eq!(config_of(None).expect("a readable project").db, None);
}

/// The same reading the build makes: a `db` object that names no backend is
/// SQLite, and no object at all is no database.
#[test]
fn an_unnamed_backend_reads_as_sqlite() {
    let value: serde_json::Value = serde_json::from_str(r#"{"db":{}}"#).unwrap();
    assert!(value.get("db").is_some_and(|d| d.is_object()));
}

/// `Cargo.toml` is never rewritten, so this is the one thing an upgrade can
/// only report. Getting the detection wrong means either a missing warning or
/// a permanent false one.
#[test]
fn a_manifest_is_checked_for_sea_orm() {
    assert!(config_of(Some(Backend::Sqlite)).unwrap().has_sea_orm);
    assert!(!config_of(None).unwrap().has_sea_orm);
}