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::gateway::sync_service_gateway_route_imports_for_root;
use crate::service::{
CreateServiceSpec, add_workspace_member, create_service_from_template, parse_exposes,
};
use crate::service_local::{find_service_root_from, service_dev_command};
use crate::util::{make_vars, repo_root};
use clap::Parser;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn collect_files_with_text(
root: &Path,
needle: &str,
out: &mut Vec<PathBuf>,
) -> crate::util::Result<()> {
for entry in fs::read_dir(root)? {
let path = entry?.path();
if path.is_dir() {
collect_files_with_text(&path, needle, out)?;
} else if path.is_file()
&& fs::read_to_string(&path)
.map(|text| text.contains(needle))
.unwrap_or(false)
{
out.push(path);
}
}
Ok(())
}
#[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 syncs_service_owned_gateway_route_imports() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let gateway_api = root.join("services/core/gateway/api");
let service_routes = root.join("services/core/auth/gateway/routes");
let service_api = root.join("services/core/auth/api");
fs::create_dir_all(&gateway_api).unwrap();
fs::create_dir_all(&service_routes).unwrap();
fs::create_dir_all(&service_api).unwrap();
fs::write(
gateway_api.join("gateway-routes.json"),
r#"{
"version": 1,
"imports": [
"routes/universal-routes.json"
],
"routes": [],
"upstreams": [],
"services": [],
"consumers": []
}
"#,
)
.unwrap();
fs::write(
service_api.join("service.proto"),
"syntax = \"proto3\";\npackage executesoft.core.auth.v1;\n",
)
.unwrap();
fs::write(
service_routes.join("auth-routes.json"),
r#"{
"version": 1,
"routes": [
{
"id": "auth-master-data-list-zones",
"public": {
"method": "GET",
"path": "/auth/master-data/zones",
"auth": "public"
},
"target": {
"protocol": "grpc",
"proto": "services/core/auth/api/service.proto",
"package": "executesoft.core.auth.v1",
"service": "AuthService",
"rpc": "ListZones",
"endpoint_env": "AUTH_GRPC_ENDPOINT"
}
}
]
}
"#,
)
.unwrap();
sync_service_gateway_route_imports_for_root(root).unwrap();
let updated = fs::read_to_string(gateway_api.join("gateway-routes.json")).unwrap();
assert!(updated.contains("\"routes/universal-routes.json\""));
assert!(updated.contains("\"../../../../services/core/auth/gateway/routes/auth-routes.json\""));
}
#[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 {}"));
}
#[test]
fn adds_rust_service_to_workspace_members() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\n \"tools/exe\",\n]\nresolver = \"3\"\n",
)
.unwrap();
let service_dir = root.join("services/learning/course");
fs::create_dir_all(&service_dir).unwrap();
add_workspace_member(root, &service_dir).unwrap();
add_workspace_member(root, &service_dir).unwrap();
let manifest = fs::read_to_string(root.join("Cargo.toml")).unwrap();
assert!(manifest.contains("\"services/learning/course\""));
assert_eq!(manifest.matches("services/learning/course").count(), 1);
}
#[test]
fn rust_template_uses_settings_module_for_config() {
let root = repo_root();
let skeleton = root.join("tools/templates/template-rust-grpc/skeleton");
assert!(
skeleton
.join("src/infrastructure/config/settings.rs")
.exists()
);
assert!(
!skeleton
.join("src/infrastructure/config/config.rs")
.exists()
);
let config_mod = fs::read_to_string(skeleton.join("src/infrastructure/config/mod.rs")).unwrap();
assert!(config_mod.contains("pub mod settings;"));
let mut stale_imports = Vec::new();
collect_files_with_text(&skeleton.join("src"), "config::config", &mut stale_imports).unwrap();
assert!(stale_imports.is_empty(), "{stale_imports:?}");
}