use devforge::Config;
#[test]
fn parse_minimal_config() {
let toml = r#"
env_files = [".env"]
required_tools = ["docker", "cargo"]
[docker]
[dev]
"#;
let config: Config = toml::from_str(toml).unwrap();
assert_eq!(config.env_files.len(), 1);
assert_eq!(config.env_files[0].path, ".env");
assert!(config.env_files[0].template.is_none());
assert_eq!(config.required_tools, vec!["docker", "cargo"]);
assert!(config.docker.health_checks.is_empty());
assert!(config.dev.hooks.is_empty());
assert!(config.commands.is_empty());
}
#[test]
fn parse_full_config() {
let toml = r#"
env_files = [".env", "web/.env.local"]
required_tools = ["docker", "cargo", "node", "npm", "mprocs"]
[docker]
compose_file = "docker-compose.yml"
[[docker.health_checks]]
name = "postgres"
cmd = ["docker", "compose", "exec", "-T", "postgres", "pg_isready", "-U", "myuser"]
timeout = 30
[[docker.health_checks]]
name = "minio"
url = "http://localhost:9000/minio/health/live"
timeout = 15
[dev]
mprocs_config = "mprocs.yaml"
[[dev.hooks]]
cmd = "npm install"
cwd = "web"
[dev.hooks.condition]
missing = "web/node_modules"
[[commands]]
name = "migrate"
cmd = ["cargo", "run", "--release", "--package", "my-api", "--", "migrate"]
description = "Run database migrations"
docker = true
[[commands]]
name = "lint"
cmd = ["cargo", "clippy", "--workspace"]
description = "Run lints"
docker = false
"#;
let config: Config = toml::from_str(toml).unwrap();
assert_eq!(config.env_files.len(), 2);
assert_eq!(config.docker.compose_file, "docker-compose.yml");
assert_eq!(config.docker.health_checks.len(), 2);
let pg = &config.docker.health_checks[0];
assert_eq!(pg.name, "postgres");
assert!(pg.cmd.is_some());
assert_eq!(pg.timeout, 30);
let minio = &config.docker.health_checks[1];
assert_eq!(minio.name, "minio");
assert!(minio.url.is_some());
assert_eq!(minio.timeout, 15);
assert_eq!(config.dev.mprocs_config, "mprocs.yaml");
assert_eq!(config.dev.hooks.len(), 1);
assert_eq!(config.dev.hooks[0].cwd.as_deref(), Some("web"));
assert_eq!(
config.dev.hooks[0]
.condition
.as_ref()
.unwrap()
.missing
.as_deref(),
Some("web/node_modules")
);
assert_eq!(config.commands.len(), 2);
assert_eq!(config.commands[0].name, "migrate");
assert!(config.commands[0].docker);
assert!(!config.commands[1].docker);
}
#[test]
fn parse_empty_config() {
let config = Config::load("").unwrap();
assert!(config.env_files.is_empty());
assert!(config.required_tools.is_empty());
assert_eq!(config.docker.compose_file, "docker-compose.yml");
assert_eq!(config.dev.mprocs_config, "mprocs.yaml");
assert!(config.commands.is_empty());
}
#[test]
fn parse_tcp_health_check() {
let toml = r#"
[[docker.health_checks]]
name = "postgres"
tcp = "localhost:5432"
timeout = 10
"#;
let config = Config::load(toml).unwrap();
let check = &config.docker.health_checks[0];
assert_eq!(check.name, "postgres");
assert_eq!(check.tcp.as_deref(), Some("localhost:5432"));
assert!(check.cmd.is_none());
assert!(check.url.is_none());
assert_eq!(check.timeout, 10);
}
#[test]
fn tcp_health_check_default_timeout() {
let toml = r#"
[[docker.health_checks]]
name = "redis"
tcp = "localhost:6379"
"#;
let config = Config::load(toml).unwrap();
assert_eq!(config.docker.health_checks[0].timeout, 30);
}
#[test]
fn reject_non_http_url_scheme() {
let toml = r#"
[[docker.health_checks]]
name = "bad"
url = "ftp://localhost/file"
"#;
let err = Config::load(toml).unwrap_err().to_string();
assert!(err.contains("http:// or https://"), "got: {err}");
}
#[test]
fn reject_tcp_url_scheme() {
let toml = r#"
[[docker.health_checks]]
name = "bad"
url = "tcp://localhost:5432"
"#;
let err = Config::load(toml).unwrap_err().to_string();
assert!(err.contains("http:// or https://"), "got: {err}");
}
#[test]
fn accept_http_url() {
let toml = r#"
[[docker.health_checks]]
name = "api"
url = "http://localhost:3000/health"
"#;
let config = Config::load(toml).unwrap();
assert_eq!(
config.docker.health_checks[0].url.as_deref(),
Some("http://localhost:3000/health")
);
}
#[test]
fn accept_https_url() {
let toml = r#"
[[docker.health_checks]]
name = "api"
url = "https://localhost:3000/health"
"#;
let config = Config::load(toml).unwrap();
assert!(config.docker.health_checks[0].url.is_some());
}
#[test]
fn reject_cmd_and_url_together() {
let toml = r#"
[[docker.health_checks]]
name = "conflict"
cmd = ["echo", "hi"]
url = "http://localhost:3000"
"#;
let err = Config::load(toml).unwrap_err().to_string();
assert!(
err.contains("only one of cmd, url, or tcp"),
"got: {err}"
);
}
#[test]
fn reject_cmd_and_tcp_together() {
let toml = r#"
[[docker.health_checks]]
name = "conflict"
cmd = ["echo", "hi"]
tcp = "localhost:5432"
"#;
let err = Config::load(toml).unwrap_err().to_string();
assert!(
err.contains("only one of cmd, url, or tcp"),
"got: {err}"
);
}
#[test]
fn reject_url_and_tcp_together() {
let toml = r#"
[[docker.health_checks]]
name = "conflict"
url = "http://localhost:3000"
tcp = "localhost:5432"
"#;
let err = Config::load(toml).unwrap_err().to_string();
assert!(
err.contains("only one of cmd, url, or tcp"),
"got: {err}"
);
}
#[test]
fn reject_all_three_together() {
let toml = r#"
[[docker.health_checks]]
name = "conflict"
cmd = ["echo", "hi"]
url = "http://localhost:3000"
tcp = "localhost:5432"
"#;
let err = Config::load(toml).unwrap_err().to_string();
assert!(
err.contains("only one of cmd, url, or tcp"),
"got: {err}"
);
}
#[test]
fn reject_health_check_with_no_check_type() {
let toml = r#"
[[docker.health_checks]]
name = "empty"
"#;
let err = Config::load(toml).unwrap_err().to_string();
assert!(
err.contains("must specify one of cmd, url, or tcp"),
"got: {err}"
);
}
#[test]
fn runner_mprocs_explicit() {
let toml = r#"
[dev.runner]
type = "mprocs"
"#;
let config = Config::load(toml).unwrap();
assert!(matches!(config.dev.runner, Some(_)));
}
#[test]
fn runner_shell() {
let toml = r#"
[dev.runner]
type = "shell"
cmd = "npm run dev"
"#;
let config = Config::load(toml).unwrap();
assert!(config.dev.runner.is_some());
let debug = format!("{:?}", config.dev.runner);
assert!(debug.contains("Shell"), "got: {debug}");
assert!(debug.contains("npm run dev"), "got: {debug}");
}
#[test]
fn runner_none() {
let toml = r#"
[dev.runner]
type = "none"
"#;
let config = Config::load(toml).unwrap();
let debug = format!("{:?}", config.dev.runner);
assert!(debug.contains("None"), "got: {debug}");
}
#[test]
fn runner_shell_requires_cmd() {
let toml = r#"
[dev.runner]
type = "shell"
"#;
assert!(Config::load(toml).is_err());
}
#[test]
fn no_runner_block_infers_mprocs() {
let toml = r#"
[dev]
mprocs_config = "mprocs.yaml"
"#;
let config = Config::load(toml).unwrap();
assert!(config.dev.runner.is_none());
}
#[test]
fn runner_with_mprocs_config_backward_compat() {
let toml = r#"
[dev]
mprocs_config = "custom-mprocs.yaml"
"#;
let config = Config::load(toml).unwrap();
assert_eq!(config.dev.mprocs_config, "custom-mprocs.yaml");
assert!(config.dev.runner.is_none());
}
#[test]
fn runner_invalid_type() {
let toml = r#"
[dev.runner]
type = "docker"
"#;
assert!(Config::load(toml).is_err());
}
#[test]
fn env_files_string_shorthand() {
let toml = r#"env_files = [".env", "web/.env.local"]"#;
let config = Config::load(toml).unwrap();
assert_eq!(config.env_files.len(), 2);
assert_eq!(config.env_files[0].path, ".env");
assert!(config.env_files[0].template.is_none());
assert_eq!(config.env_files[1].path, "web/.env.local");
assert!(config.env_files[1].template.is_none());
}
#[test]
fn env_files_object_with_template() {
let toml = r#"
[[env_files]]
path = "safe-route/.env"
template = "safe-route/.env.example"
[[env_files]]
path = "safe-route/dashboard/.env.local"
template = "safe-route/dashboard/.env.example"
"#;
let config = Config::load(toml).unwrap();
assert_eq!(config.env_files.len(), 2);
assert_eq!(config.env_files[0].path, "safe-route/.env");
assert_eq!(
config.env_files[0].template.as_deref(),
Some("safe-route/.env.example")
);
assert_eq!(config.env_files[1].path, "safe-route/dashboard/.env.local");
assert_eq!(
config.env_files[1].template.as_deref(),
Some("safe-route/dashboard/.env.example")
);
}
#[test]
fn env_files_object_without_template() {
let toml = r#"
[[env_files]]
path = ".env"
"#;
let config = Config::load(toml).unwrap();
assert_eq!(config.env_files.len(), 1);
assert_eq!(config.env_files[0].path, ".env");
assert!(config.env_files[0].template.is_none());
}
#[test]
fn env_files_mixed_not_supported() {
let string_form = r#"env_files = [".env"]"#;
let object_form = r#"
[[env_files]]
path = ".env"
template = ".env.example"
"#;
assert!(Config::load(string_form).is_ok());
assert!(Config::load(object_form).is_ok());
}