cargo-rahti 0.0.8

Create and maintain Rahti projects: cargo rahti new, cargo rahti upgrade.
//! Editing files the scaffold does not own, without disturbing them.

use super::*;

// ------------------------------------------------------------- dependencies

const MANIFEST: &str = "[package]\nname = \"demo\"\nedition = \"2024\"\n\n\
                        [dependencies]\naxum = \"0.8.9\"\nrahti = \"0.0.7\"\n\n\
                        [build-dependencies]\nrahti-build = \"0.0.7\"\n";

/// The whole point: a project that gained a database compiles afterwards.
/// The addition goes inside `[dependencies]`, not into the table that happens
/// to be last in the file.
#[test]
fn a_dependency_lands_in_the_dependency_table() {
    let [(name, line), _] = t::sea_orm_dependencies(Backend::Sqlite);
    let out = with_dependency(MANIFEST, name, &line).expect("an amended manifest");

    let dependencies = out
        .split("[build-dependencies]")
        .next()
        .expect("the first table");
    assert!(dependencies.contains("sea-orm ="), "{out}");
    assert!(out.contains("sqlx-sqlite"), "{out}");

    // Everything the author had is still there, and still where it was.
    assert!(out.contains("axum = \"0.8.9\""), "{out}");
    assert!(out.contains("rahti = \"0.0.7\""), "{out}");
    assert!(out.contains("[build-dependencies]\nrahti-build"), "{out}");
    assert!(out.ends_with('\n'));
}

/// Twice is once. An upgrade run again must not write a second `sea-orm` key
/// — cargo refuses to parse a manifest with a duplicate.
#[test]
fn a_dependency_that_is_there_is_not_added_again() {
    let [(name, line), _] = t::sea_orm_dependencies(Backend::Sqlite);
    let once = with_dependency(MANIFEST, name, &line).expect("an amended manifest");
    assert_eq!(with_dependency(&once, name, &line), None);
}

/// `sea-orm-migration` starts with `sea-orm`, and a check by prefix would
/// read the migration crate as the ORM and leave the project short of one.
#[test]
fn a_longer_name_is_not_the_dependency() {
    let manifest = "[dependencies]\nsea-orm-migration = \"2\"\n";
    assert!(!declares(manifest, "sea-orm"));
    assert!(declares(manifest, "sea-orm-migration"));
}

/// The other spelling cargo accepts. A dependency declared as its own table
/// is still declared.
#[test]
fn a_dependency_table_counts_as_declared() {
    assert!(declares(
        "[dependencies.sea-orm]\nversion = \"2\"\n",
        "sea-orm"
    ));
}

/// A key written after a sub-table header belongs to the sub-table, so the
/// addition has to stop at the first header of any kind.
#[test]
fn a_sub_table_ends_the_dependency_table() {
    let manifest = "[dependencies]\naxum = \"0.8.9\"\n\n\
                    [dependencies.rahti]\nversion = \"0.0.7\"\n";
    let out = with_dependency(manifest, "sea-orm", "sea-orm = \"2\"").expect("an amended manifest");
    let before = out.split("[dependencies.rahti]").next().expect("the table");
    assert!(before.contains("sea-orm = \"2\""), "{out}");
}

/// A package may have no dependency table yet — cargo does not require one —
/// and the addition still has to land somewhere legal.
#[test]
fn a_manifest_without_the_table_gains_one() {
    let out = with_dependency("[package]\nname = \"demo\"\n", "sea-orm", "sea-orm = \"2\"")
        .expect("an amended manifest");
    assert!(out.contains("[dependencies]\nsea-orm = \"2\""), "{out}");
}

// -------------------------------------------------------------- ws feature

#[test]
fn the_ws_feature_is_folded_into_a_bare_version() {
    let out = with_ws_feature(MANIFEST)
        .expect("a readable manifest")
        .expect("an amended manifest");
    assert!(
        out.contains("rahti = { version = \"0.0.7\", features = [\"ws\"] }"),
        "{out}"
    );
    // The build dependency has no such feature, and starts with the same word.
    assert!(out.contains("rahti-build = \"0.0.7\""), "{out}");
}

/// A `--local` project points at a checkout, and has to keep pointing at it.
#[test]
fn the_ws_feature_is_folded_into_a_path_dependency() {
    let manifest = "[dependencies]\nrahti = { path = \"/checkout/crates/rahti\" }\n";
    let out = with_ws_feature(manifest)
        .expect("a readable manifest")
        .expect("an amended manifest");
    assert!(
        out.contains("rahti = { path = \"/checkout/crates/rahti\", features = [\"ws\"] }"),
        "{out}"
    );
}

/// A feature list the author already wrote is joined, not replaced.
#[test]
fn an_existing_feature_list_keeps_what_is_in_it() {
    let manifest = "[dependencies]\nrahti = { version = \"0.0.7\", features = [\"other\"] }\n";
    let out = with_ws_feature(manifest)
        .expect("a readable manifest")
        .expect("an amended manifest");
    assert!(out.contains("\"ws\""), "{out}");
    assert!(out.contains("\"other\""), "{out}");
}

#[test]
fn the_ws_feature_is_not_added_twice() {
    let manifest = "[dependencies]\nrahti = { version = \"0.0.7\", features = [\"ws\"] }\n";
    assert_eq!(
        with_ws_feature(manifest).expect("a readable manifest"),
        None
    );
}

/// The shapes this refuses rather than guesses at. Each is reported to the
/// author with the line to write, which is a better answer than a manifest
/// edited wrongly.
#[test]
fn an_unreadable_dependency_is_refused_rather_than_mangled() {
    assert!(with_ws_feature("[dependencies]\naxum = \"0.8.9\"\n").is_err());
    assert!(with_ws_feature("[dependencies.rahti]\nversion = \"0.0.7\"\n").is_err());
    assert!(
        with_ws_feature("[dependencies]\nrahti = {\n  version = \"0.0.7\",\n}\n").is_err(),
        "a dependency spread over several lines is not a value to rewrite"
    );
}

// --------------------------------------------------------------------- .env

#[test]
fn the_connection_string_is_added_above_what_is_there() {
    let existing = "AUTH_SECRET=\"abc\"\n";
    let out = with_database_url(existing, Backend::Postgres).expect("an amended file");

    assert!(out.contains("DATABASE_URL=\"postgres://"), "{out}");
    assert!(out.ends_with(existing), "{out}");
    // Idempotent, so a second upgrade does not stack a second heading.
    assert_eq!(with_database_url(&out, Backend::Postgres), None);
}

/// The author's own value is theirs, whatever backend the config names.
#[test]
fn an_existing_connection_string_is_left_alone() {
    let env = "DATABASE_URL=\"postgres://elsewhere/db\"\n";
    assert_eq!(with_database_url(env, Backend::Sqlite), None);
}