use crate::diagnostic::Severity;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct PolicyConfig {
#[serde(default)]
pub diagnostics: DiagnosticsConfig,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub freshness: FreshnessConfig,
#[serde(default)]
pub supply_chain: SupplyChainConfig,
#[serde(default)]
pub registries: RegistriesConfig,
#[serde(default)]
pub network: NetworkConfig,
#[serde(default)]
pub license_policy: LicensePolicyConfig,
#[serde(default)]
pub typosquat: TyposquatConfig,
#[serde(default)]
pub gossip: GossipConfig,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PolicyConfigDiff {
pub workspace_registries_changed: bool,
pub nuget_user_profile_sources_changed: bool,
pub gitlab_instance_host_changed: bool,
}
impl PolicyConfig {
#[must_use]
pub fn diff(old: &Self, new: &Self) -> PolicyConfigDiff {
let Self {
diagnostics: new_diagnostics,
cache: new_cache,
freshness: new_freshness,
supply_chain: new_supply_chain,
registries: new_registries,
network: new_network,
license_policy: new_license_policy,
typosquat: new_typosquat,
gossip: new_gossip,
} = new;
let DiagnosticsConfig {
outdated_severity: _,
unknown_severity: _,
yanked_severity: _,
unsatisfiable_severity: _,
deprecated_severity: _,
mutable_ref_pin_severity: _,
mutable_ref_pin_enabled: _,
sha_comment_mismatch_severity: _,
vulnerabilities_enabled: _,
} = new_diagnostics;
let CacheConfig {
enabled: _,
fetch_timeout_secs: _,
max_concurrent_fetches: _,
} = new_cache;
let FreshnessConfig {
enabled: _,
cooldown_secs: _,
} = new_freshness;
let SupplyChainConfig { enabled: _ } = new_supply_chain;
let NetworkConfig { offline: _ } = new_network;
let LicensePolicyConfig { allow: _, deny: _ } = new_license_policy;
let TyposquatConfig { enabled: _ } = new_typosquat;
let GossipConfig { enabled: _ } = new_gossip;
let RegistriesConfig {
workspace_registries: new_workspace_registries,
nuget_user_profile_sources: new_nuget_user_profile_sources,
gitlab_instance_host: new_gitlab_instance_host,
} = new_registries;
let RegistriesConfig {
workspace_registries: old_workspace_registries,
nuget_user_profile_sources: old_nuget_user_profile_sources,
gitlab_instance_host: old_gitlab_instance_host,
} = &old.registries;
PolicyConfigDiff {
workspace_registries_changed: old_workspace_registries != new_workspace_registries,
nuget_user_profile_sources_changed: old_nuget_user_profile_sources
!= new_nuget_user_profile_sources,
gitlab_instance_host_changed: old_gitlab_instance_host != new_gitlab_instance_host,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
pub struct DiagnosticsConfig {
#[serde(default = "default_outdated_severity")]
pub outdated_severity: Severity,
#[serde(default = "default_unknown_severity")]
pub unknown_severity: Severity,
#[serde(default = "default_yanked_severity")]
pub yanked_severity: Severity,
#[serde(default = "default_unsatisfiable_severity")]
pub unsatisfiable_severity: Severity,
#[serde(default = "default_deprecated_severity")]
pub deprecated_severity: Severity,
#[serde(default = "default_mutable_ref_pin_severity")]
pub mutable_ref_pin_severity: Severity,
#[serde(default = "default_true")]
pub mutable_ref_pin_enabled: bool,
#[serde(default = "default_sha_comment_mismatch_severity")]
pub sha_comment_mismatch_severity: Severity,
#[serde(default = "default_true")]
pub vulnerabilities_enabled: bool,
}
impl Default for DiagnosticsConfig {
fn default() -> Self {
Self::new()
}
}
impl DiagnosticsConfig {
#[must_use]
pub const fn new() -> Self {
Self {
outdated_severity: default_outdated_severity(),
unknown_severity: default_unknown_severity(),
yanked_severity: default_yanked_severity(),
unsatisfiable_severity: default_unsatisfiable_severity(),
deprecated_severity: default_deprecated_severity(),
mutable_ref_pin_severity: default_mutable_ref_pin_severity(),
mutable_ref_pin_enabled: true,
sha_comment_mismatch_severity: default_sha_comment_mismatch_severity(),
vulnerabilities_enabled: true,
}
}
#[must_use]
pub const fn with_outdated_severity(mut self, outdated_severity: Severity) -> Self {
self.outdated_severity = outdated_severity;
self
}
#[must_use]
pub const fn with_unknown_severity(mut self, unknown_severity: Severity) -> Self {
self.unknown_severity = unknown_severity;
self
}
#[must_use]
pub const fn with_yanked_severity(mut self, yanked_severity: Severity) -> Self {
self.yanked_severity = yanked_severity;
self
}
#[must_use]
pub const fn with_unsatisfiable_severity(mut self, unsatisfiable_severity: Severity) -> Self {
self.unsatisfiable_severity = unsatisfiable_severity;
self
}
#[must_use]
pub const fn with_deprecated_severity(mut self, deprecated_severity: Severity) -> Self {
self.deprecated_severity = deprecated_severity;
self
}
#[must_use]
pub const fn with_mutable_ref_pin_severity(
mut self,
mutable_ref_pin_severity: Severity,
) -> Self {
self.mutable_ref_pin_severity = mutable_ref_pin_severity;
self
}
#[must_use]
pub const fn with_mutable_ref_pin_enabled(mut self, mutable_ref_pin_enabled: bool) -> Self {
self.mutable_ref_pin_enabled = mutable_ref_pin_enabled;
self
}
#[must_use]
pub const fn with_sha_comment_mismatch_severity(
mut self,
sha_comment_mismatch_severity: Severity,
) -> Self {
self.sha_comment_mismatch_severity = sha_comment_mismatch_severity;
self
}
#[must_use]
pub const fn with_vulnerabilities_enabled(mut self, vulnerabilities_enabled: bool) -> Self {
self.vulnerabilities_enabled = vulnerabilities_enabled;
self
}
#[must_use]
pub const fn to_severities(&self) -> crate::DiagnosticSeverities {
crate::DiagnosticSeverities::new()
.with_outdated(self.outdated_severity)
.with_unknown(self.unknown_severity)
.with_yanked(self.yanked_severity)
.with_unsatisfiable(self.unsatisfiable_severity)
.with_deprecated(self.deprecated_severity)
.with_mutable_ref_pin(self.mutable_ref_pin_severity)
.with_mutable_ref_pin_enabled(self.mutable_ref_pin_enabled)
.with_sha_comment_mismatch(self.sha_comment_mismatch_severity)
.with_vulnerabilities_enabled(self.vulnerabilities_enabled)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
pub struct CacheConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(
default = "default_fetch_timeout_secs",
deserialize_with = "deserialize_fetch_timeout"
)]
pub fetch_timeout_secs: u64,
#[serde(
default = "default_max_concurrent_fetches",
deserialize_with = "deserialize_max_concurrent"
)]
pub max_concurrent_fetches: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self::new()
}
}
impl CacheConfig {
#[must_use]
pub const fn new() -> Self {
Self {
enabled: true,
fetch_timeout_secs: default_fetch_timeout_secs(),
max_concurrent_fetches: default_max_concurrent_fetches(),
}
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub const fn with_fetch_timeout_secs(mut self, fetch_timeout_secs: u64) -> Self {
self.fetch_timeout_secs = fetch_timeout_secs;
self
}
#[must_use]
pub const fn with_max_concurrent_fetches(mut self, max_concurrent_fetches: usize) -> Self {
self.max_concurrent_fetches = if max_concurrent_fetches == 0 {
MIN_CONCURRENT_FETCHES
} else {
max_concurrent_fetches
};
self
}
}
const fn default_true() -> bool {
true
}
const fn default_outdated_severity() -> Severity {
Severity::Hint
}
const fn default_unknown_severity() -> Severity {
Severity::Warning
}
const fn default_yanked_severity() -> Severity {
Severity::Warning
}
const fn default_unsatisfiable_severity() -> Severity {
Severity::Warning
}
const fn default_deprecated_severity() -> Severity {
Severity::Warning
}
const fn default_mutable_ref_pin_severity() -> Severity {
Severity::Hint
}
const fn default_sha_comment_mismatch_severity() -> Severity {
Severity::Warning
}
const fn default_fetch_timeout_secs() -> u64 {
5
}
const fn default_max_concurrent_fetches() -> usize {
20
}
const MIN_FETCH_TIMEOUT_SECS: u64 = 1;
const MAX_FETCH_TIMEOUT_SECS: u64 = 300;
const MIN_CONCURRENT_FETCHES: usize = 1;
const MAX_CONCURRENT_FETCHES: usize = 100;
fn deserialize_fetch_timeout<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: serde::Deserializer<'de>,
{
let secs = u64::deserialize(deserializer)?;
let clamped = secs.clamp(MIN_FETCH_TIMEOUT_SECS, MAX_FETCH_TIMEOUT_SECS);
if clamped != secs {
tracing::warn!(
"fetch_timeout_secs {} clamped to {} (valid range: {}-{})",
secs,
clamped,
MIN_FETCH_TIMEOUT_SECS,
MAX_FETCH_TIMEOUT_SECS
);
}
Ok(clamped)
}
fn deserialize_max_concurrent<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
let count = usize::deserialize(deserializer)?;
let clamped = count.clamp(MIN_CONCURRENT_FETCHES, MAX_CONCURRENT_FETCHES);
if clamped != count {
tracing::warn!(
"max_concurrent_fetches {} clamped to {} (valid range: {}-{})",
count,
clamped,
MIN_CONCURRENT_FETCHES,
MAX_CONCURRENT_FETCHES
);
}
Ok(clamped)
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
pub struct FreshnessConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(
default = "default_cooldown_secs",
deserialize_with = "deserialize_cooldown_secs"
)]
pub cooldown_secs: u64,
}
impl Default for FreshnessConfig {
fn default() -> Self {
Self::new()
}
}
impl FreshnessConfig {
#[must_use]
pub const fn new() -> Self {
Self {
enabled: true,
cooldown_secs: default_cooldown_secs(),
}
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub const fn with_cooldown_secs(mut self, cooldown_secs: u64) -> Self {
self.cooldown_secs = cooldown_secs;
self
}
#[must_use]
pub const fn to_freshness(&self) -> crate::FreshnessSettings {
if self.enabled {
crate::FreshnessSettings::Enabled {
cooldown: crate::CooldownWindow::from_secs(self.cooldown_secs),
}
} else {
crate::FreshnessSettings::Disabled
}
}
}
const fn default_cooldown_secs() -> u64 {
crate::DEFAULT_COOLDOWN_SECS
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
pub struct SupplyChainConfig {
#[serde(default = "default_true")]
pub enabled: bool,
}
impl Default for SupplyChainConfig {
fn default() -> Self {
Self::new()
}
}
impl SupplyChainConfig {
#[must_use]
pub const fn new() -> Self {
Self { enabled: true }
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
const MIN_COOLDOWN_SECS: u64 = 0;
const MAX_COOLDOWN_SECS: u64 = 30 * 24 * 60 * 60;
fn deserialize_cooldown_secs<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: serde::Deserializer<'de>,
{
let secs = u64::deserialize(deserializer)?;
let clamped = secs.clamp(MIN_COOLDOWN_SECS, MAX_COOLDOWN_SECS);
if clamped != secs {
tracing::warn!(
"freshness.cooldown_secs {} clamped to {} (valid range: {}-{})",
secs,
clamped,
MIN_COOLDOWN_SECS,
MAX_COOLDOWN_SECS
);
}
Ok(clamped)
}
#[non_exhaustive]
#[derive(Clone, Deserialize, crate::redact_debug::RedactingDebug)]
pub struct RegistriesConfig {
#[serde(default)]
#[raw]
pub workspace_registries: WorkspaceRegistriesSetting,
#[serde(default)]
#[raw]
pub nuget_user_profile_sources: bool,
#[serde(default)]
#[redact(url)]
pub gitlab_instance_host: String,
}
impl Default for RegistriesConfig {
fn default() -> Self {
Self::new()
}
}
impl RegistriesConfig {
#[must_use]
pub fn new() -> Self {
Self {
workspace_registries: WorkspaceRegistriesSetting::PublicOnly,
nuget_user_profile_sources: false,
gitlab_instance_host: String::new(),
}
}
#[must_use]
pub const fn with_workspace_registries(
mut self,
workspace_registries: WorkspaceRegistriesSetting,
) -> Self {
self.workspace_registries = workspace_registries;
self
}
#[must_use]
pub const fn with_nuget_user_profile_sources(
mut self,
nuget_user_profile_sources: bool,
) -> Self {
self.nuget_user_profile_sources = nuget_user_profile_sources;
self
}
#[must_use]
pub fn with_gitlab_instance_host(mut self, gitlab_instance_host: impl Into<String>) -> Self {
self.gitlab_instance_host = gitlab_instance_host.into();
self
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct RegistryRuntimeSettings {
pub workspace_registries: crate::net_policy::WorkspaceRegistryAccess,
pub nuget_user_profile_sources: bool,
pub gitlab_instance_host: Option<String>,
}
impl std::fmt::Debug for RegistryRuntimeSettings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegistryRuntimeSettings")
.field("workspace_registries", &self.workspace_registries)
.field(
"nuget_user_profile_sources",
&self.nuget_user_profile_sources,
)
.field(
"gitlab_instance_host",
&self
.gitlab_instance_host
.as_deref()
.map(crate::redact::RedactedUrl::new),
)
.finish()
}
}
impl RegistriesConfig {
#[must_use]
pub fn resolve(&self) -> RegistryRuntimeSettings {
RegistryRuntimeSettings {
workspace_registries: self.workspace_registries.to_policy(),
nuget_user_profile_sources: self.nuget_user_profile_sources,
gitlab_instance_host: (!self.gitlab_instance_host.is_empty())
.then(|| self.gitlab_instance_host.clone()),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceRegistriesSetting {
Off,
#[default]
PublicOnly,
All,
}
impl WorkspaceRegistriesSetting {
#[must_use]
pub const fn to_policy(self) -> crate::net_policy::WorkspaceRegistryAccess {
match self {
Self::Off => crate::net_policy::WorkspaceRegistryAccess::Off,
Self::PublicOnly => crate::net_policy::WorkspaceRegistryAccess::PublicOnly,
Self::All => crate::net_policy::WorkspaceRegistryAccess::All,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
pub struct NetworkConfig {
#[serde(default)]
pub offline: bool,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self::new()
}
}
impl NetworkConfig {
#[must_use]
pub const fn new() -> Self {
Self { offline: false }
}
#[must_use]
pub const fn with_offline(mut self, offline: bool) -> Self {
self.offline = offline;
self
}
#[must_use]
pub fn mode(&self) -> crate::NetworkMode {
crate::NetworkMode::from_offline_flag(self.offline)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Default)]
pub struct LicensePolicyConfig {
#[serde(default, deserialize_with = "deserialize_spdx_list")]
pub allow: Vec<String>,
#[serde(default, deserialize_with = "deserialize_spdx_list")]
pub deny: Vec<String>,
}
impl LicensePolicyConfig {
#[must_use]
pub const fn new() -> Self {
Self {
allow: Vec::new(),
deny: Vec::new(),
}
}
#[must_use]
pub fn with_allow(mut self, allow: Vec<String>) -> Self {
self.allow = allow;
self
}
#[must_use]
pub fn with_deny(mut self, deny: Vec<String>) -> Self {
self.deny = deny;
self
}
#[must_use]
pub fn to_policy(&self) -> crate::LicensePolicy {
crate::LicensePolicy::new(self.allow.clone(), self.deny.clone())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Default)]
pub struct TyposquatConfig {
#[serde(default)]
pub enabled: bool,
}
impl TyposquatConfig {
#[must_use]
pub const fn new() -> Self {
Self { enabled: false }
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Default)]
pub struct GossipConfig {
#[serde(default)]
pub enabled: bool,
}
impl GossipConfig {
#[must_use]
pub const fn new() -> Self {
Self { enabled: false }
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
fn deserialize_spdx_list<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = Vec::<String>::deserialize(deserializer)?;
Ok(crate::licenses::filter_valid_spdx_ids(raw))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_config_defaults() {
let policy = PolicyConfig::default();
assert!(policy.diagnostics.vulnerabilities_enabled);
assert!(!policy.network.offline);
assert_eq!(
policy.registries.workspace_registries,
WorkspaceRegistriesSetting::PublicOnly
);
}
#[test]
fn test_workspace_registries_setting_deserializes_all_variants() {
assert_eq!(
serde_json::from_str::<WorkspaceRegistriesSetting>("\"off\"").unwrap(),
WorkspaceRegistriesSetting::Off
);
assert_eq!(
serde_json::from_str::<WorkspaceRegistriesSetting>("\"public_only\"").unwrap(),
WorkspaceRegistriesSetting::PublicOnly
);
assert_eq!(
serde_json::from_str::<WorkspaceRegistriesSetting>("\"all\"").unwrap(),
WorkspaceRegistriesSetting::All
);
}
#[test]
fn test_registries_config_debug_redacts_gitlab_instance_host_credential() {
let config = RegistriesConfig {
gitlab_instance_host: "user:hunter2@gitlab.corp".to_string(),
..RegistriesConfig::default()
};
let section_debug = format!("{config:?}");
assert!(!section_debug.contains("hunter2"), "{section_debug}");
assert!(section_debug.contains("gitlab.corp"), "{section_debug}");
}
#[test]
fn test_workspace_registries_setting_to_policy() {
use crate::net_policy::WorkspaceRegistryAccess;
assert_eq!(
WorkspaceRegistriesSetting::Off.to_policy(),
WorkspaceRegistryAccess::Off
);
assert_eq!(
WorkspaceRegistriesSetting::PublicOnly.to_policy(),
WorkspaceRegistryAccess::PublicOnly
);
assert_eq!(
WorkspaceRegistriesSetting::All.to_policy(),
WorkspaceRegistryAccess::All
);
}
#[test]
fn test_workspace_registry_access_default_matches_workspace_registries_setting_default() {
assert_eq!(
crate::net_policy::WorkspaceRegistryAccess::default(),
WorkspaceRegistriesSetting::default().to_policy()
);
}
#[test]
fn test_registries_config_resolve_normalizes_empty_gitlab_instance_host_to_none() {
let resolved = RegistriesConfig::default().resolve();
assert_eq!(resolved.gitlab_instance_host, None);
assert!(!resolved.nuget_user_profile_sources);
}
#[test]
fn test_registries_config_resolve_carries_non_default_settings() {
use crate::net_policy::WorkspaceRegistryAccess;
let config = RegistriesConfig {
workspace_registries: WorkspaceRegistriesSetting::All,
nuget_user_profile_sources: true,
gitlab_instance_host: "gitlab.corp".to_string(),
};
let resolved = config.resolve();
assert_eq!(resolved.workspace_registries, WorkspaceRegistryAccess::All);
assert!(resolved.nuget_user_profile_sources);
assert_eq!(
resolved.gitlab_instance_host.as_deref(),
Some("gitlab.corp")
);
}
#[test]
fn test_diagnostics_config_sha_comment_mismatch_severity_default_and_override() {
let default: DiagnosticsConfig = serde_json::from_str("{}").unwrap();
assert_eq!(default.sha_comment_mismatch_severity, Severity::Warning);
assert_eq!(
default.to_severities().sha_comment_mismatch,
Severity::Warning
);
let config: DiagnosticsConfig =
serde_json::from_str(r#"{ "sha_comment_mismatch_severity": 1 }"#).unwrap();
assert_eq!(config.sha_comment_mismatch_severity, Severity::Error);
assert_eq!(config.to_severities().sha_comment_mismatch, Severity::Error);
}
#[test]
fn test_diagnostics_config_deserialization() {
let json = r#"{
"outdated_severity": 1,
"unknown_severity": 2,
"yanked_severity": 2,
"unsatisfiable_severity": 1,
"deprecated_severity": 1
}"#;
let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.outdated_severity, Severity::Error);
assert_eq!(config.unknown_severity, Severity::Warning);
assert_eq!(config.yanked_severity, Severity::Warning);
assert_eq!(config.unsatisfiable_severity, Severity::Error);
assert_eq!(config.deprecated_severity, Severity::Error);
}
#[test]
fn test_diagnostics_config_mutable_ref_pin_enabled_can_be_disabled() {
let json = r#"{ "mutable_ref_pin_enabled": false }"#;
let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
assert!(!config.mutable_ref_pin_enabled);
}
#[test]
fn test_diagnostics_config_vulnerabilities_enabled_can_be_disabled() {
let json = r#"{ "vulnerabilities_enabled": false }"#;
let config: DiagnosticsConfig = serde_json::from_str(json).unwrap();
assert!(!config.vulnerabilities_enabled);
}
#[test]
fn test_cache_config_deserialization() {
let json = r#"{
"enabled": false
}"#;
let config: CacheConfig = serde_json::from_str(json).unwrap();
assert!(!config.enabled);
}
#[test]
fn test_cache_config_with_enabled() {
let config = CacheConfig::new().with_enabled(false);
assert!(!config.enabled);
}
#[test]
fn test_cache_config_defaults() {
let config = CacheConfig::default();
assert!(config.enabled);
assert_eq!(config.fetch_timeout_secs, 5);
assert_eq!(config.max_concurrent_fetches, 20);
}
#[test]
fn test_cache_config_fetch_timeout_clamped() {
let json = r#"{ "fetch_timeout_secs": 10000 }"#;
let config: CacheConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.fetch_timeout_secs, MAX_FETCH_TIMEOUT_SECS);
}
#[test]
fn test_cache_config_max_concurrent_fetches_clamped() {
let json = r#"{ "max_concurrent_fetches": 0 }"#;
let config: CacheConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.max_concurrent_fetches, MIN_CONCURRENT_FETCHES);
}
#[test]
fn test_freshness_config_defaults() {
let config = FreshnessConfig::default();
assert!(config.enabled);
assert_eq!(config.cooldown_secs, crate::DEFAULT_COOLDOWN_SECS);
}
#[test]
fn test_freshness_config_cooldown_clamped() {
let json = r#"{ "cooldown_secs": 99999999 }"#;
let config: FreshnessConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.cooldown_secs, MAX_COOLDOWN_SECS);
}
#[test]
fn test_supply_chain_config_defaults() {
assert!(SupplyChainConfig::default().enabled);
}
#[test]
fn test_supply_chain_config_with_enabled() {
assert!(!SupplyChainConfig::new().with_enabled(false).enabled);
}
#[test]
fn test_network_config_defaults() {
assert!(!NetworkConfig::default().offline);
}
#[test]
fn test_network_config_with_offline() {
assert!(NetworkConfig::new().with_offline(true).offline);
}
#[test]
fn test_gossip_config_defaults() {
assert!(!GossipConfig::default().enabled);
}
#[test]
fn test_gossip_config_with_enabled() {
assert!(GossipConfig::new().with_enabled(true).enabled);
}
#[test]
fn test_gossip_config_partial_object_parses() {
let config: PolicyConfig = serde_json::from_str(r#"{"gossip": {}}"#).unwrap();
assert!(!config.gossip.enabled);
}
#[test]
fn test_policy_config_diff_no_change_returns_all_false() {
let old = PolicyConfig::default();
let new = PolicyConfig::default();
assert_eq!(PolicyConfig::diff(&old, &new), PolicyConfigDiff::default());
}
#[test]
fn test_policy_config_diff_workspace_registries_change() {
let old = PolicyConfig::default();
let mut new = PolicyConfig::default();
new.registries.workspace_registries = WorkspaceRegistriesSetting::Off;
let diff = PolicyConfig::diff(&old, &new);
assert!(diff.workspace_registries_changed);
assert!(!diff.nuget_user_profile_sources_changed);
assert!(!diff.gitlab_instance_host_changed);
}
#[test]
fn test_policy_config_diff_nuget_user_profile_sources_change() {
let old = PolicyConfig::default();
let mut new = PolicyConfig::default();
new.registries.nuget_user_profile_sources = true;
let diff = PolicyConfig::diff(&old, &new);
assert!(!diff.workspace_registries_changed);
assert!(diff.nuget_user_profile_sources_changed);
assert!(!diff.gitlab_instance_host_changed);
}
#[test]
fn test_policy_config_diff_gitlab_instance_host_change() {
let old = PolicyConfig::default();
let mut new = PolicyConfig::default();
new.registries.gitlab_instance_host = "gitlab.corp".to_string();
let diff = PolicyConfig::diff(&old, &new);
assert!(!diff.workspace_registries_changed);
assert!(!diff.nuget_user_profile_sources_changed);
assert!(diff.gitlab_instance_host_changed);
}
#[test]
fn test_policy_config_diff_non_registries_change_returns_all_false() {
let old = PolicyConfig::default();
let mut new = PolicyConfig::default();
new.network.offline = true;
new.freshness.cooldown_secs = 60;
assert_eq!(PolicyConfig::diff(&old, &new), PolicyConfigDiff::default());
}
#[test]
fn test_license_policy_config_invalid_spdx_dropped() {
let json = r#"{ "allow": ["MIT", "not a valid spdx id!!"] }"#;
let config: LicensePolicyConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.allow, vec!["MIT".to_string()]);
}
crate::debug_redaction_conformance!(
test_registry_runtime_settings_debug_redacts_credentials,
1,
RegistryRuntimeSettings {
workspace_registries: crate::net_policy::WorkspaceRegistryAccess::PublicOnly,
nuget_user_profile_sources: false,
gitlab_instance_host: Some(crate::conformance::CREDENTIAL_PROBE_URL.to_string()),
},
);
}