use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::tempdir;
#[test]
fn converts_json_to_yaml_file() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.yaml");
std::fs::write(
&input,
r#"{"server":{"host":"localhost","port":8080},"debug":true}"#,
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("server:"));
assert!(rendered.contains("host: localhost"));
assert!(rendered.contains("port: 8080"));
assert!(rendered.contains("debug: true"));
}
#[test]
fn converts_toml_to_json_stdout() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
std::fs::write(&input, "[server]\nhost = \"localhost\"\nport = 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--to", "json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""server""#))
.stdout(predicate::str::contains(r#""host": "localhost""#))
.stdout(predicate::str::contains(r#""port": 8080"#));
}
#[test]
fn converts_yaml_to_toml_file() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.toml");
std::fs::write(&input, "server:\n host: localhost\n port: 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("[server]"));
assert!(rendered.contains("host = \"localhost\""));
assert!(rendered.contains("port = 8080"));
}
#[test]
fn inspect_reports_detected_format_and_root() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
std::fs::write(&input, "items:\n - one\n - two\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["inspect", input.to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("format: yaml"))
.stdout(predicate::str::contains("root: object"));
}
#[test]
fn stdout_conversion_requires_to_format() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
std::fs::write(&input, r#"{"ok":true}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains(
"--to is required when --output is not provided",
));
}
#[test]
fn toml_output_requires_object_root() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.toml");
std::fs::write(&input, r#"[1,2,3]"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains(
"TOML output requires an object at the document root",
));
}
#[test]
fn rejects_json_unsigned_integer_over_i64_range() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
std::fs::write(&input, r#"{"too_large":18446744073709551615}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--to", "yaml"])
.assert()
.failure()
.stderr(predicate::str::contains(
"JSON unsigned integer exceeds the supported i64 range",
));
}
#[test]
fn check_validates_conversion_without_writing_output() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "[server]\nhost = \"localhost\"\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"--to",
"yaml",
"--check",
"-o",
output.to_str().unwrap(),
])
.assert()
.success()
.stdout(predicate::str::contains("ok: conversion to yaml is valid"));
assert!(!output.exists());
}
#[test]
fn check_requires_explicit_output_format() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
std::fs::write(&input, "[server]\nhost = \"localhost\"\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--check"])
.assert()
.failure()
.stderr(predicate::str::contains(
"output format is required for --check",
));
}
#[test]
fn refuses_to_overwrite_existing_output_by_default() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.yaml");
std::fs::write(&input, r#"{"ok":true}"#).unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains(
"already exists; pass --overwrite to replace it",
));
assert_eq!(std::fs::read_to_string(output).unwrap(), "keep: true\n");
}
#[test]
fn overwrite_replaces_existing_output() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.yaml");
std::fs::write(&input, r#"{"ok":true}"#).unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
"--overwrite",
])
.assert()
.success();
assert_eq!(std::fs::read_to_string(output).unwrap(), "ok: true\n");
}
#[test]
fn validate_reports_valid_detected_format() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
std::fs::write(&input, "server:\n port: 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["validate", input.to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("ok: input.yaml is valid yaml"));
}
#[test]
fn converts_json5_to_json_stdout() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json5");
std::fs::write(
&input,
"{\n // local config\n server: { host: 'localhost', port: 8080, },\n}\n",
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--to", "json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""server""#))
.stdout(predicate::str::contains(r#""host": "localhost""#))
.stdout(predicate::str::contains(r#""port": 8080"#));
}
#[test]
fn json5_output_is_explicitly_unsupported() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
std::fs::write(&input, r#"{"ok":true}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--to", "json5"])
.assert()
.failure()
.stderr(predicate::str::contains(
"JSON5 output is not supported yet",
));
}
#[test]
fn validate_accepts_schema_for_supported_input_formats() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let schema = dir.path().join("schema.json");
std::fs::write(&input, "server:\n host: localhost\n port: 8080\n").unwrap();
std::fs::write(
&schema,
r#"{
"type": "object",
"required": ["server"],
"properties": {
"server": {
"type": "object",
"required": ["host", "port"],
"properties": {
"host": { "type": "string" },
"port": { "type": "integer" }
}
}
}
}"#,
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"validate",
input.to_str().unwrap(),
"--schema",
schema.to_str().unwrap(),
])
.assert()
.success()
.stdout(predicate::str::contains(
"ok: input.yaml is valid yaml and matches schema",
));
}
#[test]
fn validate_reports_schema_errors() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let schema = dir.path().join("schema.json");
std::fs::write(&input, r#"{"server":{"host":"localhost","port":"8080"}}"#).unwrap();
std::fs::write(
&schema,
r#"{
"type": "object",
"properties": {
"server": {
"type": "object",
"properties": {
"port": { "type": "integer" }
}
}
}
}"#,
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"validate",
input.to_str().unwrap(),
"--schema",
schema.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains("schema validation failed"));
}
#[test]
fn validate_reports_parse_errors() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
std::fs::write(&input, r#"{"server":]"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["validate", input.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("failed to parse JSON"));
}
#[test]
fn get_prints_scalar_value() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
std::fs::write(&input, "[server]\nport = 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "server.port"])
.assert()
.success()
.stdout("8080\n");
}
#[test]
fn get_prints_array_index_value() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
std::fs::write(&input, "items:\n - name: alpha\n - name: beta\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "items.1.name"])
.assert()
.success()
.stdout("beta\n");
}
#[test]
fn get_prints_object_as_json_by_default() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
std::fs::write(&input, "[server]\nhost = \"localhost\"\nport = 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "server"])
.assert()
.success()
.stdout(predicate::str::contains(r#""host": "localhost""#))
.stdout(predicate::str::contains(r#""port": 8080"#));
}
#[test]
fn get_supports_explicit_yaml_output() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
std::fs::write(&input, "[server]\nhost = \"localhost\"\nport = 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "server", "--to", "yaml"])
.assert()
.success()
.stdout(predicate::str::contains("host: localhost"))
.stdout(predicate::str::contains("port: 8080"));
}
#[test]
fn get_supports_explicit_toml_output_for_objects() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
std::fs::write(&input, "server:\n host: localhost\n port: 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "server", "--to", "toml"])
.assert()
.success()
.stdout(predicate::str::contains("host = \"localhost\""))
.stdout(predicate::str::contains("port = 8080"));
}
#[test]
fn get_rejects_empty_path() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
std::fs::write(&input, r#"{"server":{"port":8080}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), ""])
.assert()
.failure()
.stderr(predicate::str::contains("empty path is not supported"));
}
#[test]
fn get_reports_missing_key() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
std::fs::write(&input, r#"{"server":{"port":8080}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "server.host"])
.assert()
.failure()
.stderr(predicate::str::contains("path `server.host` not found"));
}
#[test]
fn get_reports_out_of_bounds_index() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
std::fs::write(&input, "items:\n - one\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "items.3"])
.assert()
.failure()
.stderr(predicate::str::contains(
"path `items.3` index out of bounds",
));
}
#[test]
fn get_reports_wrong_segment_type() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
std::fs::write(&input, "[server]\nport = 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "server.port.value"])
.assert()
.failure()
.stderr(predicate::str::contains(
"path segment `value` cannot be applied to integer",
));
}
#[test]
fn get_rejects_toml_output_for_scalar() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.toml");
std::fs::write(&input, "[server]\nport = 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"get",
input.to_str().unwrap(),
"server.port",
"--to",
"toml",
])
.assert()
.failure()
.stderr(predicate::str::contains(
"TOML output requires an object at the document root",
));
}
#[test]
fn set_updates_existing_object_key_to_string() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "server:\n host: localhost\n port: 8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"server.host",
"prod",
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("host: prod"));
assert!(rendered.contains("port: 8080"));
}
#[test]
fn set_supports_json_number_values() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.json");
std::fs::write(&input, r#"{"server":{"port":8080,"enabled":false}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"server.port",
"9090",
"--value-format",
"json",
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains(r#""port": 9090"#));
assert!(rendered.contains(r#""enabled": false"#));
}
#[test]
fn set_supports_json_object_values() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.json");
std::fs::write(&input, r#"{"server":{"metadata":{"old":true}}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"server.metadata",
r#"{"new":true}"#,
"--value-format",
"json",
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains(r#""metadata": {"#));
assert!(rendered.contains(r#""new": true"#));
assert!(!rendered.contains(r#""old": true"#));
}
#[test]
fn set_updates_array_item_field() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "items:\n - name: alpha\n - name: beta\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"items.0.name",
"gamma",
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("name: gamma"));
assert!(rendered.contains("name: beta"));
assert!(!rendered.contains("name: alpha"));
}
#[test]
fn set_reports_missing_path() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.json");
std::fs::write(&input, r#"{"server":{"port":8080}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"server.host",
"localhost",
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains("path `server.host` not found"));
assert!(!output.exists());
}
#[test]
fn set_refuses_to_overwrite_existing_output_by_default() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "server:\n host: localhost\n").unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"server.host",
"prod",
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains(
"already exists; pass --overwrite to replace it",
));
assert_eq!(std::fs::read_to_string(output).unwrap(), "keep: true\n");
}
#[test]
fn set_overwrites_existing_output_when_requested() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "server:\n host: localhost\n").unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"server.host",
"prod",
"-o",
output.to_str().unwrap(),
"--overwrite",
])
.assert()
.success();
assert!(
std::fs::read_to_string(output)
.unwrap()
.contains("host: prod")
);
}
#[test]
fn set_requires_output_file() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
std::fs::write(&input, "server:\n host: localhost\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["set", input.to_str().unwrap(), "server.host", "prod"])
.assert()
.failure()
.stderr(predicate::str::contains("--output <OUTPUT>"));
}
#[test]
fn delete_removes_object_key() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "server:\n port: 8080\n debug: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"delete",
input.to_str().unwrap(),
"server.debug",
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("port: 8080"));
assert!(!rendered.contains("debug"));
}
#[test]
fn delete_removes_array_index() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "items:\n - alpha\n - beta\n - gamma\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"delete",
input.to_str().unwrap(),
"items.1",
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("- alpha"));
assert!(rendered.contains("- gamma"));
assert!(!rendered.contains("- beta"));
}
#[test]
fn delete_reports_missing_path() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("output.json");
std::fs::write(&input, r#"{"server":{"port":8080}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"delete",
input.to_str().unwrap(),
"server.host",
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains("path `server.host` not found"));
assert!(!output.exists());
}
#[test]
fn delete_refuses_to_overwrite_existing_output_by_default() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "server:\n debug: true\n").unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"delete",
input.to_str().unwrap(),
"server.debug",
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains(
"already exists; pass --overwrite to replace it",
));
assert_eq!(std::fs::read_to_string(output).unwrap(), "keep: true\n");
}
#[test]
fn delete_overwrites_existing_output_when_requested() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("output.yaml");
std::fs::write(&input, "server:\n debug: true\n port: 8080\n").unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"delete",
input.to_str().unwrap(),
"server.debug",
"-o",
output.to_str().unwrap(),
"--overwrite",
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("port: 8080"));
assert!(!rendered.contains("debug"));
}
#[test]
fn delete_requires_output_file() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
std::fs::write(&input, "server:\n debug: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["delete", input.to_str().unwrap(), "server.debug"])
.assert()
.failure()
.stderr(predicate::str::contains("--output <OUTPUT>"));
}
#[test]
fn merge_recursively_combines_objects() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.yaml");
let override_file = dir.path().join("override.yaml");
let output = dir.path().join("merged.yaml");
std::fs::write(
&base,
"server:\n host: localhost\n port: 8080\ndatabase:\n pool: 5\n",
)
.unwrap();
std::fs::write(
&override_file,
"server:\n port: 9090\n ssl: true\nfeatures:\n cache: true\n",
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"merge",
base.to_str().unwrap(),
override_file.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("host: localhost"));
assert!(rendered.contains("port: 9090"));
assert!(rendered.contains("ssl: true"));
assert!(rendered.contains("pool: 5"));
assert!(rendered.contains("cache: true"));
}
#[test]
fn merge_replaces_arrays_instead_of_concatenating() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.yaml");
let override_file = dir.path().join("override.yaml");
let output = dir.path().join("merged.yaml");
std::fs::write(&base, "items:\n - alpha\n - beta\n").unwrap();
std::fs::write(&override_file, "items:\n - gamma\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"merge",
base.to_str().unwrap(),
override_file.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("- gamma"));
assert!(!rendered.contains("- alpha"));
assert!(!rendered.contains("- beta"));
}
#[test]
fn merge_supports_mixed_input_formats_and_explicit_toml_output() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.yaml");
let override_file = dir.path().join("override.json");
let output = dir.path().join("merged.out");
std::fs::write(&base, "server:\n host: localhost\n port: 8080\n").unwrap();
std::fs::write(&override_file, r#"{"server":{"port":9090}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"merge",
base.to_str().unwrap(),
override_file.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
"--to",
"toml",
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("[server]"));
assert!(rendered.contains("host = \"localhost\""));
assert!(rendered.contains("port = 9090"));
}
#[test]
fn merge_refuses_to_overwrite_existing_output_by_default() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.yaml");
let override_file = dir.path().join("override.yaml");
let output = dir.path().join("merged.yaml");
std::fs::write(&base, "server:\n host: localhost\n").unwrap();
std::fs::write(&override_file, "server:\n host: prod\n").unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"merge",
base.to_str().unwrap(),
override_file.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains(
"already exists; pass --overwrite to replace it",
));
assert_eq!(std::fs::read_to_string(output).unwrap(), "keep: true\n");
}
#[test]
fn merge_overwrites_existing_output_when_requested() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.yaml");
let override_file = dir.path().join("override.yaml");
let output = dir.path().join("merged.yaml");
std::fs::write(&base, "server:\n host: localhost\n").unwrap();
std::fs::write(&override_file, "server:\n host: prod\n").unwrap();
std::fs::write(&output, "keep: true\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"merge",
base.to_str().unwrap(),
override_file.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
"--overwrite",
])
.assert()
.success();
assert!(
std::fs::read_to_string(output)
.unwrap()
.contains("host: prod")
);
}
#[test]
fn diff_reports_added_removed_and_changed_paths() {
let dir = tempdir().unwrap();
let old = dir.path().join("old.yaml");
let new = dir.path().join("new.yaml");
std::fs::write(
&old,
"server:\n host: localhost\n port: 8080\n debug: true\n",
)
.unwrap();
std::fs::write(&new, "server:\n host: prod\n port: 9090\n timeout: 30\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["diff", old.to_str().unwrap(), new.to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("changed server.host"))
.stdout(predicate::str::contains("changed server.port"))
.stdout(predicate::str::contains("removed server.debug"))
.stdout(predicate::str::contains("added server.timeout"));
}
#[test]
fn diff_reports_array_index_paths() {
let dir = tempdir().unwrap();
let old = dir.path().join("old.yaml");
let new = dir.path().join("new.yaml");
std::fs::write(&old, "items:\n - alpha\n - beta\n").unwrap();
std::fs::write(&new, "items:\n - alpha\n - gamma\n - delta\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["diff", old.to_str().unwrap(), new.to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("changed items.1"))
.stdout(predicate::str::contains("added items.2"));
}
#[test]
fn diff_reports_no_changes_for_equal_documents() {
let dir = tempdir().unwrap();
let old = dir.path().join("old.toml");
let new = dir.path().join("new.toml");
std::fs::write(&old, "[server]\nhost = \"localhost\"\n").unwrap();
std::fs::write(&new, "[server]\nhost = \"localhost\"\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["diff", old.to_str().unwrap(), new.to_str().unwrap()])
.assert()
.success()
.stdout("no changes\n");
}
#[test]
fn converts_env_to_json() {
let dir = tempdir().unwrap();
let input = dir.path().join(".env");
std::fs::write(
&input,
"# local settings\nAPP_HOST=localhost\nAPP_PORT=8080\nAPP_DEBUG=true\n",
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--to", "json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""APP_HOST": "localhost""#))
.stdout(predicate::str::contains(r#""APP_PORT": "8080""#))
.stdout(predicate::str::contains(r#""APP_DEBUG": "true""#));
}
#[test]
fn converts_json_to_env_file() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join(".env");
std::fs::write(&input, r#"{"APP_HOST":"localhost","APP_PORT":"8080"}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("APP_HOST=localhost"));
assert!(rendered.contains("APP_PORT=8080"));
}
#[test]
fn converts_ini_to_yaml() {
let dir = tempdir().unwrap();
let input = dir.path().join("app.ini");
std::fs::write(
&input,
"[server]\nhost=localhost\nport=8080\n[client]\nname=web\n",
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--to", "yaml"])
.assert()
.success()
.stdout(predicate::str::contains("server:"))
.stdout(predicate::str::contains("host: localhost"))
.stdout(predicate::str::contains("port: '8080'"))
.stdout(predicate::str::contains("client:"))
.stdout(predicate::str::contains("name: web"));
}
#[test]
fn converts_yaml_to_ini_file() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.yaml");
let output = dir.path().join("app.ini");
std::fs::write(&input, "server:\n host: localhost\n port: '8080'\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("[server]"));
assert!(rendered.contains("host=localhost"));
assert!(rendered.contains("port=8080"));
}
#[test]
fn converts_properties_to_json_with_dot_keys_expanded() {
let dir = tempdir().unwrap();
let input = dir.path().join("app.properties");
std::fs::write(
&input,
"server.host=localhost\nserver.port=8080\nfeature.cache=true\n",
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["convert", input.to_str().unwrap(), "--to", "json"])
.assert()
.success()
.stdout(predicate::str::contains(r#""server""#))
.stdout(predicate::str::contains(r#""host": "localhost""#))
.stdout(predicate::str::contains(r#""port": "8080""#))
.stdout(predicate::str::contains(r#""cache": "true""#));
}
#[test]
fn converts_json_to_properties_file_with_dot_keys() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join("app.properties");
std::fs::write(
&input,
r#"{"server":{"host":"localhost","port":"8080"},"feature":{"cache":"true"}}"#,
)
.unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("server.host=localhost"));
assert!(rendered.contains("server.port=8080"));
assert!(rendered.contains("feature.cache=true"));
}
#[test]
fn get_reads_ini_section_path() {
let dir = tempdir().unwrap();
let input = dir.path().join("app.ini");
std::fs::write(&input, "[server]\nhost=localhost\nport=8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args(["get", input.to_str().unwrap(), "server.host"])
.assert()
.success()
.stdout("localhost\n");
}
#[test]
fn set_updates_existing_ini_key() {
let dir = tempdir().unwrap();
let input = dir.path().join("app.ini");
let output = dir.path().join("updated.ini");
std::fs::write(&input, "[server]\nhost=localhost\nport=8080\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"set",
input.to_str().unwrap(),
"server.host",
"prod",
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("[server]"));
assert!(rendered.contains("host=prod"));
assert!(rendered.contains("port=8080"));
}
#[test]
fn merge_supports_ini_and_yaml() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.ini");
let override_file = dir.path().join("override.yaml");
let output = dir.path().join("merged.yaml");
std::fs::write(&base, "[server]\nhost=localhost\nport=8080\n").unwrap();
std::fs::write(&override_file, "server:\n port: '9090'\n ssl: 'true'\n").unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"merge",
base.to_str().unwrap(),
override_file.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.success();
let rendered = std::fs::read_to_string(output).unwrap();
assert!(rendered.contains("host: localhost"));
assert!(rendered.contains("port: '9090'"));
assert!(rendered.contains("ssl: 'true'"));
}
#[test]
fn env_output_rejects_nested_objects() {
let dir = tempdir().unwrap();
let input = dir.path().join("input.json");
let output = dir.path().join(".env");
std::fs::write(&input, r#"{"server":{"host":"localhost"}}"#).unwrap();
Command::cargo_bin("config-forge")
.unwrap()
.args([
"convert",
input.to_str().unwrap(),
"-o",
output.to_str().unwrap(),
])
.assert()
.failure()
.stderr(predicate::str::contains(
"env output only supports top-level string values",
));
}