use secretspec_derive::declare_secrets;
mod basic_generation {
use super::*;
declare_secrets!("tests/fixtures/basic.toml");
#[test]
fn test_struct_fields_exist() {
fn _test_field_types(s: SecretSpec) {
let _: String = s.api_key;
let _: String = s.database_url;
let _: String = s.optional_secret; }
}
}
mod prompt_missing {
use super::*;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
declare_secrets!("tests/fixtures/basic.toml");
const CHILD_CASE_VAR: &str = "SECRETSPEC_DERIVE_PROMPT_MISSING_CHILD";
fn run_in_isolated_project(case: &str, env_file: Option<&str>) -> bool {
let project = TempDir::new().expect("create isolated project");
fs::write(
project.path().join("secretspec.toml"),
include_str!("fixtures/basic.toml"),
)
.expect("write project manifest");
if let Some(contents) = env_file {
fs::write(project.path().join(".env"), contents).expect("write dotenv provider");
}
Command::new(std::env::current_exe().expect("locate integration test binary"))
.args([&format!("prompt_missing::{case}"), "--exact", "--nocapture"])
.current_dir(project.path())
.env(CHILD_CASE_VAR, "1")
.env("HOME", project.path())
.env("XDG_CONFIG_HOME", project.path())
.env_remove("SECRETSPEC_PROFILE")
.env_remove("SECRETSPEC_PROVIDER")
.status()
.expect("run isolated child test")
.success()
}
#[test]
fn prompt_missing_load_succeeds_when_nothing_is_missing() {
if std::env::var_os(CHILD_CASE_VAR).is_none() {
assert!(run_in_isolated_project(
"prompt_missing_load_succeeds_when_nothing_is_missing",
Some("API_KEY=key-value\nDATABASE_URL=postgres://localhost/db\n"),
));
return;
}
let resolved = SecretSpec::builder()
.with_provider("dotenv://.env")
.with_reason("integration test")
.prompt_missing(true)
.load()
.expect("load with prompt_missing(true) and no missing secrets");
assert_eq!(resolved.secrets.api_key, "key-value");
assert_eq!(resolved.secrets.database_url, "postgres://localhost/db");
}
#[test]
fn default_fails_fast_on_missing_secret() {
if std::env::var_os(CHILD_CASE_VAR).is_none() {
assert!(run_in_isolated_project(
"default_fails_fast_on_missing_secret",
None
));
return;
}
let result = SecretSpec::builder()
.with_provider("dotenv://.env")
.with_reason("integration test")
.load();
assert!(matches!(
result,
Err(secretspec::SecretSpecError::RequiredSecretMissing(_))
));
}
#[test]
fn prompt_missing_without_a_terminal_fails_with_required_secret_missing() {
if std::env::var_os(CHILD_CASE_VAR).is_none() {
assert!(run_in_isolated_project(
"prompt_missing_without_a_terminal_fails_with_required_secret_missing",
None,
));
return;
}
let result = SecretSpec::builder()
.with_provider("dotenv://.env")
.with_reason("integration test")
.prompt_missing(true)
.load();
assert!(matches!(
result,
Err(secretspec::SecretSpecError::RequiredSecretMissing(_))
));
}
}
mod prompt_missing_constraint_violation {
use super::*;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
declare_secrets!("tests/fixtures/constraint_violation.toml");
const CHILD_CASE_VAR: &str = "SECRETSPEC_DERIVE_PROMPT_MISSING_CONSTRAINT_CHILD";
#[test]
fn prompt_missing_does_not_intercept_constraint_violations() {
if std::env::var_os(CHILD_CASE_VAR).is_none() {
let project = TempDir::new().expect("create isolated project");
fs::write(
project.path().join("secretspec.toml"),
include_str!("fixtures/constraint_violation.toml"),
)
.expect("write project manifest");
let status = Command::new(std::env::current_exe().expect("locate integration test binary"))
.args([
"prompt_missing_constraint_violation::prompt_missing_does_not_intercept_constraint_violations",
"--exact",
"--nocapture",
])
.current_dir(project.path())
.env(CHILD_CASE_VAR, "1")
.env("HOME", project.path())
.env("XDG_CONFIG_HOME", project.path())
.env_remove("SECRETSPEC_PROFILE")
.env_remove("SECRETSPEC_PROVIDER")
.status()
.expect("run isolated child test");
assert!(status.success());
return;
}
let result = SecretSpec::builder()
.with_provider("dotenv://.env")
.with_reason("integration test")
.prompt_missing(true)
.load();
assert!(matches!(
result,
Err(secretspec::SecretSpecError::ValidationFailed(_))
));
}
}
mod prompt_missing_load_profile {
use super::*;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
declare_secrets!("tests/fixtures/profiles.toml");
const CHILD_CASE_VAR: &str = "SECRETSPEC_DERIVE_PROMPT_MISSING_LOAD_PROFILE_CHILD";
#[test]
fn prompt_missing_load_profile_succeeds_when_nothing_is_missing() {
if std::env::var_os(CHILD_CASE_VAR).is_none() {
let project = TempDir::new().expect("create isolated project");
fs::write(
project.path().join("secretspec.toml"),
include_str!("fixtures/profiles.toml"),
)
.expect("write project manifest");
let status = Command::new(std::env::current_exe().expect("locate integration test binary"))
.args([
"prompt_missing_load_profile::prompt_missing_load_profile_succeeds_when_nothing_is_missing",
"--exact",
"--nocapture",
])
.current_dir(project.path())
.env(CHILD_CASE_VAR, "1")
.env("HOME", project.path())
.env("XDG_CONFIG_HOME", project.path())
.env_remove("SECRETSPEC_PROFILE")
.env_remove("SECRETSPEC_PROVIDER")
.status()
.expect("run isolated child test");
assert!(status.success());
return;
}
let resolved = SecretSpec::builder()
.with_provider("dotenv://.env")
.with_profile("development")
.with_reason("integration test")
.prompt_missing(true)
.load_profile()
.expect("load_profile with prompt_missing(true) and no missing secrets");
match resolved.secrets {
SecretSpecProfile::Development { api_key, .. } => {
assert_eq!(api_key, "dev-api-key");
}
_ => panic!("Expected Development variant"),
}
}
}
mod profile_generation {
use super::*;
declare_secrets!("tests/fixtures/profiles.toml");
#[test]
fn test_profile_enum_variants() {
let _dev = Profile::Development;
let _staging = Profile::Staging;
let _prod = Profile::Production;
}
#[test]
fn test_profile_specific_types() {
fn _test_development(profile: SecretSpecProfile) {
match profile {
SecretSpecProfile::Development {
api_key,
database_url,
redis_url,
} => {
let _: String = api_key; let _: String = database_url; let _: Option<String> = redis_url; }
_ => panic!("Expected Development variant"),
}
}
fn _test_production(profile: SecretSpecProfile) {
match profile {
SecretSpecProfile::Production {
api_key,
database_url,
redis_url,
} => {
let _: String = api_key; let _: String = database_url; let _: String = redis_url; }
_ => panic!("Expected Production variant"),
}
}
}
#[test]
fn test_union_type_fields() {
fn _test_field_types(s: SecretSpec) {
let _: String = s.api_key; let _: String = s.database_url; let _: Option<String> = s.redis_url; }
}
}
mod complex_generation {
use super::*;
declare_secrets!("tests/fixtures/complex.toml");
#[test]
fn test_complex_field_types() {
fn _test_field_types(s: SecretSpec) {
let _: String = s.always_required;
let _: String = s.required_with_default; let _: Option<String> = s.always_optional;
let _: Option<String> = s.complex_secret; let _: Option<String> = s.multi_profile; }
}
#[test]
fn test_all_profiles_generated() {
let _dev = Profile::Development;
let _staging = Profile::Staging;
let _prod = Profile::Production;
let _test = Profile::Test;
}
}
mod as_path_lifetime {
use super::*;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
declare_secrets!("tests/fixtures/as_path.toml");
const CHILD_PROCESS: &str = "SECRETSPEC_DERIVE_AS_PATH_LIFETIME_CHILD";
#[test]
fn typed_as_path_lifetime_child() {
if std::env::var_os(CHILD_PROCESS).is_none() {
return;
}
let resolved =
SecretSpec::load(Some("dotenv://.env"), None).expect("load the typed as_path secret");
let path = resolved.secrets.cert_data.clone();
assert!(
path.exists(),
"the generated loader must keep an as_path file alive while Resolved is alive"
);
drop(resolved);
assert!(
!path.exists(),
"dropping Resolved must remove its as_path temporary file"
);
}
#[test]
fn typed_as_path_file_lives_as_long_as_resolved() {
if std::env::var_os(CHILD_PROCESS).is_some() {
return;
}
let project = TempDir::new().expect("create isolated project");
fs::write(
project.path().join("secretspec.toml"),
include_str!("fixtures/as_path.toml"),
)
.expect("write project manifest");
fs::write(
project.path().join(".env"),
"CERT_DATA=certificate-content\n",
)
.expect("write dotenv provider");
let status = Command::new(std::env::current_exe().expect("locate integration test binary"))
.args([
"as_path_lifetime::typed_as_path_lifetime_child",
"--exact",
"--nocapture",
])
.current_dir(project.path())
.env(CHILD_PROCESS, "1")
.env("HOME", project.path())
.env("XDG_CONFIG_HOME", project.path())
.env_remove("SECRETSPEC_PROFILE")
.env_remove("SECRETSPEC_PROVIDER")
.status()
.expect("run isolated child test");
assert!(status.success(), "child lifetime assertion failed");
}
}
mod json_serialization {
use super::*;
declare_secrets!("tests/fixtures/basic.toml");
#[test]
fn test_secret_spec_secrets_json_serialization() {
use secretspec::Resolved;
let spec = SecretSpec {
api_key: "test_key".to_string(),
database_url: "postgres://localhost/db".to_string(),
optional_secret: "optional".to_string(),
};
let secrets_wrapper = Resolved::new(spec, "dotenv".to_string(), "production".to_string());
let json = serde_json::to_string(&secrets_wrapper).expect("Failed to serialize Resolved");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("Failed to parse JSON");
assert_eq!(parsed["provider"], "dotenv");
assert_eq!(parsed["profile"], "production");
assert_eq!(parsed["secrets"]["api_key"], "test_key");
let deserialized: Resolved<SecretSpec> =
serde_json::from_str(&json).expect("Failed to deserialize Resolved");
assert_eq!(deserialized.provider, "dotenv");
assert_eq!(deserialized.profile, "production");
assert_eq!(deserialized.secrets.api_key, "test_key");
}
}
mod profile_inheritance {
use super::*;
declare_secrets!("tests/fixtures/profile_inheritance.toml");
#[test]
fn test_profile_inheritance_compilation() {
let _default = Profile::Default;
let _dev = Profile::Development;
let _prod = Profile::Production;
let _staging = Profile::Staging;
}
#[test]
fn test_union_type_with_inheritance() {
fn _test_field_types(s: SecretSpec) {
let _: String = s.database_url;
let _: String = s.api_key;
let _: String = s.session_secret;
let _: String = s.cache_ttl;
let _: Option<String> = s.debug_mode;
let _: Option<String> = s.enable_profiling;
}
}
#[test]
fn test_profile_specific_with_inheritance() {
fn _test_default(profile: SecretSpecProfile) {
match profile {
SecretSpecProfile::Default {
database_url,
api_key,
session_secret,
cache_ttl,
} => {
let _: String = database_url; let _: String = api_key; let _: String = session_secret; let _: String = cache_ttl; }
_ => panic!("Expected Default variant"),
}
}
fn _test_development(profile: SecretSpecProfile) {
match profile {
SecretSpecProfile::Development {
database_url,
session_secret,
debug_mode,
api_key,
cache_ttl,
} => {
let _: String = database_url; let _: String = session_secret; let _: String = debug_mode; let _: String = api_key; let _: String = cache_ttl; }
_ => panic!("Expected Development variant"),
}
}
fn _test_production(profile: SecretSpecProfile) {
match profile {
SecretSpecProfile::Production {
database_url,
api_key,
session_secret,
cache_ttl,
} => {
let _: String = database_url; let _: String = api_key; let _: String = session_secret; let _: String = cache_ttl; }
_ => panic!("Expected Production variant"),
}
}
fn _test_staging(profile: SecretSpecProfile) {
match profile {
SecretSpecProfile::Staging {
database_url,
session_secret,
enable_profiling,
api_key,
cache_ttl,
} => {
let _: String = database_url; let _: String = session_secret; let _: String = enable_profiling; let _: String = api_key; let _: String = cache_ttl; }
_ => panic!("Expected Staging variant"),
}
}
}
#[test]
fn test_builder_works_with_inherited_profiles() {
let _builder = SecretSpec::builder();
let _ = SecretSpec::builder()
.with_profile("development")
.with_provider("dotenv://.env");
let _ = SecretSpec::builder()
.with_profile(Profile::Production)
.with_provider("keyring://");
let _ = SecretSpec::builder()
.with_reason("running database migrations")
.with_provider("dotenv://.env");
let _ = SecretSpec::builder()
.with_caller(
secretspec::CallerContext::new("git")
.with_operation("credential_get")
.with_resource("github.com"),
)
.with_provider("dotenv://.env");
let _ = SecretSpec::builder()
.prompt_missing(true)
.with_provider("dotenv://.env");
}
}