ferrflow 7.8.0

Universal semantic versioning for monorepos and classic repos
Documentation
use super::*;
use crate::config::types::{ChannelValue, ForgeKind};

fn build(raw: &str) -> (Config, MigrationReport) {
    build_config_from_releaserc(raw).expect("valid releaserc")
}

fn branch<'a>(cfg: &'a Config, name: &str) -> &'a BranchChannelConfig {
    cfg.workspace
        .branches
        .as_ref()
        .expect("branches present")
        .iter()
        .find(|b| b.name == name)
        .unwrap_or_else(|| panic!("branch {name} not converted"))
}

#[test]
fn tag_format_version_token_is_rewritten() {
    let (cfg, report) = build(r#"{"tagFormat": "v${version}"}"#);
    assert_eq!(cfg.workspace.tag_template.as_deref(), Some("v{{version}}"));
    assert!(report.mapped.iter().any(|m| m.contains("tagTemplate")));
}

#[test]
fn tag_format_with_prefix_and_suffix() {
    let (cfg, _) = build(r#"{"tagFormat": "release-${version}-stable"}"#);
    assert_eq!(
        cfg.workspace.tag_template.as_deref(),
        Some("release-{{version}}-stable")
    );
}

#[test]
fn branches_map_stable_and_prerelease() {
    let (cfg, _) = build(r#"{"branches": ["main", {"name": "beta", "prerelease": true}]}"#);
    assert!(matches!(
        branch(&cfg, "main").channel,
        ChannelValue::Stable(true)
    ));
    assert!(matches!(
        &branch(&cfg, "beta").channel,
        ChannelValue::Named(c) if c == "beta"
    ));
}

#[test]
fn prerelease_string_names_the_channel() {
    let (cfg, _) = build(r#"{"branches": [{"name": "next-major", "prerelease": "next"}]}"#);
    assert!(matches!(
        &branch(&cfg, "next-major").channel,
        ChannelValue::Named(c) if c == "next"
    ));
}

#[test]
fn prerelease_false_is_not_a_channel() {
    let (cfg, _) = build(r#"{"branches": [{"name": "1.x", "prerelease": false}]}"#);
    assert!(matches!(
        branch(&cfg, "1.x").channel,
        ChannelValue::Stable(false)
    ));
}

#[test]
fn a_single_branch_string_is_accepted() {
    let (cfg, _) = build(r#"{"branches": "main"}"#);
    assert!(matches!(
        branch(&cfg, "main").channel,
        ChannelValue::Stable(true)
    ));
}

#[test]
fn changelog_plugin_sets_the_package_changelog() {
    let (cfg, _) = build(
        r#"{"plugins": [["@semantic-release/changelog", {"changelogFile": "docs/CHANGES.md"}]]}"#,
    );
    assert_eq!(
        cfg.packages[0].changelog.as_deref(),
        Some("docs/CHANGES.md")
    );
}

#[test]
fn changelog_plugin_without_options_defaults_the_path() {
    let (cfg, _) = build(r#"{"plugins": ["@semantic-release/changelog"]}"#);
    assert_eq!(cfg.packages[0].changelog.as_deref(), Some("CHANGELOG.md"));
}

#[test]
fn github_plugin_sets_the_forge() {
    let (cfg, _) = build(r#"{"plugins": ["@semantic-release/github"]}"#);
    assert!(matches!(cfg.workspace.forge, ForgeKind::Github));
}

#[test]
fn gitlab_plugin_sets_the_forge() {
    let (cfg, _) = build(r#"{"plugins": ["@semantic-release/gitlab"]}"#);
    assert!(matches!(cfg.workspace.forge, ForgeKind::Gitlab));
}

#[test]
fn exec_commands_map_to_hooks() {
    let (cfg, _) = build(
        r#"{"plugins": [["@semantic-release/exec", {
            "prepareCmd": "npm run build",
            "publishCmd": "npm publish",
            "successCmd": "echo done",
            "failCmd": "echo failed",
            "verifyConditionsCmd": "npm run lint"
        }]]}"#,
    );
    let hooks = cfg.workspace.hooks.as_ref().expect("hooks present");
    assert_eq!(hooks.pre_bump.as_deref(), Some("npm run build"));
    assert_eq!(hooks.post_publish.as_deref(), Some("npm publish"));
    assert_eq!(hooks.on_success.as_deref(), Some("echo done"));
    assert_eq!(hooks.on_error.as_deref(), Some("echo failed"));
    assert_eq!(hooks.pre_release.as_deref(), Some("npm run lint"));
}

#[test]
fn custom_release_rules_warn() {
    let (_, report) = build(
        r#"{"plugins": [["@semantic-release/commit-analyzer", {
            "releaseRules": [{"type": "docs", "release": "patch"}]
        }]]}"#,
    );
    assert!(
        report.warnings.iter().any(|w| w.contains("releaseRules")),
        "expected a warning about custom releaseRules, got {:?}",
        report.warnings
    );
}

#[test]
fn default_commit_analyzer_does_not_warn() {
    let (_, report) = build(r#"{"plugins": ["@semantic-release/commit-analyzer"]}"#);
    assert!(
        !report.warnings.iter().any(|w| w.contains("releaseRules")),
        "a plain commit-analyzer should not warn about rules"
    );
}

#[test]
fn npm_plugin_warns_rather_than_mapping() {
    let (_, report) = build(r#"{"plugins": ["@semantic-release/npm"]}"#);
    assert!(report.warnings.iter().any(|w| w.contains("npm")));
}

#[test]
fn unknown_plugin_warns() {
    let (_, report) = build(r#"{"plugins": ["@some/custom-plugin"]}"#);
    assert!(
        report
            .warnings
            .iter()
            .any(|w| w.contains("@some/custom-plugin"))
    );
}

#[test]
fn repository_url_is_reported_as_ignored() {
    let (cfg, report) = build(r#"{"repositoryUrl": "https://github.com/o/r.git"}"#);
    assert!(report.ignored.iter().any(|i| i.contains("repositoryUrl")));
    assert!(cfg.workspace.tag_template.is_none());
}

#[test]
fn empty_releaserc_still_produces_a_usable_config() {
    let (cfg, report) = build("{}");
    assert_eq!(cfg.packages.len(), 1);
    assert_eq!(cfg.packages[0].path, ".");
    assert!(report.warnings.iter().any(|w| w.contains("single-package")));
}

#[test]
fn malformed_json_is_an_error() {
    assert!(build_config_from_releaserc("{ not json").is_err());
}

#[test]
fn json5_features_are_tolerated() {
    let (cfg, _) = build(
        r#"{
            "tagFormat": "v${version}",
        }"#,
    );
    assert_eq!(cfg.workspace.tag_template.as_deref(), Some("v{{version}}"));
}

#[test]
fn source_label_is_stable() {
    assert_eq!(Source::SemanticRelease.label(), "semantic-release");
}

#[test]
fn yaml_config_converts_then_migrates() {
    let yaml = "\
tagFormat: \"v${version}\"
branches:
  - main
  - name: beta
    prerelease: true
plugins:
  - \"@semantic-release/github\"
";
    let json = yaml_to_json(yaml).expect("yaml converts to json");
    let (cfg, _) = build_config_from_releaserc(&json).expect("converted json is valid");
    assert_eq!(cfg.workspace.tag_template.as_deref(), Some("v{{version}}"));
    assert!(matches!(cfg.workspace.forge, ForgeKind::Github));
    let beta = cfg
        .workspace
        .branches
        .as_ref()
        .unwrap()
        .iter()
        .find(|b| b.name == "beta")
        .unwrap();
    assert!(matches!(&beta.channel, ChannelValue::Named(c) if c == "beta"));
}

#[test]
fn yaml_to_json_produces_parseable_json() {
    let json = yaml_to_json("a: 1\nb: [x, y]\n").expect("valid yaml");
    let value: serde_json::Value = serde_json::from_str(&json).expect("valid json out");
    assert_eq!(value["a"], 1);
    assert_eq!(value["b"][1], "y");
}

#[test]
fn malformed_yaml_is_an_error() {
    assert!(yaml_to_json("plugins: [unclosed").is_err());
}