executesoft 0.2.0

ExecuteSoft repository automation CLI
use crate::cli::{Cli, Commands, DeployCommand, DevCommand, ServiceCommand};
use crate::commands::{bump_manifest_patch_version, route_import_is_stale, simple_yaml_value};
use crate::dev::{collect_service_databases, load_env_file, postgres_database_name};
use crate::service::{CreateServiceSpec, create_service_from_template, parse_exposes};
use crate::service_local::{find_service_root_from, service_dev_command};
use crate::util::make_vars;
use clap::Parser;
use std::env;
use std::fs;

#[test]
fn clap_parses_service_create() {
    let cli = Cli::try_parse_from([
        "exe",
        "service",
        "create",
        "--name",
        "orders",
        "--domain",
        "commerce",
        "--template",
        "rust",
    ])
    .unwrap();
    match cli.command {
        Commands::Service {
            command: ServiceCommand::Create(args),
        } => {
            assert_eq!(args.name, "orders");
            assert_eq!(args.domain, "commerce");
            assert_eq!(args.template_lang, "rust");
        }
        _ => panic!("unexpected command"),
    }
}

#[test]
fn maps_make_vars() {
    let vars = make_vars(&[
        "tag=latest".into(),
        "--image-tag=v1".into(),
        "--ignored".into(),
    ]);
    assert_eq!(vars, vec!["tag=latest", "IMAGE_TAG=v1"]);
}

#[test]
fn clap_parses_service_local_setup_and_dev() {
    let setup = Cli::try_parse_from(["exe", "setup", "SKIP_DOCKER=1"]).unwrap();
    match setup.command {
        Commands::Setup(args) => assert_eq!(args.vars, vec!["SKIP_DOCKER=1"]),
        _ => panic!("unexpected command"),
    }

    let dev = Cli::try_parse_from(["exe", "dev"]).unwrap();
    match dev.command {
        Commands::Dev { command } => assert!(command.is_none()),
        _ => panic!("unexpected command"),
    }
}

#[test]
fn clap_parses_explicit_dev_compose_file() {
    let cli = Cli::try_parse_from([
        "exe",
        "dev",
        "up",
        "DEV_COMPOSE_FILE=services/core/auth/development/compose.dev.yml",
    ])
    .unwrap();
    match cli.command {
        Commands::Dev {
            command: Some(DevCommand::Up(args)),
        } => assert_eq!(
            args.vars,
            vec!["DEV_COMPOSE_FILE=services/core/auth/development/compose.dev.yml"]
        ),
        _ => panic!("unexpected command"),
    }
}

#[test]
fn clap_parses_default_shared_dev_dependencies() {
    let cli = Cli::try_parse_from(["exe", "dev", "up"]).unwrap();
    match cli.command {
        Commands::Dev {
            command: Some(DevCommand::Up(args)),
        } => assert!(args.vars.is_empty()),
        _ => panic!("unexpected command"),
    }
}

#[test]
fn clap_parses_release_sync_deploy_command() {
    let cli = Cli::try_parse_from(["exe", "deploy", "release-sync"]).unwrap();
    match cli.command {
        Commands::Deploy {
            command: DeployCommand::ReleaseSync(args),
        } => assert!(args.vars.is_empty()),
        _ => panic!("unexpected command"),
    }
}

#[test]
fn clap_parses_top_level_release_sync_command() {
    let cli = Cli::try_parse_from(["exe", "release-sync"]).unwrap();
    match cli.command {
        Commands::ReleaseSync(args) => assert!(args.vars.is_empty()),
        _ => panic!("unexpected command"),
    }
}

#[test]
fn clap_parses_publish_args() {
    let cli = Cli::try_parse_from(["exe", "publish", "--allow-dirty"]).unwrap();
    match cli.command {
        Commands::Publish(args) => assert_eq!(args.vars, vec!["--allow-dirty"]),
        _ => panic!("unexpected command"),
    }
}

#[test]
fn bumps_manifest_patch_version() {
    let dir = tempfile::tempdir().unwrap();
    let manifest = dir.path().join("Cargo.toml");
    fs::write(
        &manifest,
        "[package]\nname = \"executesoft\"\nversion = \"0.1.8\"\nedition = \"2024\"\n",
    )
    .unwrap();

    let version = bump_manifest_patch_version(&manifest).unwrap();
    assert_eq!(version, "0.1.9");
    let updated = fs::read_to_string(&manifest).unwrap();
    assert!(updated.contains("version = \"0.1.9\""));
}

#[test]
fn detects_stale_gateway_route_import_when_target_proto_is_missing() {
    let dir = tempfile::tempdir().unwrap();
    let route_file = dir.path().join("routes/catalogs-routes.json");
    fs::create_dir_all(route_file.parent().unwrap()).unwrap();
    fs::write(
        &route_file,
        r#"{
  "version": 1,
  "routes": [
    {
      "id": "catalogs-create-category",
      "target": {
        "proto": "services/storexmart/catalogs/api/service.proto"
      }
    }
  ]
}
"#,
    )
    .unwrap();

    assert!(route_import_is_stale(dir.path(), &route_file).unwrap());
}

#[test]
fn parses_simple_service_metadata_values() {
    let text = "name: auth\ndomain: core\nruntime: rust\n";
    assert_eq!(simple_yaml_value(text, "domain").unwrap(), "core");
    assert_eq!(simple_yaml_value(text, "name").unwrap(), "auth");
    assert!(simple_yaml_value(text, "missing").is_none());
}

#[test]
fn finds_service_root_from_nested_directory() {
    let dir = tempfile::tempdir().unwrap();
    let service_root = dir.path().join("services/core/auth");
    let nested = service_root.join("src/bootstrap");
    fs::create_dir_all(&nested).unwrap();
    fs::write(
        service_root.join("service.yaml"),
        "name: auth\nruntime: rust\n",
    )
    .unwrap();

    assert_eq!(
        find_service_root_from(&nested).unwrap(),
        service_root.to_path_buf()
    );
}

#[test]
fn chooses_dev_command_from_service_runtime() {
    let dir = tempfile::tempdir().unwrap();
    fs::write(dir.path().join("service.yaml"), "name: auth\nruntime: go\n").unwrap();
    fs::create_dir_all(dir.path().join("cmd/server")).unwrap();
    fs::write(dir.path().join("cmd/server/main.go"), "package main\n").unwrap();

    assert_eq!(
        service_dev_command(dir.path()).unwrap(),
        vec!["go", "run", "./cmd/server"]
    );
}

#[test]
fn env_file_loader_preserves_existing_values() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("app.env.example");
    let key = format!("EXE_TEST_NATS_URL_{}", std::process::id());
    fs::write(&path, format!("{key}=nats://localhost:4222\n")).unwrap();

    unsafe { env::set_var(&key, "nats://localhost:14999") };
    load_env_file(&path);
    assert_eq!(env::var(&key).unwrap(), "nats://localhost:14999");
    unsafe { env::remove_var(&key) };
}

#[test]
fn parses_postgres_database_names_from_urls() {
    assert_eq!(
        postgres_database_name(
            "postgres://executesoft:executesoft@localhost:5432/core_auth?sslmode=disable"
        )
        .unwrap(),
        "core_auth"
    );
    assert_eq!(
        postgres_database_name(
            "postgres://executesoft:executesoft@localhost:5432/storexmart_order"
        )
        .unwrap(),
        "storexmart_order"
    );
    assert!(
        postgres_database_name(
            "postgres://executesoft:executesoft@localhost:5432/__DOMAIN_____SERVICE_NAME__"
        )
        .is_none()
    );
}

#[test]
fn discovers_service_databases_dynamically() {
    let dir = tempfile::tempdir().unwrap();
    let service = dir.path().join("services/core/new-service");
    fs::create_dir_all(service.join("configs")).unwrap();
    fs::write(
        service.join("configs/app.env.example"),
        "DATABASE_URL=postgres://executesoft:executesoft@localhost:5432/core_new_service?sslmode=disable\n",
    )
    .unwrap();
    fs::write(
        service.join("Makefile"),
        "MIGRATION_DB_NAME ?= core_new_service_events\n",
    )
    .unwrap();

    let mut databases = Vec::new();
    collect_service_databases(&dir.path().join("services"), &mut databases).unwrap();
    databases.sort();

    assert_eq!(
        databases,
        vec![
            "core_new_service".to_string(),
            "core_new_service_events".to_string()
        ]
    );
}

#[test]
fn parses_exposes() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("service.yaml");
    fs::write(&path, "name: auth\nexposes:\n  - protobuf: api/auth.proto\n  - openapi: api/openapi.yaml\nruntime: rust\n").unwrap();
    let exposes = parse_exposes(&path);
    assert_eq!(exposes.get("protobuf").unwrap(), "api/auth.proto");
    assert_eq!(exposes.get("openapi").unwrap(), "api/openapi.yaml");
}

#[test]
fn help_describes_commands() {
    let mut command = <Cli as clap::CommandFactory>::command();
    let mut help = Vec::new();
    command.write_long_help(&mut help).unwrap();
    let help = String::from_utf8(help).unwrap();
    assert!(help.contains("Create, validate, build, test, and run services"));
    assert!(help.contains("Generate and validate public gateway route artifacts"));
    assert!(help.contains("Examples:"));

    let mut service = <Cli as clap::CommandFactory>::command()
        .find_subcommand_mut("service")
        .unwrap()
        .clone();
    let mut service_help = Vec::new();
    service.write_long_help(&mut service_help).unwrap();
    let service_help = String::from_utf8(service_help).unwrap();
    assert!(service_help.contains("Validate all service roots"));
    assert!(service_help.contains("Create a service from a certified template"));
}

#[test]
fn creates_service_from_template() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    let template = root.join("tools/templates/template-go-grpc");
    let skeleton = template.join("skeleton");
    fs::create_dir_all(skeleton.join("api")).unwrap();
    fs::write(
        skeleton.join("service.yaml"),
        "name: __SERVICE_NAME__\ndomain: __DOMAIN__\nowner_team: __OWNER_TEAM__\n",
    )
    .unwrap();
    fs::write(
        skeleton.join("api/service.proto"),
        "package __PROTO_PACKAGE__;\nservice __SERVICE_CLASS__Service {}\n",
    )
    .unwrap();

    let output = root.join("services/commerce/order-service");
    create_service_from_template(CreateServiceSpec {
        root,
        template_dir: &template,
        output_dir: &output,
        service_name: "order-service",
        domain: "commerce",
        owner_team: "team-orders",
        framework: "native",
        template_name: "template-go-grpc",
        proto_package: "executesoft.commerce.order.service.v1",
        service_class: "OrderService",
    })
    .unwrap();

    let metadata = fs::read_to_string(output.join("service.yaml")).unwrap();
    assert!(metadata.contains("name: order-service"));
    assert!(metadata.contains("domain: commerce"));
    assert!(metadata.contains("owner_team: team-orders"));

    let shared =
        fs::read_to_string(root.join("contracts/protobuf/commerce-order-service.proto")).unwrap();
    assert!(shared.contains("package executesoft.commerce.order.service.v1;"));
    assert!(shared.contains("service OrderServiceService {}"));
}