use serde::{Deserialize, Serialize};
use crate::config::MailSection;
pub const MAX_AGENT_PRUNE_DAYS: u32 = 36_500;
pub const DEFAULT_COLLECT_RETENTION_DAYS: u32 = 30;
pub const MAX_COLLECT_RETENTION_DAYS: u32 = 3650;
pub const DEFAULT_SESSION_TTL_HOURS: u32 = 24;
pub const MAX_SESSION_TTL_HOURS: u32 = 8_760;
pub const DEFAULT_CHECK_STATUS_STALE_DAYS: u32 = 30;
pub const MAX_CHECK_STATUS_STALE_DAYS: u32 = 3650;
pub const DEFAULT_SUPPORT_UNLOCK_TTL_MINUTES: u32 = 15;
pub const MAX_SUPPORT_UNLOCK_TTL_MINUTES: u32 = 480;
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[serde(default)]
pub struct ObjectStoreCaps {
#[serde(skip_serializing_if = "Option::is_none")]
pub result_output_mib: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_releases_mib: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_packages_mib: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scripts_mib: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collections_mib: Option<u32>,
}
pub const DEFAULT_RESULT_OUTPUT_CAP_MIB: u32 = 1024;
pub const DEFAULT_RESULT_OUTPUT_RETENTION_DAYS: u32 = 7;
pub const MAX_RESULT_OUTPUT_RETENTION_DAYS: u32 = 30;
pub const DEFAULT_AGENT_RELEASES_CAP_MIB: u32 = 2048;
pub const DEFAULT_APP_PACKAGES_CAP_MIB: u32 = 5120;
pub const DEFAULT_SCRIPTS_CAP_MIB: u32 = 256;
pub const DEFAULT_COLLECTIONS_CAP_MIB: u32 = 5120;
pub const MAX_OBJECT_STORE_CAP_MIB: u32 = 51_200;
pub const MAX_OBJECT_STORE_TOTAL_MIB: u32 = 46_272;
impl ObjectStoreCaps {
fn effective(v: Option<u32>, default: u32) -> u32 {
v.unwrap_or(default).clamp(1, MAX_OBJECT_STORE_CAP_MIB)
}
pub fn effective_result_output_mib(&self) -> u32 {
Self::effective(self.result_output_mib, DEFAULT_RESULT_OUTPUT_CAP_MIB)
}
pub fn effective_agent_releases_mib(&self) -> u32 {
Self::effective(self.agent_releases_mib, DEFAULT_AGENT_RELEASES_CAP_MIB)
}
pub fn effective_app_packages_mib(&self) -> u32 {
Self::effective(self.app_packages_mib, DEFAULT_APP_PACKAGES_CAP_MIB)
}
pub fn effective_scripts_mib(&self) -> u32 {
Self::effective(self.scripts_mib, DEFAULT_SCRIPTS_CAP_MIB)
}
pub fn effective_collections_mib(&self) -> u32 {
Self::effective(self.collections_mib, DEFAULT_COLLECTIONS_CAP_MIB)
}
pub fn effective_all(&self) -> [(&'static str, u32); 5] {
[
(
crate::kv::OBJECT_AGENT_RELEASES,
self.effective_agent_releases_mib(),
),
(
crate::kv::OBJECT_APP_PACKAGES,
self.effective_app_packages_mib(),
),
(crate::kv::OBJECT_SCRIPTS, self.effective_scripts_mib()),
(
crate::kv::OBJECT_RESULT_OUTPUT,
self.effective_result_output_mib(),
),
(
crate::kv::OBJECT_COLLECTIONS,
self.effective_collections_mib(),
),
]
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[serde(default)]
pub struct SupportCode {
pub scope: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub hash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl_minutes: Option<u32>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub disabled: bool,
}
impl SupportCode {
pub fn effective_ttl_minutes(&self) -> u32 {
self.ttl_minutes
.unwrap_or(DEFAULT_SUPPORT_UNLOCK_TTL_MINUTES)
.clamp(1, MAX_SUPPORT_UNLOCK_TTL_MINUTES)
}
pub fn is_usable(&self) -> bool {
!self.disabled && !self.hash.is_empty()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[serde(default)]
pub struct SupportCodesProjection {
pub support_codes: Vec<SupportCode>,
}
impl SupportCodesProjection {
pub fn from_settings(settings: &ServerSettings) -> Self {
Self {
support_codes: settings.support_codes.clone(),
}
}
}
#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentInstallSection {
#[serde(skip_serializing_if = "Option::is_none")]
pub nats_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nats_token: Option<String>,
#[serde(skip_deserializing)]
pub nats_token_set: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub nats_user: Option<String>,
#[serde(skip_deserializing)]
pub nats_user_set: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub nats_password: Option<String>,
#[serde(skip_deserializing)]
pub nats_password_set: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_signed_commands: Option<bool>,
}
impl std::fmt::Debug for AgentInstallSection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mask = |v: &Option<String>| v.as_ref().map(|_| "<redacted>");
f.debug_struct("AgentInstallSection")
.field("nats_url", &self.nats_url)
.field("nats_token", &mask(&self.nats_token))
.field("nats_token_set", &self.nats_token_set)
.field("nats_user", &mask(&self.nats_user))
.field("nats_user_set", &self.nats_user_set)
.field("nats_password", &mask(&self.nats_password))
.field("nats_password_set", &self.nats_password_set)
.field("require_signed_commands", &self.require_signed_commands)
.finish()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NatsAuthMode {
#[default]
Token,
Users,
}
impl NatsAuthMode {
pub fn as_str(self) -> &'static str {
match self {
NatsAuthMode::Token => "token",
NatsAuthMode::Users => "users",
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[serde(default)]
pub struct ServerSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_prune_days: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub controller_group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mail: Option<MailSection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_install: Option<AgentInstallSection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collect_retention_days: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result_output_retention_days: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_ttl_hours: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub check_status_stale_days: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_store_caps: Option<ObjectStoreCaps>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nats_auth_mode: Option<NatsAuthMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nats_auth_mode_changed_at: Option<chrono::DateTime<chrono::Utc>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub support_codes: Vec<SupportCode>,
}
impl ServerSettings {
pub fn defaults() -> Self {
Self {
agent_prune_days: None,
controller_group: None,
mail: None,
agent_install: None,
collect_retention_days: Some(DEFAULT_COLLECT_RETENTION_DAYS),
result_output_retention_days: Some(DEFAULT_RESULT_OUTPUT_RETENTION_DAYS),
session_ttl_hours: Some(DEFAULT_SESSION_TTL_HOURS),
check_status_stale_days: Some(DEFAULT_CHECK_STATUS_STALE_DAYS),
nats_auth_mode: None,
nats_auth_mode_changed_at: None,
object_store_caps: Some(ObjectStoreCaps {
result_output_mib: Some(DEFAULT_RESULT_OUTPUT_CAP_MIB),
agent_releases_mib: Some(DEFAULT_AGENT_RELEASES_CAP_MIB),
app_packages_mib: Some(DEFAULT_APP_PACKAGES_CAP_MIB),
scripts_mib: Some(DEFAULT_SCRIPTS_CAP_MIB),
collections_mib: Some(DEFAULT_COLLECTIONS_CAP_MIB),
}),
support_codes: Vec::new(),
}
}
pub fn effective_object_store_caps(&self) -> ObjectStoreCaps {
let c = self.object_store_caps.clone().unwrap_or_default();
ObjectStoreCaps {
result_output_mib: Some(c.effective_result_output_mib()),
agent_releases_mib: Some(c.effective_agent_releases_mib()),
app_packages_mib: Some(c.effective_app_packages_mib()),
scripts_mib: Some(c.effective_scripts_mib()),
collections_mib: Some(c.effective_collections_mib()),
}
}
pub fn support_code(&self, scope: &str) -> Option<&SupportCode> {
self.support_codes
.iter()
.find(|c| c.scope == scope && c.is_usable())
}
#[must_use]
pub fn redacted(mut self) -> Self {
for c in &mut self.support_codes {
c.hash.clear();
}
if let Some(ai) = self.agent_install.as_mut() {
ai.nats_token_set = ai.nats_token.is_some();
ai.nats_token = None;
ai.nats_user_set = ai.nats_user.is_some();
ai.nats_user = None;
ai.nats_password_set = ai.nats_password.is_some();
ai.nats_password = None;
}
self
}
pub fn effective_controller_group(&self) -> Option<&str> {
self.controller_group
.as_deref()
.map(str::trim)
.filter(|g| !g.is_empty())
}
pub fn effective_agent_prune_days(&self) -> u32 {
self.agent_prune_days
.or(Self::defaults().agent_prune_days)
.unwrap_or(0)
.min(MAX_AGENT_PRUNE_DAYS)
}
pub fn effective_collect_retention_days(&self) -> u32 {
self.collect_retention_days
.or(Self::defaults().collect_retention_days)
.unwrap_or(DEFAULT_COLLECT_RETENTION_DAYS)
.clamp(1, MAX_COLLECT_RETENTION_DAYS)
}
pub fn effective_result_output_retention_days(&self) -> u32 {
self.result_output_retention_days
.or(Self::defaults().result_output_retention_days)
.unwrap_or(DEFAULT_RESULT_OUTPUT_RETENTION_DAYS)
.clamp(1, MAX_RESULT_OUTPUT_RETENTION_DAYS)
}
pub fn effective_session_ttl_hours(&self) -> u32 {
self.session_ttl_hours
.or(Self::defaults().session_ttl_hours)
.unwrap_or(DEFAULT_SESSION_TTL_HOURS)
.clamp(1, MAX_SESSION_TTL_HOURS)
}
pub fn effective_nats_auth_mode(&self) -> NatsAuthMode {
self.nats_auth_mode.unwrap_or_default()
}
pub fn effective_check_status_stale_days(&self) -> u32 {
self.check_status_stale_days
.or(Self::defaults().check_status_stale_days)
.unwrap_or(DEFAULT_CHECK_STATUS_STALE_DAYS)
.min(MAX_CHECK_STATUS_STALE_DAYS)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_unset() {
assert_eq!(ServerSettings::default().agent_prune_days, None);
}
#[test]
fn unset_resolves_to_disabled() {
assert_eq!(ServerSettings::default().effective_agent_prune_days(), 0);
}
#[test]
fn stored_value_wins_over_default() {
let s = ServerSettings {
agent_prune_days: Some(30),
..Default::default()
};
assert_eq!(s.effective_agent_prune_days(), 30);
}
#[test]
fn effective_clamps_to_max() {
let s = ServerSettings {
agent_prune_days: Some(u32::MAX),
..Default::default()
};
assert_eq!(s.effective_agent_prune_days(), MAX_AGENT_PRUNE_DAYS);
}
#[test]
fn round_trips_through_json() {
let s = ServerSettings {
agent_prune_days: Some(30),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, r#"{"agent_prune_days":30}"#);
let back: ServerSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back, s);
}
#[test]
fn unset_serialises_to_empty_object() {
let s = ServerSettings::default();
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, "{}");
let back: ServerSettings = serde_json::from_str(&json).unwrap();
assert_eq!(back, s);
}
#[test]
fn explicit_null_decodes_to_unset() {
let s: ServerSettings = serde_json::from_str(r#"{"agent_prune_days":null}"#).unwrap();
assert_eq!(s.agent_prune_days, None);
}
#[test]
fn empty_object_decodes_to_default() {
let s: ServerSettings = serde_json::from_str("{}").unwrap();
assert_eq!(s, ServerSettings::default());
}
#[test]
fn controller_group_effective_trims_and_blank_is_unset() {
assert_eq!(ServerSettings::default().effective_controller_group(), None);
let s = ServerSettings {
controller_group: Some(" feed-runners ".into()),
..Default::default()
};
assert_eq!(s.effective_controller_group(), Some("feed-runners"));
let blank = ServerSettings {
controller_group: Some(" ".into()),
..Default::default()
};
assert_eq!(blank.effective_controller_group(), None);
}
#[test]
fn controller_group_round_trips_and_omits_when_unset() {
let s = ServerSettings {
controller_group: Some("infra".into()),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, r#"{"controller_group":"infra"}"#);
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
assert_eq!(
serde_json::to_string(&ServerSettings::default()).unwrap(),
"{}"
);
}
#[test]
fn mail_round_trips_and_omits_when_unset() {
use crate::config::{MailEncryption, MailSection};
assert_eq!(
serde_json::to_string(&ServerSettings::default()).unwrap(),
"{}"
);
let s = ServerSettings {
mail: Some(MailSection {
host: "smtp.example.com".into(),
port: 587,
encryption: MailEncryption::Starttls,
from: "kanade-noreply@example.com".into(),
username: Some("kanade-noreply".into()),
}),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains(r#""encryption":"starttls""#), "json: {json}");
assert!(!json.contains("password"), "password must never serialise");
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
}
#[test]
fn mail_defaults_to_unset() {
assert_eq!(ServerSettings::default().mail, None);
let s: ServerSettings = serde_json::from_str(r#"{"agent_prune_days":7}"#).unwrap();
assert_eq!(s.mail, None);
assert_eq!(s.agent_prune_days, Some(7));
}
#[test]
fn collect_retention_unset_resolves_to_builtin_default() {
assert_eq!(ServerSettings::default().collect_retention_days, None);
assert_eq!(
ServerSettings::default().effective_collect_retention_days(),
DEFAULT_COLLECT_RETENTION_DAYS,
);
assert_eq!(
ServerSettings::defaults().collect_retention_days,
Some(DEFAULT_COLLECT_RETENTION_DAYS),
);
}
#[test]
fn collect_retention_stored_value_wins() {
let s = ServerSettings {
collect_retention_days: Some(90),
..Default::default()
};
assert_eq!(s.effective_collect_retention_days(), 90);
}
#[test]
fn collect_retention_effective_clamps_out_of_band_writes() {
let big = ServerSettings {
collect_retention_days: Some(u32::MAX),
..Default::default()
};
assert_eq!(
big.effective_collect_retention_days(),
MAX_COLLECT_RETENTION_DAYS,
);
let zero = ServerSettings {
collect_retention_days: Some(0),
..Default::default()
};
assert_eq!(zero.effective_collect_retention_days(), 1);
}
#[test]
fn result_output_retention_defaults_to_a_week_not_the_old_month() {
assert_eq!(DEFAULT_RESULT_OUTPUT_RETENTION_DAYS, 7);
assert_eq!(
ServerSettings::default().effective_result_output_retention_days(),
7
);
}
#[test]
fn result_output_retention_is_capped_by_the_stream_it_serves() {
assert_eq!(MAX_RESULT_OUTPUT_RETENTION_DAYS, 30);
let big = ServerSettings {
result_output_retention_days: Some(u32::MAX),
..Default::default()
};
assert_eq!(
big.effective_result_output_retention_days(),
MAX_RESULT_OUTPUT_RETENTION_DAYS
);
let zero = ServerSettings {
result_output_retention_days: Some(0),
..Default::default()
};
assert_eq!(zero.effective_result_output_retention_days(), 1);
}
#[test]
fn collect_retention_round_trips_and_omits_when_unset() {
let s = ServerSettings {
collect_retention_days: Some(90),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, r#"{"collect_retention_days":90}"#);
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
assert!(
!serde_json::to_string(&ServerSettings::default())
.unwrap()
.contains("collect_retention_days")
);
}
#[test]
fn session_ttl_unset_resolves_to_builtin_default() {
assert_eq!(ServerSettings::default().session_ttl_hours, None);
assert_eq!(
ServerSettings::default().effective_session_ttl_hours(),
DEFAULT_SESSION_TTL_HOURS,
);
assert_eq!(
ServerSettings::defaults().session_ttl_hours,
Some(DEFAULT_SESSION_TTL_HOURS),
);
}
#[test]
fn session_ttl_stored_value_wins() {
let s = ServerSettings {
session_ttl_hours: Some(72),
..Default::default()
};
assert_eq!(s.effective_session_ttl_hours(), 72);
}
#[test]
fn session_ttl_effective_clamps_out_of_band_writes() {
let zero = ServerSettings {
session_ttl_hours: Some(0),
..Default::default()
};
assert_eq!(zero.effective_session_ttl_hours(), 1);
let huge = ServerSettings {
session_ttl_hours: Some(u32::MAX),
..Default::default()
};
assert_eq!(huge.effective_session_ttl_hours(), MAX_SESSION_TTL_HOURS);
}
#[test]
fn session_ttl_round_trips_and_omits_when_unset() {
let s = ServerSettings {
session_ttl_hours: Some(48),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, r#"{"session_ttl_hours":48}"#);
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
assert!(
!serde_json::to_string(&ServerSettings::default())
.unwrap()
.contains("session_ttl_hours")
);
}
#[test]
fn check_stale_unset_resolves_to_builtin_default() {
assert_eq!(ServerSettings::default().check_status_stale_days, None);
assert_eq!(
ServerSettings::default().effective_check_status_stale_days(),
DEFAULT_CHECK_STATUS_STALE_DAYS,
);
assert_eq!(
ServerSettings::defaults().check_status_stale_days,
Some(DEFAULT_CHECK_STATUS_STALE_DAYS),
);
}
#[test]
fn check_stale_zero_disables() {
let s = ServerSettings {
check_status_stale_days: Some(0),
..Default::default()
};
assert_eq!(s.effective_check_status_stale_days(), 0);
}
#[test]
fn check_stale_stored_value_wins_and_clamps() {
let s = ServerSettings {
check_status_stale_days: Some(7),
..Default::default()
};
assert_eq!(s.effective_check_status_stale_days(), 7);
let big = ServerSettings {
check_status_stale_days: Some(u32::MAX),
..Default::default()
};
assert_eq!(
big.effective_check_status_stale_days(),
MAX_CHECK_STATUS_STALE_DAYS,
);
}
#[test]
fn check_stale_round_trips_and_omits_when_unset() {
let s = ServerSettings {
check_status_stale_days: Some(14),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, r#"{"check_status_stale_days":14}"#);
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
assert!(
!serde_json::to_string(&ServerSettings::default())
.unwrap()
.contains("check_status_stale_days")
);
}
#[test]
fn support_codes_absent_by_default_and_omitted_from_the_wire() {
let s = ServerSettings::default();
assert!(s.support_codes.is_empty());
assert_eq!(serde_json::to_string(&s).unwrap(), "{}");
assert!(s.support_code("support").is_none());
}
#[test]
fn support_code_lookup_fails_closed() {
let s = ServerSettings {
support_codes: vec![
SupportCode {
scope: "support".into(),
hash: "$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA".into(),
label: Some("ヘルプデスク".into()),
..Default::default()
},
SupportCode {
scope: "admin".into(),
hash: "$argon2id$v=19$m=19456,t=2,p=1$c2FsdA$aGFzaA".into(),
disabled: true,
..Default::default()
},
SupportCode {
scope: "blank".into(),
..Default::default()
},
],
..Default::default()
};
assert!(s.support_code("support").is_some());
assert!(s.support_code("admin").is_none(), "disabled must not match");
assert!(
s.support_code("blank").is_none(),
"blank hash must not match"
);
assert!(s.support_code("nope").is_none());
}
#[test]
fn redacted_blanks_hashes_but_keeps_the_roster() {
let s = ServerSettings {
support_codes: vec![SupportCode {
scope: "support".into(),
hash: "$argon2id$secret".into(),
label: Some("ヘルプデスク".into()),
ttl_minutes: Some(30),
disabled: false,
}],
..Default::default()
};
let json = serde_json::to_string(&s.redacted()).unwrap();
assert!(!json.contains("argon2"), "wire leaked the hash: {json}");
assert!(!json.contains("hash"), "wire leaked the hash key: {json}");
assert!(json.contains("\"scope\":\"support\""), "wire: {json}");
assert!(json.contains("\"ttl_minutes\":30"), "wire: {json}");
}
#[test]
fn support_code_ttl_clamps() {
let unset = SupportCode::default();
assert_eq!(
unset.effective_ttl_minutes(),
DEFAULT_SUPPORT_UNLOCK_TTL_MINUTES
);
let zero = SupportCode {
ttl_minutes: Some(0),
..Default::default()
};
assert_eq!(zero.effective_ttl_minutes(), 1);
let huge = SupportCode {
ttl_minutes: Some(u32::MAX),
..Default::default()
};
assert_eq!(huge.effective_ttl_minutes(), MAX_SUPPORT_UNLOCK_TTL_MINUTES);
}
#[test]
fn accepts_unknown_fields_for_forward_compat() {
let json = r#"{"agent_prune_days":7,"some_future_knob":true}"#;
let s: ServerSettings = serde_json::from_str(json).unwrap();
assert_eq!(s.agent_prune_days, Some(7));
}
#[test]
fn object_store_caps_unset_resolves_to_builtin_defaults() {
let s = ServerSettings::default();
assert_eq!(s.object_store_caps, None);
let c = s.effective_object_store_caps();
assert_eq!(c.result_output_mib, Some(DEFAULT_RESULT_OUTPUT_CAP_MIB));
assert_eq!(c.agent_releases_mib, Some(DEFAULT_AGENT_RELEASES_CAP_MIB));
assert_eq!(c.app_packages_mib, Some(DEFAULT_APP_PACKAGES_CAP_MIB));
assert_eq!(c.scripts_mib, Some(DEFAULT_SCRIPTS_CAP_MIB));
assert_eq!(c.collections_mib, Some(DEFAULT_COLLECTIONS_CAP_MIB));
}
#[test]
fn object_store_caps_partial_override_keeps_other_defaults() {
let s = ServerSettings {
object_store_caps: Some(ObjectStoreCaps {
app_packages_mib: Some(8192),
..Default::default()
}),
..Default::default()
};
let c = s.effective_object_store_caps();
assert_eq!(c.app_packages_mib, Some(8192));
assert_eq!(c.scripts_mib, Some(DEFAULT_SCRIPTS_CAP_MIB));
}
#[test]
fn object_store_caps_clamps_out_of_band_writes() {
let s = ServerSettings {
object_store_caps: Some(ObjectStoreCaps {
result_output_mib: Some(0),
app_packages_mib: Some(u32::MAX),
..Default::default()
}),
..Default::default()
};
let c = s.effective_object_store_caps();
assert_eq!(c.result_output_mib, Some(1));
assert_eq!(c.app_packages_mib, Some(MAX_OBJECT_STORE_CAP_MIB));
}
#[test]
fn object_store_caps_round_trips_and_omits_when_unset() {
let s = ServerSettings {
object_store_caps: Some(ObjectStoreCaps {
scripts_mib: Some(512),
..Default::default()
}),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(json, r#"{"object_store_caps":{"scripts_mib":512}}"#);
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
assert!(
!serde_json::to_string(&ServerSettings::default())
.unwrap()
.contains("object_store_caps")
);
}
#[test]
fn agent_install_defaults_to_unset_and_omits_when_unset() {
assert_eq!(ServerSettings::default().agent_install, None);
assert_eq!(ServerSettings::defaults().agent_install, None);
let s: ServerSettings = serde_json::from_str(r#"{"agent_prune_days":7}"#).unwrap();
assert_eq!(s.agent_install, None);
assert!(
!serde_json::to_string(&ServerSettings::default())
.unwrap()
.contains("agent_install")
);
}
#[test]
fn agent_install_round_trips() {
let s = ServerSettings {
agent_install: Some(AgentInstallSection {
nats_url: Some("nats://broker.corp:4222".into()),
nats_token: Some("s3cret".into()),
nats_token_set: false,
require_signed_commands: None,
..Default::default()
}),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(
json,
r#"{"agent_install":{"nats_url":"nats://broker.corp:4222","nats_token":"s3cret","nats_token_set":false,"nats_user_set":false,"nats_password_set":false}}"#
);
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
}
#[test]
fn agent_install_require_signed_commands_round_trips() {
let s = ServerSettings {
agent_install: Some(AgentInstallSection {
require_signed_commands: Some(true),
..Default::default()
}),
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
assert_eq!(
json,
r#"{"agent_install":{"nats_token_set":false,"nats_user_set":false,"nats_password_set":false,"require_signed_commands":true}}"#
);
assert_eq!(serde_json::from_str::<ServerSettings>(&json).unwrap(), s);
}
#[test]
fn agent_install_token_set_is_never_accepted_from_the_wire() {
let s: ServerSettings = serde_json::from_str(
r#"{"agent_install":{"nats_url":"nats://b:4222","nats_token_set":true}}"#,
)
.unwrap();
let ai = s.agent_install.unwrap();
assert!(!ai.nats_token_set);
assert_eq!(ai.nats_url.as_deref(), Some("nats://b:4222"));
}
#[test]
fn redacted_strips_the_install_token_but_reports_its_presence() {
let with_token = ServerSettings {
agent_install: Some(AgentInstallSection {
nats_url: Some("nats://broker.corp:4222".into()),
nats_token: Some("s3cret".into()),
nats_token_set: false,
require_signed_commands: None,
..Default::default()
}),
..Default::default()
};
let redacted = with_token.redacted();
let ai = redacted.agent_install.as_ref().unwrap();
assert_eq!(ai.nats_token, None, "token must not survive redacted()");
assert!(ai.nats_token_set);
assert_eq!(ai.nats_url.as_deref(), Some("nats://broker.corp:4222"));
let json = serde_json::to_string(&redacted).unwrap();
assert!(!json.contains("s3cret"), "wire leaked the token: {json}");
assert!(!json.contains("nats_token\""), "wire: {json}");
assert!(json.contains(r#""nats_token_set":true"#), "wire: {json}");
let sans_token = ServerSettings {
agent_install: Some(AgentInstallSection {
nats_url: Some("nats://broker.corp:4222".into()),
..Default::default()
}),
..Default::default()
}
.redacted();
let ai = sans_token.agent_install.as_ref().unwrap();
assert!(!ai.nats_token_set);
}
#[test]
fn redacted_strips_the_user_pair_but_reports_presence() {
let redacted = ServerSettings {
agent_install: Some(AgentInstallSection {
nats_user: Some("agent-u".into()),
nats_password: Some("p@ss'w0rd".into()),
..Default::default()
}),
..Default::default()
}
.redacted();
let ai = redacted.agent_install.as_ref().unwrap();
assert_eq!(ai.nats_user, None);
assert_eq!(ai.nats_password, None);
assert!(ai.nats_user_set && ai.nats_password_set);
let json = serde_json::to_string(&redacted).unwrap();
assert!(!json.contains("agent-u"), "wire leaked the user: {json}");
assert!(!json.contains("w0rd"), "wire leaked the password: {json}");
assert!(json.contains(r#""nats_user_set":true"#), "wire: {json}");
assert!(json.contains(r#""nats_password_set":true"#), "wire: {json}");
}
#[test]
fn user_pair_flags_are_never_accepted_from_the_wire() {
let s: ServerSettings = serde_json::from_str(
r#"{"agent_install":{"nats_user_set":true,"nats_password_set":true}}"#,
)
.unwrap();
let ai = s.agent_install.unwrap();
assert!(!ai.nats_user_set && !ai.nats_password_set);
}
#[test]
fn debug_never_prints_the_credentials() {
let ai = AgentInstallSection {
nats_token: Some("tok-secret".into()),
nats_user: Some("user-secret".into()),
nats_password: Some("pw-secret".into()),
..Default::default()
};
let dbg = format!("{ai:?}");
for needle in ["tok-secret", "user-secret", "pw-secret"] {
assert!(!dbg.contains(needle), "Debug leaked {needle}: {dbg}");
}
}
}