#[cfg(test)]
mod tests {
use crate::capitalize_first;
use secretspec::Config;
#[test]
fn test_capitalize_first() {
assert_eq!(capitalize_first("development"), "Development");
assert_eq!(capitalize_first("production"), "Production");
assert_eq!(capitalize_first("test"), "Test");
assert_eq!(capitalize_first(""), "");
assert_eq!(capitalize_first("a"), "A");
}
#[test]
fn test_parse_basic_config() {
let toml_str = r#"[project]
name = "test"
revision = "1.0"
[profiles.default]
API_KEY = { description = "API key", required = true }
DATABASE_URL = { description = "Database URL", required = false, default = "postgres://localhost" }
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.profiles.len(), 1);
let default_profile = &config.profiles["default"];
assert_eq!(default_profile.secrets.len(), 2);
let api_key = &default_profile.secrets["API_KEY"];
assert_eq!(api_key.required, Some(true));
assert!(api_key.default.is_none());
let db_url = &default_profile.secrets["DATABASE_URL"];
assert_eq!(db_url.required, Some(false));
assert_eq!(db_url.default.as_deref(), Some("postgres://localhost"));
}
#[test]
fn test_parse_profile_overrides() {
let toml_str = r#"
[profiles.default]
API_KEY = { description = "API key", required = true }
[profiles.development]
API_KEY = { description = "API key", required = false, default = "dev-key" }
[profiles.production]
API_KEY = { description = "API key", required = true }
"#;
let config: Config = toml::from_str(&format!(
r#"[project]
name = "test"
revision = "1.0"
{}"#,
toml_str
))
.unwrap();
let api_key = &config.profiles["default"].secrets["API_KEY"];
assert_eq!(api_key.required, Some(true));
assert_eq!(config.profiles.len(), 3);
let dev_api_key = &config.profiles["development"].secrets["API_KEY"];
assert_eq!(dev_api_key.required, Some(false));
assert_eq!(dev_api_key.default.as_deref(), Some("dev-key"));
let prod_api_key = &config.profiles["production"].secrets["API_KEY"];
assert_eq!(prod_api_key.required, Some(true));
assert!(prod_api_key.default.is_none());
}
#[test]
fn test_field_type_determination() {
let toml_str = r#"[project]
name = "test"
revision = "1.0"
[profiles.default]
SOMETIMES_REQUIRED = { description = "Sometimes required secret", required = true }
[profiles.development]
SOMETIMES_REQUIRED = { description = "Sometimes required secret", required = false }
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let mut is_ever_optional = false;
for (_profile_name, profile_config) in &config.profiles {
if let Some(secret_config) = profile_config.secrets.get("SOMETIMES_REQUIRED") {
if secret_config.required != Some(true) || secret_config.default.is_some() {
is_ever_optional = true;
break;
}
} else {
is_ever_optional = true;
break;
}
}
assert!(
is_ever_optional,
"Field should be optional since it's optional in development"
);
}
#[test]
fn test_always_required_field() {
let toml_str = r#"[project]
name = "test"
revision = "1.0"
[profiles.default]
ALWAYS_REQUIRED = { description = "Always required secret", required = true }
[profiles.development]
ALWAYS_REQUIRED = { description = "Always required secret", required = true }
[profiles.production]
ALWAYS_REQUIRED = { description = "Always required secret", required = true }
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let secret_config = &config.profiles["default"].secrets["ALWAYS_REQUIRED"];
let mut is_ever_optional = false;
if secret_config.required != Some(true) || secret_config.default.is_some() {
is_ever_optional = true;
}
assert!(!is_ever_optional, "Field should never be optional");
}
#[test]
fn test_default_makes_optional() {
let toml_str = r#"[project]
name = "test"
revision = "1.0"
[profiles.default]
HAS_DEFAULT = { description = "Secret with default", required = true, default = "some-default" }
"#;
let config: Config = toml::from_str(toml_str).unwrap();
let secret_config = &config.profiles["default"].secrets["HAS_DEFAULT"];
let is_ever_optional =
secret_config.required != Some(true) || secret_config.default.is_some();
assert!(
is_ever_optional,
"Field with default should be treated as optional"
);
}
#[test]
fn test_is_valid_rust_identifier() {
use crate::is_valid_rust_identifier;
assert!(is_valid_rust_identifier("valid_name"));
assert!(is_valid_rust_identifier("_valid"));
assert!(is_valid_rust_identifier("Valid123"));
assert!(is_valid_rust_identifier("a"));
assert!(is_valid_rust_identifier("API_KEY"));
assert!(is_valid_rust_identifier("database_url"));
assert!(!is_valid_rust_identifier(""));
assert!(!is_valid_rust_identifier("123invalid"));
assert!(!is_valid_rust_identifier("invalid-name"));
assert!(!is_valid_rust_identifier("invalid.name"));
assert!(!is_valid_rust_identifier("invalid name"));
assert!(!is_valid_rust_identifier("invalid@name"));
assert!(!is_valid_rust_identifier("invalid#name"));
}
#[test]
fn test_validate_rust_identifiers() {
use crate::validate_rust_identifiers;
use secretspec::{Profile, Project, Secret};
use std::collections::HashMap;
let mut errors = Vec::new();
let mut valid_secrets = HashMap::new();
valid_secrets.insert(
"API_KEY".to_string(),
Secret {
description: Some("API Key".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
valid_secrets.insert(
"database_url".to_string(),
Secret {
description: Some("Database URL".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
let mut valid_profiles = HashMap::new();
valid_profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: valid_secrets,
},
);
let valid_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: valid_profiles,
providers: None,
};
validate_rust_identifiers(&valid_config, &mut errors);
assert!(
errors.is_empty(),
"Valid identifiers should not produce errors"
);
let mut invalid_secrets = HashMap::new();
invalid_secrets.insert(
"123invalid".to_string(),
Secret {
description: Some("Invalid name".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
invalid_secrets.insert(
"invalid-name".to_string(),
Secret {
description: Some("Invalid name".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
let mut invalid_profiles = HashMap::new();
invalid_profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: invalid_secrets,
},
);
let invalid_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: invalid_profiles,
providers: None,
};
errors.clear();
validate_rust_identifiers(&invalid_config, &mut errors);
assert_eq!(
errors.len(),
2,
"Should have errors for invalid identifiers"
);
let error_text = errors.join(" ");
assert!(
error_text.contains("123invalid"),
"Errors should mention 123invalid: {:?}",
errors
);
assert!(
error_text.contains("invalid-name"),
"Errors should mention invalid-name: {:?}",
errors
);
}
#[test]
fn test_validate_rust_keywords() {
use crate::validate_rust_identifiers;
use secretspec::{Profile, Project, Secret};
use std::collections::HashMap;
let mut errors = Vec::new();
let mut keyword_secrets = HashMap::new();
keyword_secrets.insert(
"fn".to_string(),
Secret {
description: Some("Function keyword".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
keyword_secrets.insert(
"struct".to_string(),
Secret {
description: Some("Struct keyword".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
keyword_secrets.insert(
"async".to_string(),
Secret {
description: Some("Async keyword".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
let mut keyword_profiles = HashMap::new();
keyword_profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: keyword_secrets,
},
);
let keyword_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: keyword_profiles,
providers: None,
};
validate_rust_identifiers(&keyword_config, &mut errors);
assert_eq!(errors.len(), 3, "Should have errors for all Rust keywords");
let error_text = errors.join(" ");
assert!(
error_text.contains("fn"),
"Should contain 'fn' keyword error: {:?}",
errors
);
assert!(
error_text.contains("struct"),
"Should contain 'struct' keyword error: {:?}",
errors
);
assert!(
error_text.contains("async"),
"Should contain 'async' keyword error: {:?}",
errors
);
}
#[test]
fn test_validate_duplicate_field_names() {
use crate::validate_rust_identifiers;
use secretspec::{Profile, Project, Secret};
use std::collections::HashMap;
let mut errors = Vec::new();
let mut duplicate_secrets = HashMap::new();
duplicate_secrets.insert(
"API_KEY".to_string(),
Secret {
description: Some("API Key upper".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
duplicate_secrets.insert(
"api_key".to_string(),
Secret {
description: Some("API Key lower".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
duplicate_secrets.insert(
"Api_Key".to_string(),
Secret {
description: Some("API Key mixed".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
let mut duplicate_profiles = HashMap::new();
duplicate_profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: duplicate_secrets,
},
);
let duplicate_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: duplicate_profiles,
providers: None,
};
validate_rust_identifiers(&duplicate_config, &mut errors);
let duplicate_errors: Vec<_> = errors
.iter()
.filter(|e| e.contains("multiple secrets"))
.collect();
assert_eq!(
duplicate_errors.len(),
2,
"Should detect duplicate field names"
);
}
#[test]
fn test_validate_profile_identifiers() {
use crate::validate_profile_identifiers;
use secretspec::{Profile, Project};
use std::collections::HashMap;
let mut errors = Vec::new();
let mut valid_profiles = HashMap::new();
valid_profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: HashMap::new(),
},
);
valid_profiles.insert(
"development".to_string(),
Profile {
defaults: None,
secrets: HashMap::new(),
},
);
valid_profiles.insert(
"production".to_string(),
Profile {
defaults: None,
secrets: HashMap::new(),
},
);
let valid_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: valid_profiles,
providers: None,
};
validate_profile_identifiers(&valid_config, &mut errors);
assert!(
errors.is_empty(),
"Valid profile names should not produce errors"
);
let mut invalid_profiles = HashMap::new();
invalid_profiles.insert(
"123invalid".to_string(),
Profile {
defaults: None,
secrets: HashMap::new(),
},
);
invalid_profiles.insert(
"invalid-name".to_string(),
Profile {
defaults: None,
secrets: HashMap::new(),
},
);
let invalid_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: invalid_profiles,
providers: None,
};
errors.clear();
validate_profile_identifiers(&invalid_config, &mut errors);
assert_eq!(
errors.len(),
2,
"Should have errors for invalid profile names"
);
assert!(errors.iter().any(|e| e.contains("123invalid")));
assert!(errors.iter().any(|e| e.contains("invalid-name")));
}
#[test]
fn test_field_name_ident() {
use crate::field_name_ident;
assert_eq!(field_name_ident("API_KEY").to_string(), "api_key");
assert_eq!(field_name_ident("DATABASE_URL").to_string(), "database_url");
assert_eq!(field_name_ident("simple").to_string(), "simple");
assert_eq!(field_name_ident("Mixed_Case").to_string(), "mixed_case");
}
#[test]
fn test_is_secret_optional() {
use crate::is_secret_optional;
use secretspec::Secret;
let required_no_default = Secret {
description: Some("Required".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
};
assert!(!is_secret_optional(&required_no_default));
let required_with_default = Secret {
description: Some("Required with default".to_string()),
required: Some(true),
default: Some("default_value".to_string()),
providers: None,
..Default::default()
};
assert!(!is_secret_optional(&required_with_default));
let not_required = Secret {
description: Some("Not required".to_string()),
required: Some(false),
default: None,
providers: None,
..Default::default()
};
assert!(is_secret_optional(¬_required));
let not_required_with_default = Secret {
description: Some("Not required with default".to_string()),
required: Some(false),
default: Some("default_value".to_string()),
providers: None,
..Default::default()
};
assert!(is_secret_optional(¬_required_with_default));
}
#[test]
fn test_is_field_optional_across_profiles() {
use crate::is_field_optional_across_profiles;
use secretspec::{Profile, Project, Secret};
use std::collections::HashMap;
let mut profiles = HashMap::new();
let mut default_secrets = HashMap::new();
default_secrets.insert(
"API_KEY".to_string(),
Secret {
description: Some("API Key".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
default_secrets.insert(
"DATABASE_URL".to_string(),
Secret {
description: Some("Database URL".to_string()),
required: Some(false),
default: None,
providers: None,
..Default::default()
},
);
profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: default_secrets,
},
);
let mut dev_secrets = HashMap::new();
dev_secrets.insert(
"API_KEY".to_string(),
Secret {
description: Some("API Key".to_string()),
required: Some(true),
default: Some("dev-key".to_string()),
providers: None,
..Default::default()
},
);
dev_secrets.insert(
"DATABASE_URL".to_string(),
Secret {
description: Some("Database URL".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
dev_secrets.insert(
"CACHE_URL".to_string(),
Secret {
description: Some("Cache URL".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
profiles.insert(
"development".to_string(),
Profile {
defaults: None,
secrets: dev_secrets,
},
);
let config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles,
providers: None,
};
assert!(!is_field_optional_across_profiles("API_KEY", &config));
assert!(is_field_optional_across_profiles("DATABASE_URL", &config));
assert!(is_field_optional_across_profiles("CACHE_URL", &config));
let mut strict_profiles = HashMap::new();
let mut strict_default = HashMap::new();
strict_default.insert(
"ALWAYS_REQUIRED".to_string(),
Secret {
description: Some("Always required".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
let mut strict_dev = HashMap::new();
strict_dev.insert(
"ALWAYS_REQUIRED".to_string(),
Secret {
description: Some("Always required".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
strict_profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: strict_default,
},
);
strict_profiles.insert(
"development".to_string(),
Profile {
defaults: None,
secrets: strict_dev,
},
);
let strict_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: strict_profiles,
providers: None,
};
assert!(!is_field_optional_across_profiles(
"ALWAYS_REQUIRED",
&strict_config
));
}
#[test]
fn test_analyze_field_types() {
use crate::analyze_field_types;
use secretspec::{Profile, Project, Secret};
use std::collections::HashMap;
let mut profiles = HashMap::new();
let mut default_secrets = HashMap::new();
default_secrets.insert(
"REQUIRED_SECRET".to_string(),
Secret {
description: Some("Always required".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
default_secrets.insert(
"OPTIONAL_SECRET".to_string(),
Secret {
description: Some("Optional".to_string()),
required: Some(false),
default: None,
providers: None,
..Default::default()
},
);
default_secrets.insert(
"DEFAULT_SECRET".to_string(),
Secret {
description: Some("Has default".to_string()),
required: Some(true),
default: Some("default_value".to_string()),
providers: None,
..Default::default()
},
);
profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: default_secrets,
},
);
let mut dev_secrets = HashMap::new();
dev_secrets.insert(
"REQUIRED_SECRET".to_string(),
Secret {
description: Some("Always required".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
dev_secrets.insert(
"DEV_ONLY_SECRET".to_string(),
Secret {
description: Some("Development only".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
profiles.insert(
"development".to_string(),
Profile {
defaults: None,
secrets: dev_secrets,
},
);
let config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles,
providers: None,
};
let field_info = analyze_field_types(&config);
assert_eq!(field_info.len(), 4);
let required_field = field_info.get("REQUIRED_SECRET").unwrap();
assert!(!required_field.is_optional);
let optional_field = field_info.get("OPTIONAL_SECRET").unwrap();
assert!(optional_field.is_optional);
let default_field = field_info.get("DEFAULT_SECRET").unwrap();
assert!(default_field.is_optional);
let dev_only_field = field_info.get("DEV_ONLY_SECRET").unwrap();
assert!(dev_only_field.is_optional);
}
#[test]
fn test_field_info_methods() {
use crate::FieldInfo;
use quote::quote;
let required_field = FieldInfo::new("API_KEY".to_string(), quote! { String }, false, false);
assert_eq!(required_field.name, "API_KEY");
assert!(!required_field.is_optional);
assert_eq!(required_field.field_name().to_string(), "api_key");
let struct_field = required_field.generate_struct_field();
let expected_struct = quote! { pub api_key: String };
assert_eq!(struct_field.to_string(), expected_struct.to_string());
let optional_field = FieldInfo::new(
"DATABASE_URL".to_string(),
quote! { Option<String> },
true,
false,
);
assert!(optional_field.is_optional);
assert_eq!(optional_field.field_name().to_string(), "database_url");
let optional_struct_field = optional_field.generate_struct_field();
let expected_optional_struct = quote! { pub database_url: Option<String> };
assert_eq!(
optional_struct_field.to_string(),
expected_optional_struct.to_string()
);
}
#[test]
fn test_profile_variant_methods() {
use crate::ProfileVariant;
let variant = ProfileVariant::new("development".to_string());
assert_eq!(variant.name, "development");
assert_eq!(variant.capitalized, "Development");
assert_eq!(variant.as_ident().to_string(), "Development");
let default_variant = ProfileVariant::new("default".to_string());
assert_eq!(default_variant.capitalized, "Default");
assert_eq!(default_variant.as_ident().to_string(), "Default");
let prod_variant = ProfileVariant::new("production".to_string());
assert_eq!(prod_variant.capitalized, "Production");
assert_eq!(prod_variant.as_ident().to_string(), "Production");
}
#[test]
fn test_get_profile_variants() {
use crate::get_profile_variants;
use std::collections::HashSet;
let empty_profiles = HashSet::new();
let variants = get_profile_variants(&empty_profiles);
assert_eq!(variants.len(), 1);
assert_eq!(variants[0].name, "default");
let mut profiles = HashSet::new();
profiles.insert("production".to_string());
profiles.insert("development".to_string());
profiles.insert("staging".to_string());
profiles.insert("default".to_string());
let variants = get_profile_variants(&profiles);
assert_eq!(variants.len(), 4);
let names: Vec<&String> = variants.iter().map(|v| &v.name).collect();
assert_eq!(
names,
vec!["default", "development", "production", "staging"]
);
assert_eq!(variants[0].capitalized, "Default");
assert_eq!(variants[1].capitalized, "Development");
assert_eq!(variants[2].capitalized, "Production");
assert_eq!(variants[3].capitalized, "Staging");
}
#[test]
fn test_validate_config_for_codegen() {
use crate::validate_config_for_codegen;
use secretspec::{Profile, Project, Secret};
use std::collections::HashMap;
let mut valid_secrets = HashMap::new();
valid_secrets.insert(
"API_KEY".to_string(),
Secret {
description: Some("API Key".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
valid_secrets.insert(
"database_url".to_string(),
Secret {
description: Some("Database URL".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
let mut valid_profiles = HashMap::new();
valid_profiles.insert(
"default".to_string(),
Profile {
defaults: None,
secrets: valid_secrets,
},
);
valid_profiles.insert(
"development".to_string(),
Profile {
defaults: None,
secrets: HashMap::new(),
},
);
let valid_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: valid_profiles,
providers: None,
};
let result = validate_config_for_codegen(&valid_config);
assert!(result.is_ok(), "Valid config should pass validation");
let mut invalid_secrets = HashMap::new();
invalid_secrets.insert(
"123invalid".to_string(),
Secret {
description: Some("Invalid name".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
invalid_secrets.insert(
"fn".to_string(),
Secret {
description: Some("Rust keyword".to_string()),
required: Some(true),
default: None,
providers: None,
..Default::default()
},
);
let mut invalid_profiles = HashMap::new();
invalid_profiles.insert(
"123invalid-profile".to_string(),
Profile {
defaults: None,
secrets: invalid_secrets,
},
);
let invalid_config = Config {
project: Project {
name: "test".to_string(),
..Default::default()
},
profiles: invalid_profiles,
providers: None,
};
let result = validate_config_for_codegen(&invalid_config);
assert!(result.is_err(), "Invalid config should fail validation");
let errors = result.unwrap_err();
assert!(!errors.is_empty(), "Should have validation errors");
let error_text = errors.join(" ");
assert!(
error_text.contains("123invalid"),
"Should contain secret validation errors: {:?}",
errors
);
assert!(
error_text.contains("fn"),
"Should contain keyword errors: {:?}",
errors
);
}
}