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 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>"));
}