feat: harden community production foundation through story 1.5
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_config::{
|
||||
ConfigSource, DiagnosticCode, FieldMode, ProcessKind, ProcessScope, field_registry,
|
||||
parse_migrator, parse_process,
|
||||
};
|
||||
|
||||
fn required_admin() -> BTreeMap<String, String> {
|
||||
[
|
||||
("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(|(key, value)| (key.to_owned(), value.to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn required_mcp() -> BTreeMap<String, String> {
|
||||
[("CRANK_MASTER_KEY", "master")]
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.to_owned(), value.to_owned()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrator_projection_requires_only_database_configuration() {
|
||||
let config = parse_migrator(ConfigSource::from_utf8(BTreeMap::new()))
|
||||
.expect("database defaults are sufficient for the controlled migration job");
|
||||
assert_eq!(config.database.host, "postgres");
|
||||
assert_eq!(config.database.port, 5432);
|
||||
assert_eq!(config.fingerprint().len(), 64);
|
||||
let debug = format!("{config:?}");
|
||||
assert!(
|
||||
!debug.contains("crank"),
|
||||
"database password must remain redacted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrator_ignores_service_configuration_and_rejects_database_typos() {
|
||||
let mut values = BTreeMap::from([
|
||||
("CRANK_MASTER_KEY".to_owned(), "secret-canary".to_owned()),
|
||||
(
|
||||
"CRANK_SESSION_SECRET".to_owned(),
|
||||
"secret-canary".to_owned(),
|
||||
),
|
||||
("CRANK_MCP_REFRESH_MS".to_owned(), "invalid".to_owned()),
|
||||
]);
|
||||
parse_migrator(ConfigSource::from_utf8(values.clone()))
|
||||
.expect("service fields are outside the database-only projection");
|
||||
values.insert("POSTGRES_PORRT".to_owned(), "5432".to_owned());
|
||||
let error = parse_migrator(ConfigSource::from_utf8(values)).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.code == DiagnosticCode::UnknownField)
|
||||
);
|
||||
assert!(!error.to_string().contains("secret-canary"));
|
||||
}
|
||||
|
||||
fn source_for(
|
||||
field: &crank_config::FieldSpec,
|
||||
value: String,
|
||||
) -> (ProcessKind, BTreeMap<String, String>) {
|
||||
let kind = match field.process {
|
||||
ProcessScope::McpServer => ProcessKind::McpServer,
|
||||
ProcessScope::Shared | ProcessScope::AdminApi => ProcessKind::AdminApi,
|
||||
};
|
||||
let mut vars = match kind {
|
||||
ProcessKind::AdminApi => required_admin(),
|
||||
ProcessKind::McpServer => required_mcp(),
|
||||
};
|
||||
vars.insert(field.env_name.to_owned(), value);
|
||||
match field.env_name {
|
||||
"POSTGRES_MAX_CONNECTIONS" => {
|
||||
vars.insert("POSTGRES_MIN_CONNECTIONS".into(), "0".into());
|
||||
}
|
||||
"POSTGRES_MIN_CONNECTIONS" => {
|
||||
vars.insert("POSTGRES_MAX_CONNECTIONS".into(), "1024".into());
|
||||
}
|
||||
"CRANK_ADMIN_RATE_LIMIT_RPS" => {
|
||||
vars.insert("CRANK_ADMIN_RATE_LIMIT_BURST".into(), "1000000".into());
|
||||
}
|
||||
"CRANK_ADMIN_RATE_LIMIT_BURST" => {
|
||||
vars.insert("CRANK_ADMIN_RATE_LIMIT_RPS".into(), "1".into());
|
||||
}
|
||||
"CRANK_MCP_RATE_LIMIT_RPS" => {
|
||||
vars.insert("CRANK_MCP_RATE_LIMIT_BURST".into(), "1000000".into());
|
||||
}
|
||||
"CRANK_MCP_RATE_LIMIT_BURST" => {
|
||||
vars.insert("CRANK_MCP_RATE_LIMIT_RPS".into(), "1".into());
|
||||
}
|
||||
"OTEL_BSP_MAX_QUEUE_SIZE" => {
|
||||
vars.insert("OTEL_BSP_MAX_EXPORT_BATCH_SIZE".into(), "1".into());
|
||||
}
|
||||
"OTEL_BSP_MAX_EXPORT_BATCH_SIZE" => {
|
||||
vars.insert("OTEL_BSP_MAX_QUEUE_SIZE".into(), "65536".into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
(kind, vars)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_covers_exactly_the_57_observed_runtime_names() {
|
||||
let registry = field_registry();
|
||||
assert_eq!(registry.len(), 57);
|
||||
let unique = registry
|
||||
.iter()
|
||||
.map(|field| field.env_name)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(unique.len(), registry.len());
|
||||
assert!(!unique.contains("CRANK_RUNTIME_MAX_CONCURRENT_WINDOW"));
|
||||
assert!(!unique.contains("CRANK_RUNTIME_MAX_CONCURRENT_JOBS"));
|
||||
for field in registry {
|
||||
assert!(!field.semantic_path.is_empty());
|
||||
assert!(!field.env_name.is_empty());
|
||||
assert!(!field.value_type.is_empty());
|
||||
if let (Some(minimum), Some(maximum)) = (field.minimum, field.maximum) {
|
||||
assert!(minimum <= maximum, "{}", field.env_name);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
registry
|
||||
.iter()
|
||||
.find(|field| field.env_name == "CRANK_CACHE_DEFAULT_TTL_MS")
|
||||
.unwrap()
|
||||
.mode,
|
||||
FieldMode::DeprecatedNoEffect
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_are_preserved_and_invalid_values_never_fall_back() {
|
||||
let valid = parse_process(
|
||||
ProcessKind::AdminApi,
|
||||
ConfigSource::from_utf8(required_admin()),
|
||||
)
|
||||
.expect("minimal admin config");
|
||||
let admin = valid.admin().expect("admin projection");
|
||||
assert_eq!(admin.database.port, 5432);
|
||||
assert_eq!(admin.session_ttl_hours, 24);
|
||||
assert_eq!(admin.rate_limit.requests_per_second, 30);
|
||||
|
||||
for (name, value) in [
|
||||
("POSTGRES_PORT", "bad"),
|
||||
("CRANK_SESSION_TTL_HOURS", "bad"),
|
||||
("CRANK_ADMIN_RATE_LIMIT_RPS", "bad"),
|
||||
("CRANK_TRUST_FORWARDED_HEADERS", "tru"),
|
||||
] {
|
||||
let mut vars = required_admin();
|
||||
vars.insert(name.to_owned(), value.to_owned());
|
||||
let error =
|
||||
parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
let path = field_registry()
|
||||
.iter()
|
||||
.find(|field| field.env_name == name)
|
||||
.unwrap()
|
||||
.semantic_path;
|
||||
assert!(error.diagnostics().iter().any(|item| item.field == path));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_forms_conflict_and_owned_typos_fail_closed() {
|
||||
let mut vars = required_admin();
|
||||
vars.insert(
|
||||
"CRANK_DATABASE_URL".into(),
|
||||
"postgres://user:secret@db/crank".into(),
|
||||
);
|
||||
vars.insert("POSTGRES_HOST".into(), "db".into());
|
||||
let error = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.code == DiagnosticCode::Conflict)
|
||||
);
|
||||
|
||||
let mut vars = required_admin();
|
||||
vars.insert("CRANK_SESION_SECRET".into(), "canary".into());
|
||||
let error = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.code == DiagnosticCode::UnknownField)
|
||||
);
|
||||
assert!(!error.to_string().contains("canary"));
|
||||
|
||||
for ghost in [
|
||||
"CRANK_RUNTIME_MAX_CONCURRENT_WINDOW",
|
||||
"CRANK_RUNTIME_MAX_CONCURRENT_JOBS",
|
||||
] {
|
||||
let mut vars = required_admin();
|
||||
vars.insert(ghost.into(), "4".into());
|
||||
let error =
|
||||
parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
assert!(error.diagnostics().iter().any(|item| {
|
||||
item.code == DiagnosticCode::UnknownField && item.field == "environment.unknown"
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_deployment_only_names_are_known_but_never_runtime_fields() {
|
||||
let mut vars = required_admin();
|
||||
for name in crank_config::deployment_field_registry() {
|
||||
vars.insert((*name).to_owned(), "deployment-value".to_owned());
|
||||
}
|
||||
let config = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap();
|
||||
assert!(config.admin().is_some());
|
||||
assert!(
|
||||
field_registry()
|
||||
.iter()
|
||||
.all(|field| { !crank_config::deployment_field_registry().contains(&field.env_name) })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_but_unused_cache_ttl_is_an_explicit_non_pass_contract() {
|
||||
let mut vars = required_admin();
|
||||
vars.insert("CRANK_CACHE_DEFAULT_TTL_MS".into(), "5000".into());
|
||||
let error = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
assert!(error.diagnostics().iter().any(|item| {
|
||||
item.code == DiagnosticCode::DeprecatedNoEffect && item.field == "cache.default_ttl_ms"
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn os_source_rejects_non_utf8_without_echoing_bytes() {
|
||||
use std::{ffi::OsString, os::unix::ffi::OsStringExt};
|
||||
|
||||
let error = ConfigSource::from_os_iter([(
|
||||
OsString::from("CRANK_MASTER_KEY"),
|
||||
OsString::from_vec(vec![0xff, b'S', b'E', b'C', b'R', b'E', b'T']),
|
||||
)])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.code == DiagnosticCode::InvalidEncoding)
|
||||
);
|
||||
assert!(!error.to_string().contains("SECRET"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn os_source_ignores_unrelated_invalid_or_unbounded_values() {
|
||||
use std::{ffi::OsString, os::unix::ffi::OsStringExt};
|
||||
|
||||
ConfigSource::from_os_iter([
|
||||
(
|
||||
OsString::from_vec(vec![0xff]),
|
||||
OsString::from_vec(vec![0xff]),
|
||||
),
|
||||
(
|
||||
OsString::from("JAVA_TOOL_OPTIONS"),
|
||||
OsString::from("x".repeat(20_000)),
|
||||
),
|
||||
(OsString::from("LANG"), OsString::from_vec(vec![0xff, b'x'])),
|
||||
])
|
||||
.expect("unrelated OS state is outside the runtime contract");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_specific_fields_and_zero_ports_fail_closed() {
|
||||
let mut mcp = required_mcp();
|
||||
mcp.insert("CRANK_SESSION_SECRET".into(), "wrong-process".into());
|
||||
let error = parse_process(ProcessKind::McpServer, ConfigSource::from_utf8(mcp)).unwrap_err();
|
||||
assert!(error.diagnostics().iter().any(|item| {
|
||||
item.code == DiagnosticCode::UnknownField && item.field == "admin.session.secret"
|
||||
}));
|
||||
|
||||
for (kind, name, required) in [
|
||||
(ProcessKind::AdminApi, "CRANK_ADMIN_BIND", required_admin()),
|
||||
(
|
||||
ProcessKind::AdminApi,
|
||||
"CRANK_ADMIN_METRICS_BIND",
|
||||
required_admin(),
|
||||
),
|
||||
(ProcessKind::McpServer, "CRANK_MCP_BIND", required_mcp()),
|
||||
(
|
||||
ProcessKind::McpServer,
|
||||
"CRANK_MCP_METRICS_BIND",
|
||||
required_mcp(),
|
||||
),
|
||||
] {
|
||||
let mut vars = required;
|
||||
vars.insert(name.into(), "127.0.0.1:0".into());
|
||||
let error = parse_process(kind, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
let path = field_registry()
|
||||
.iter()
|
||||
.find(|field| field.env_name == name)
|
||||
.unwrap()
|
||||
.semantic_path;
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| { item.code == DiagnosticCode::OutOfRange && item.field == path })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_secrets_and_consumer_invalid_values_fail_in_the_leaf_parser() {
|
||||
for name in [
|
||||
"CRANK_MASTER_KEY",
|
||||
"CRANK_SESSION_SECRET",
|
||||
"CRANK_PASSWORD_PEPPER",
|
||||
"CRANK_BOOTSTRAP_ADMIN_PASSWORD",
|
||||
] {
|
||||
let mut vars = required_admin();
|
||||
vars.insert(name.into(), " ".into());
|
||||
assert!(
|
||||
parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).is_err(),
|
||||
"{name}"
|
||||
);
|
||||
}
|
||||
for (name, value) in [
|
||||
("CRANK_OUTBOUND_ALLOWED_HOSTS", "example.test:443"),
|
||||
("CRANK_DATABASE_URL", "postgres://db/crank?sslmode=bogus"),
|
||||
("CRANK_ENVIRONMENT", "bad environment"),
|
||||
("CRANK_SENTRY_DSN", "not-a-dsn"),
|
||||
("OTEL_EXPORTER_OTLP_HEADERS", "bad name=value"),
|
||||
(
|
||||
"CRANK_BASE_URL",
|
||||
"https://user:secret@example.test/path?token=x",
|
||||
),
|
||||
] {
|
||||
let mut vars = required_admin();
|
||||
vars.insert(name.into(), value.into());
|
||||
let error =
|
||||
parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).expect_err(name);
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.code == DiagnosticCode::InvalidType),
|
||||
"{name}: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_field_and_typed_boundaries_fail_closed() {
|
||||
let cases = [
|
||||
("POSTGRES_MAX_CONNECTIONS", "1025"),
|
||||
("CRANK_ADMIN_RATE_LIMIT_RPS", "100001"),
|
||||
("CRANK_ADMIN_RATE_LIMIT_BURST", "0"),
|
||||
("CRANK_OUTBOUND_MAX_RESPONSE_BYTES", "67108865"),
|
||||
("OTEL_BSP_SCHEDULE_DELAY", "bad"),
|
||||
("OTEL_BSP_EXPORT_TIMEOUT", "300001"),
|
||||
("CRANK_BASE_URL", "file:///tmp/config"),
|
||||
];
|
||||
for (name, value) in cases {
|
||||
let mut vars = required_admin();
|
||||
vars.insert(name.to_owned(), value.to_owned());
|
||||
let error =
|
||||
parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).expect_err(name);
|
||||
assert!(
|
||||
error.diagnostics().iter().any(|item| item.field
|
||||
== field_registry()
|
||||
.iter()
|
||||
.find(|field| field.env_name == name)
|
||||
.unwrap()
|
||||
.semantic_path),
|
||||
"{name}: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut vars = required_admin();
|
||||
vars.insert("POSTGRES_MIN_CONNECTIONS".into(), "21".into());
|
||||
let error = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.code == DiagnosticCode::UnsafeCombination)
|
||||
);
|
||||
|
||||
let mut vars = required_admin();
|
||||
vars.insert("CRANK_CACHE_BACKEND".into(), "valkey".into());
|
||||
vars.insert("CRANK_CACHE_URL".into(), "https://not-a-cache.test".into());
|
||||
let error = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.field == "cache.url")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inclusive_edges_and_legacy_boolean_spellings_are_explicit() {
|
||||
let mut vars = required_admin();
|
||||
vars.extend([
|
||||
("POSTGRES_PORT".into(), "65535".into()),
|
||||
("POSTGRES_MAX_CONNECTIONS".into(), "1024".into()),
|
||||
("POSTGRES_MIN_CONNECTIONS".into(), "0".into()),
|
||||
("CRANK_RUNTIME_MAX_CONCURRENT_UNARY".into(), "1".into()),
|
||||
(
|
||||
"CRANK_OUTBOUND_MAX_RESPONSE_BYTES".into(),
|
||||
"67108864".into(),
|
||||
),
|
||||
("CRANK_TRUST_FORWARDED_HEADERS".into(), "yes".into()),
|
||||
("CRANK_DEMO_SEED".into(), "off".into()),
|
||||
]);
|
||||
let config = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap();
|
||||
let admin = config.admin().unwrap();
|
||||
assert_eq!(admin.database.port, 65_535);
|
||||
assert_eq!(admin.database.pool.max_connections, 1024);
|
||||
assert_eq!(admin.database.pool.min_connections, 0);
|
||||
assert_eq!(admin.runtime.max_concurrent_unary, 1);
|
||||
assert!(admin.trust_forwarded_headers);
|
||||
assert!(!admin.demo_seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_and_mcp_share_one_normalized_foundation() {
|
||||
let mut vars = required_admin();
|
||||
vars.extend([
|
||||
("CRANK_BASE_URL".into(), "https://crank.example.test".into()),
|
||||
("CRANK_RUNTIME_MAX_CONCURRENT_UNARY".into(), "72".into()),
|
||||
(
|
||||
"CRANK_OUTBOUND_ALLOWED_HOSTS".into(),
|
||||
"api.example.test".into(),
|
||||
),
|
||||
("POSTGRES_MAX_CONNECTIONS".into(), "24".into()),
|
||||
]);
|
||||
let admin =
|
||||
parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars.clone())).unwrap();
|
||||
let mut mcp_vars = vars;
|
||||
for key in [
|
||||
"CRANK_SESSION_SECRET",
|
||||
"CRANK_PASSWORD_PEPPER",
|
||||
"CRANK_BOOTSTRAP_ADMIN_EMAIL",
|
||||
"CRANK_BOOTSTRAP_ADMIN_PASSWORD",
|
||||
] {
|
||||
mcp_vars.remove(key);
|
||||
}
|
||||
let mcp = parse_process(ProcessKind::McpServer, ConfigSource::from_utf8(mcp_vars)).unwrap();
|
||||
let admin = admin.admin().unwrap();
|
||||
let mcp = mcp.mcp().unwrap();
|
||||
assert_eq!(admin.database.host, mcp.database.host);
|
||||
assert_eq!(
|
||||
admin.database.pool.max_connections,
|
||||
mcp.database.pool.max_connections
|
||||
);
|
||||
assert_eq!(admin.runtime.base_url, mcp.runtime.base_url);
|
||||
assert_eq!(
|
||||
admin.runtime.max_concurrent_unary,
|
||||
mcp.runtime.max_concurrent_unary
|
||||
);
|
||||
assert_eq!(
|
||||
admin.runtime.outbound.allowed_hosts,
|
||||
mcp.runtime.outbound.allowed_hosts
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_bounded_numeric_field_accepts_edges_and_rejects_outside_values() {
|
||||
for field in field_registry().iter().filter(|field| {
|
||||
field.mode == FieldMode::Effective && field.minimum.is_some() && field.maximum.is_some()
|
||||
}) {
|
||||
let minimum = field.minimum.unwrap();
|
||||
let maximum = field.maximum.unwrap();
|
||||
for accepted in [minimum, maximum] {
|
||||
let (kind, vars) = source_for(field, accepted.to_string());
|
||||
parse_process(kind, ConfigSource::from_utf8(vars))
|
||||
.unwrap_or_else(|error| panic!("{}={accepted}: {error}", field.env_name));
|
||||
}
|
||||
for rejected in [
|
||||
if minimum == 0 {
|
||||
"-1".to_owned()
|
||||
} else {
|
||||
(minimum - 1).to_string()
|
||||
},
|
||||
(maximum + 1).to_string(),
|
||||
] {
|
||||
let (kind, vars) = source_for(field, rejected);
|
||||
let error =
|
||||
parse_process(kind, ConfigSource::from_utf8(vars)).expect_err(field.env_name);
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.field == field.semantic_path),
|
||||
"{}: {error}",
|
||||
field.env_name
|
||||
);
|
||||
}
|
||||
|
||||
for malformed in ["-1", "184467440737095516160", "1.5", " 1", "1\n"] {
|
||||
let (kind, vars) = source_for(field, malformed.to_owned());
|
||||
let error =
|
||||
parse_process(kind, ConfigSource::from_utf8(vars)).expect_err(field.env_name);
|
||||
assert!(
|
||||
error
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.any(|item| item.field == field.semantic_path),
|
||||
"{}={malformed:?}: {error}",
|
||||
field.env_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility_values_and_otel_precedence_remain_explicit() {
|
||||
let mut vars = required_admin();
|
||||
vars.extend([
|
||||
("CRANK_CACHE_BACKEND".into(), "redis".into()),
|
||||
(
|
||||
"CRANK_CACHE_URL".into(),
|
||||
"redis://cache.example.test:6379".into(),
|
||||
),
|
||||
(
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT".into(),
|
||||
"https://generic.example.test".into(),
|
||||
),
|
||||
(
|
||||
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT".into(),
|
||||
"https://traces.example.test".into(),
|
||||
),
|
||||
("OTEL_EXPORTER_OTLP_TIMEOUT".into(), "10000".into()),
|
||||
("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT".into(), "5000".into()),
|
||||
]);
|
||||
let config = parse_process(ProcessKind::AdminApi, ConfigSource::from_utf8(vars)).unwrap();
|
||||
let admin = config.admin().unwrap();
|
||||
assert_eq!(
|
||||
admin.runtime.cache.backend,
|
||||
crank_config::CacheBackend::Redis
|
||||
);
|
||||
assert_eq!(
|
||||
admin.observability.otlp.endpoint.as_deref(),
|
||||
Some("https://generic.example.test")
|
||||
);
|
||||
assert_eq!(
|
||||
admin.observability.otlp.traces_endpoint.as_deref(),
|
||||
Some("https://traces.example.test")
|
||||
);
|
||||
assert_eq!(admin.observability.otlp.timeout.as_deref(), Some("10000"));
|
||||
assert_eq!(
|
||||
admin.observability.otlp.traces_timeout.as_deref(),
|
||||
Some("5000")
|
||||
);
|
||||
assert_eq!(config.deprecations().len(), 1);
|
||||
assert_eq!(config.deprecations()[0].field, "cache.backend");
|
||||
}
|
||||
Reference in New Issue
Block a user