use facet::Facet;
use figue::{self as args, Driver, FigueBuiltins, MockEnv, builder};
#[derive(Facet, Debug)]
struct Args {
#[facet(args::config, args::env_prefix = "APP")]
config: ServerConfig,
#[facet(flatten)]
builtins: FigueBuiltins,
}
#[derive(Facet, Debug)]
struct ServerConfig {
#[facet(default = "localhost")]
host: String,
#[facet(default = 8080)]
port: u16,
#[facet(default)]
magic_link: Option<String>,
}
#[test]
fn test_help_works_with_unknown_env_keys_strict() {
let env = MockEnv::from_pairs([
("APP__AUTH__EMAIL_FROM", "noreply@example.com"),
("APP__AUTH__MAGIC_LINK_BASE_URL", "https://example.com"),
("APP__AUTH__RP_ID", "example.com"),
("APP__AUTH__RP_NAME", "Example"),
("APP__AUTH__RP_ORIGIN", "https://example.com"),
("APP__AUTH__SMTP_HOST", "smtp.example.com"),
("APP__AUTH__SMTP_PASSWORD", "secret"),
("APP__AUTH__SMTP_USERNAME", "user"),
]);
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args(["--help"]).strict())
.env(|e| e.source(env).strict())
.build();
let driver = Driver::new(config);
let result = driver.run();
assert!(
result.is_err(),
"should be an error (Help is returned as error)"
);
let err = result.unwrap_err();
assert!(
err.is_help(),
"should be a Help error, not a parsing error. Got: {}",
err
);
let help = err.help_text().expect("should have help text");
assert!(
help.contains("--[no-]help"),
"help text should contain --[no-]help"
);
}
#[test]
fn test_help_short_flag_works_with_unknown_env_keys_strict() {
let env = MockEnv::from_pairs([("APP__UNKNOWN_FIELD", "value")]);
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args(["-h"]).strict())
.env(|e| e.source(env).strict())
.build();
let driver = Driver::new(config);
let result = driver.run();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.is_help(),
"should be a Help error with -h flag. Got: {}",
err
);
}
#[test]
fn test_help_works_with_unknown_file_keys_strict() {
let config_json = r#"{
"config": {
"auth": {
"email_from": "noreply@example.com"
}
}
}"#;
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args(["--help"]).strict())
.file(|f| f.content(config_json, "config.json").strict())
.build();
let driver = Driver::new(config);
let result = driver.run();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.is_help(),
"should be a Help error, not a parsing error. Got: {}",
err
);
}
#[test]
fn test_unknown_key_in_env_strict_mode_shows_valid_fields() {
let env = MockEnv::from_pairs([
("APP__AUTH__EMAIL_FROM", "noreply@example.com"),
("APP__AUTH__SMTP_HOST", "smtp.example.com"),
]);
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args::<[&str; 0], _>([]).strict())
.env(|e| e.source(env).strict())
.build();
let driver = Driver::new(config);
let err = driver.run().unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("auth.email_from")
|| err_str.contains("AUTH__EMAIL_FROM")
|| err_str.contains("unknown"),
"error should mention the unknown key, got: {}",
err_str
);
assert!(
err_str.contains("host")
|| err_str.contains("port")
|| err_str.contains("magic_link")
|| err_str.contains("--help")
|| err_str.contains("Valid"),
"error should show valid fields or suggest --help, got: {}",
err_str
);
}
#[test]
fn test_unknown_key_in_file_strict_mode_shows_valid_fields() {
let config_json = r#"{
"config": {
"auth": {
"email_from": "noreply@example.com",
"rp_id": "example.com"
}
}
}"#;
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args::<[&str; 0], _>([]).strict())
.file(|f| f.content(config_json, "config.json").strict())
.build();
let driver = Driver::new(config);
let err = driver.run().unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("host")
|| err_str.contains("port")
|| err_str.contains("magic_link")
|| err_str.contains("--help")
|| err_str.contains("Valid"),
"error should show valid fields or suggest --help, got: {}",
err_str
);
}
#[derive(Facet, Debug)]
struct ArgsWithConfigField {
#[facet(args::config, args::env_prefix = "APP")]
config: ServerConfig,
#[facet(flatten)]
builtins: FigueBuiltins,
}
#[derive(Facet, Debug)]
struct ArgsWithSettingsField {
#[facet(args::config, args::env_prefix = "APP")]
settings: ServerConfig,
#[facet(flatten)]
builtins: FigueBuiltins,
}
#[derive(Facet, Debug)]
struct ArgsWithRenamedConfigField {
#[facet(args::config, args::env_prefix = "APP", rename = "cfg")]
config: ServerConfig,
#[facet(flatten)]
builtins: FigueBuiltins,
}
#[derive(Facet, Debug)]
struct ArgsWithFlattenedConfigRoot {
#[facet(args::config, args::env_prefix = "APP_RUN")]
#[facet(flatten)]
run: RunConfig,
#[facet(flatten)]
builtins: FigueBuiltins,
}
#[derive(Facet, Debug)]
struct RunConfig {
#[facet(default = "default-model")]
model: String,
#[facet(default)]
tui: bool,
}
#[test]
fn test_config_path_flag_not_silently_ignored() {
let config = builder::<ArgsWithConfigField>()
.unwrap()
.cli(|cli| cli.args(["--config", "/etc/app/config.json"]).strict())
.build();
let driver = Driver::new(config);
let result = driver.run();
match result.into_result() {
Ok(output) => {
panic!(
"--config flag was silently ignored! Got success with defaults: {:?}",
output.value
);
}
Err(err) => {
let err_str = err.to_string();
assert!(
!err_str.contains("unknown flag: --config"),
"--config should be recognized as the config file path flag, got: {}",
err_str
);
}
}
}
#[test]
fn test_settings_path_flag_not_silently_ignored() {
let config = builder::<ArgsWithSettingsField>()
.unwrap()
.cli(|cli| cli.args(["--settings", "/etc/app/settings.json"]).strict())
.build();
let driver = Driver::new(config);
let result = driver.run();
match result.into_result() {
Ok(output) => {
panic!(
"--settings flag was silently ignored! Got success with defaults: {:?}",
output.value
);
}
Err(err) => {
let err_str = err.to_string();
assert!(
!err_str.contains("unknown flag: --settings"),
"--settings should be recognized as the config file path flag, got: {}",
err_str
);
}
}
}
#[test]
fn test_renamed_config_path_flag_not_silently_ignored() {
let config = builder::<ArgsWithRenamedConfigField>()
.unwrap()
.cli(|cli| cli.args(["--cfg", "/etc/app/config.json"]).strict())
.build();
let driver = Driver::new(config);
let result = driver.run();
match result.into_result() {
Ok(output) => {
panic!(
"--cfg flag was silently ignored! Got success with defaults: {:?}",
output.value
);
}
Err(err) => {
let err_str = err.to_string();
assert!(
!err_str.contains("unknown flag: --cfg"),
"--cfg should be recognized as the config file path flag, got: {}",
err_str
);
}
}
}
#[test]
fn test_unknown_cli_config_override_errors_without_strict_mode() {
let config = builder::<ArgsWithRenamedConfigField>()
.unwrap()
.cli(|cli| cli.args(["--cfg.does-not-exist", "yolo"]))
.build();
let err = Driver::new(config).run().unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("unknown flag: --cfg.does-not-exist"),
"unknown config override should be a CLI parse error, got: {}",
err_str
);
}
#[test]
fn test_kebab_case_cli_config_override_is_accepted() {
let config = builder::<ArgsWithRenamedConfigField>()
.unwrap()
.cli(|cli| cli.args(["--cfg.magic-link", "https://example.com/login"]))
.build();
let args = Driver::new(config).run().unwrap();
assert_eq!(
args.config.magic_link.as_deref(),
Some("https://example.com/login")
);
}
#[test]
fn test_unknown_flattened_config_root_flag_errors_without_strict_mode() {
let config = builder::<ArgsWithFlattenedConfigRoot>()
.unwrap()
.cli(|cli| cli.args(["--does-not-exist", "yolo"]))
.build();
let err = Driver::new(config).run().unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("unknown flag: --does-not-exist"),
"unknown flattened config root flag should be a CLI parse error, got: {}",
err_str
);
}
#[test]
fn test_unknown_namespaced_flattened_config_root_override_errors_without_strict_mode() {
let config = builder::<ArgsWithFlattenedConfigRoot>()
.unwrap()
.cli(|cli| cli.args(["--run.does-not-exist", "yolo"]))
.build();
let err = Driver::new(config).run().unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("unknown flag: --run.does-not-exist"),
"unknown namespaced flattened config root override should be a CLI parse error, got: {}",
err_str
);
}