use std::collections::BTreeMap; use crank_config::{ConfigSource, ProcessKind, parse_process}; fn config(secret: &str) -> crank_config::EffectiveConfig { let vars = [ ("CRANK_MASTER_KEY", secret), ("CRANK_SESSION_SECRET", secret), ("CRANK_PASSWORD_PEPPER", secret), ("CRANK_BOOTSTRAP_ADMIN_EMAIL", "owner@example.test"), ("CRANK_BOOTSTRAP_ADMIN_PASSWORD", secret), ] .into_iter() .map(|(key, value)| (key.to_owned(), value.to_owned())) .collect::>(); parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap() } #[test] fn secrets_are_absent_from_debug_display_and_fingerprint() { let first = config("CANARY_ONE"); let second = config("CANARY_TWO"); let rendered = format!("{first:?}"); assert!(!rendered.contains("CANARY_ONE")); assert_eq!(first.fingerprint(), second.fingerprint()); assert_eq!(first.fingerprint().len(), 64); assert!( first .fingerprint() .bytes() .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) ); } #[test] fn effective_semantics_not_input_spelling_drive_fingerprint() { let mut canonical = [ ("CRANK_MASTER_KEY", "master"), ("CRANK_SESSION_SECRET", "session"), ("CRANK_PASSWORD_PEPPER", "pepper"), ("CRANK_BOOTSTRAP_ADMIN_EMAIL", "owner@example.test"), ("CRANK_BOOTSTRAP_ADMIN_PASSWORD", "password"), ("CRANK_TRUST_FORWARDED_HEADERS", "true"), ] .into_iter() .map(|(key, value)| (key.to_owned(), value.to_owned())) .collect::>(); let mut compatibility = canonical.clone(); compatibility.insert("CRANK_TRUST_FORWARDED_HEADERS".into(), "yes".into()); let canonical_config = parse_process( ProcessKind::AdminApi, ConfigSource::from_utf8(canonical.clone()), ) .unwrap(); let compatibility_config = parse_process( ProcessKind::AdminApi, ConfigSource::from_utf8(compatibility), ) .unwrap(); assert_eq!( canonical_config.fingerprint(), compatibility_config.fingerprint() ); assert_eq!(compatibility_config.deprecations().len(), 1); canonical.insert("CRANK_SESSION_TTL_HOURS".into(), "48".into()); let changed = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(canonical)).unwrap(); assert_ne!(changed.fingerprint(), compatibility_config.fingerprint()); } #[test] fn diagnostics_are_bounded_json_and_never_echo_secret_canaries() { let canary = "CANARY_SECRET_VALUE"; let vars = [ ("CRANK_MASTER_KEY", canary), ("CRANK_SESSION_SECRET", canary), ("CRANK_PASSWORD_PEPPER", canary), ("CRANK_BOOTSTRAP_ADMIN_EMAIL", "owner@example.test"), ("CRANK_BOOTSTRAP_ADMIN_PASSWORD", canary), ( "CRANK_DATABASE_URL", "postgres://owner:CANARY_SECRET_VALUE@db/crank", ), ("POSTGRES_PASSWORD", canary), ("CRANK_CACHE_BACKEND", "memory"), ("CRANK_CACHE_URL", "redis://:CANARY_SECRET_VALUE@cache:6379"), ( "CRANK_SENTRY_DSN", "https://CANARY_SECRET_VALUE@sentry.test/1", ), ("CRANK_METRICS_BEARER_TOKEN", canary), ( "OTEL_EXPORTER_OTLP_HEADERS", "authorization=CANARY_SECRET_VALUE", ), ] .into_iter() .map(|(key, value)| (key.to_owned(), value.to_owned())) .collect::>(); let error = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err(); let display = error.to_string(); let json = error.to_json(); assert!(json.len() <= 65_536); assert!(serde_json::from_str::(&json).is_ok()); assert!(!display.contains(canary)); assert!(!json.contains(canary)); assert!(error.diagnostics().len() <= 100); } #[test] fn public_projection_debug_omits_urls_hosts_paths_and_identity_values() { let mut vars = [ ("CRANK_MASTER_KEY", "master"), ("CRANK_SESSION_SECRET", "session"), ("CRANK_PASSWORD_PEPPER", "pepper"), ("CRANK_BOOTSTRAP_ADMIN_EMAIL", "owner@CANARY.test"), ("CRANK_BOOTSTRAP_ADMIN_PASSWORD", "password"), ("CRANK_STORAGE_ROOT", "/CANARY/private/storage"), ("POSTGRES_HOST", "CANARY-db.internal"), ("CRANK_OUTBOUND_ALLOWED_HOSTS", "CANARY-api.internal"), ] .into_iter() .map(|(k, v)| (k.to_owned(), v.to_owned())) .collect::>(); vars.insert( "CRANK_BASE_URL".into(), "https://CANARY.example.test".into(), ); let config = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap(); let rendered = format!("{:?}", config.admin().unwrap()); assert!(!rendered.contains("CANARY"), "{rendered}"); } #[test] fn normalized_database_and_admin_default_urls_drive_fingerprint() { let base = [ ("CRANK_MASTER_KEY", "master"), ("CRANK_SESSION_SECRET", "session"), ("CRANK_PASSWORD_PEPPER", "pepper"), ("CRANK_BOOTSTRAP_ADMIN_EMAIL", "owner@example.test"), ("CRANK_BOOTSTRAP_ADMIN_PASSWORD", "password"), ] .into_iter() .map(|(k, v)| (k.to_owned(), v.to_owned())) .collect::>(); let implicit = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(base.clone())).unwrap(); let mut explicit = base.clone(); explicit.insert("CRANK_BASE_URL".into(), "http://localhost:3000".into()); let explicit = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(explicit)).unwrap(); assert_eq!(implicit.fingerprint(), explicit.fingerprint()); let mut url = base; url.insert( "CRANK_DATABASE_URL".into(), "postgres://crank:rotated@postgres/crank".into(), ); let url = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(url)).unwrap(); assert_eq!(implicit.fingerprint(), url.fingerprint()); let tls = [ ("CRANK_MASTER_KEY", "master"), ("CRANK_SESSION_SECRET", "session"), ("CRANK_PASSWORD_PEPPER", "pepper"), ("CRANK_BOOTSTRAP_ADMIN_EMAIL", "owner@example.test"), ("CRANK_BOOTSTRAP_ADMIN_PASSWORD", "password"), ( "CRANK_DATABASE_URL", "postgres://crank:rotated@postgres/crank?sslmode=require", ), ] .into_iter() .map(|(k, v)| (k.to_owned(), v.to_owned())) .collect::>(); let tls = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(tls)).unwrap(); assert_ne!(implicit.fingerprint(), tls.fingerprint()); }