From e55d98522eaf2a60db5eea0810427310e5fed0a4 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 17 Jul 2026 20:03:27 -0500 Subject: [PATCH 1/4] Preserve Prebid bidder parameter string types --- crates/trusted-server-core/src/settings.rs | 61 ++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index c49e99686..c186a6501 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -191,7 +191,19 @@ impl IntegrationSettings { match value { JsonValue::Object(map) => JsonValue::Object( map.into_iter() - .map(|(key, val)| (key, Self::normalize_env_value(val))) + .map(|(key, value)| { + let value = if matches!( + key.as_str(), + "bid_param_overrides" + | "bid_param_zone_overrides" + | "bid_param_override_rules" + ) { + Self::normalize_opaque_json_value(value) + } else { + Self::normalize_env_value(value) + }; + (key, value) + }) .collect(), ), JsonValue::Array(items) => { @@ -208,6 +220,15 @@ impl IntegrationSettings { } } + fn normalize_opaque_json_value(value: JsonValue) -> JsonValue { + match value { + JsonValue::String(raw) => { + serde_json::from_str::(&raw).unwrap_or_else(|_| JsonValue::String(raw)) + } + other => other, + } + } + /// Normalizes all entries in place, converting JSON-encoded strings from /// environment variables into their proper typed representations. /// Called eagerly after deserialization so that TOML serialization in @@ -3198,6 +3219,38 @@ origin_host_header_overide = "www.example.com""#, ); } + #[test] + fn prebid_numeric_string_bid_param_overrides_survive_config_roundtrip() { + let toml_str = format!( + r#"{} + +[integrations.prebid.bid_param_overrides.pubmatic] +publisherId = "12345" +adSlot = "67890" +"#, + crate_test_settings_str() + ); + let settings = Settings::from_toml(&toml_str).expect("should parse TOML settings"); + let serialized = serde_json::to_value(&settings).expect("should serialize settings"); + let runtime_settings = + Settings::from_json_value(serialized).expect("should parse runtime JSON settings"); + let raw = runtime_settings + .integrations + .get("prebid") + .expect("should contain Prebid settings"); + + assert_eq!( + raw["bid_param_overrides"]["pubmatic"]["publisherId"], + json!("12345"), + "should preserve numeric-looking publisherId as a string" + ); + assert_eq!( + raw["bid_param_overrides"]["pubmatic"]["adSlot"], + json!("67890"), + "should preserve numeric-looking adSlot as a string" + ); + } + #[test] fn test_prebid_bid_param_overrides_override_with_json_env() { let toml_str = crate_test_settings_str(); @@ -3221,7 +3274,7 @@ origin_host_header_overide = "www.example.com""#, || { temp_env::with_var( env_key, - Some(r#"{"criteo":{"networkId":99999,"pubid":"server-pub"}}"#), + Some(r#"{"criteo":{"networkId":99999,"pubid":"24680"}}"#), || { let settings = Settings::from_toml_and_env(&toml_str) .expect("Settings should parse with bidder param override env"); @@ -3239,8 +3292,8 @@ origin_host_header_overide = "www.example.com""#, ); assert_eq!( cfg_json["bid_param_overrides"]["criteo"]["pubid"], - json!("server-pub"), - "should deserialize pubid override from env JSON" + json!("24680"), + "should preserve numeric-looking pubid override from env JSON as a string" ); }, ); From 5eef5f166cd67d3daf645f28760434adbf5e1c30 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 17 Jul 2026 20:07:29 -0500 Subject: [PATCH 2/4] Satisfy Clippy for override normalization --- crates/trusted-server-core/src/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index c186a6501..e8743e7c4 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -223,7 +223,7 @@ impl IntegrationSettings { fn normalize_opaque_json_value(value: JsonValue) -> JsonValue { match value { JsonValue::String(raw) => { - serde_json::from_str::(&raw).unwrap_or_else(|_| JsonValue::String(raw)) + serde_json::from_str::(&raw).unwrap_or(JsonValue::String(raw)) } other => other, } From 02c985fcd67b59033312c1dd61963e08f7f63204 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 21 Jul 2026 09:31:26 -0500 Subject: [PATCH 3/4] Simplify Prebid bid parameter normalization --- .../src/integrations/prebid.rs | 10 -- crates/trusted-server-core/src/settings.rs | 155 +++--------------- 2 files changed, 21 insertions(+), 144 deletions(-) diff --git a/crates/trusted-server-core/src/integrations/prebid.rs b/crates/trusted-server-core/src/integrations/prebid.rs index 903b4f355..cfc73548f 100644 --- a/crates/trusted-server-core/src/integrations/prebid.rs +++ b/crates/trusted-server-core/src/integrations/prebid.rs @@ -289,11 +289,6 @@ pub struct PrebidIntegrationConfig { /// param1 = 12345 /// param2 = "value" /// ``` - /// - /// Example via environment variable: - /// ```text - /// TRUSTED_SERVER__INTEGRATIONS__PREBID__BID_PARAM_OVERRIDES='{"bidder-name":{"param1":12345,"param2":"value"}}' - /// ``` #[serde(default)] pub bid_param_overrides: HashMap>, /// Canonical ordered bidder-param override rules. @@ -311,11 +306,6 @@ pub struct PrebidIntegrationConfig { /// when.zone = "header" /// set = { placementId = "_abc" } /// ``` - /// - /// Example via environment variable: - /// ```text - /// TRUSTED_SERVER__INTEGRATIONS__PREBID__BID_PARAM_OVERRIDE_RULES='[{"when":{"bidder":"kargo","zone":"header"},"set":{"placementId":"_abc"}}]' - /// ``` #[serde(default)] pub bid_param_override_rules: Vec, /// How consent signals are forwarded to Prebid Server. diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index e8743e7c4..67fff82b9 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -187,23 +187,12 @@ impl IntegrationSettings { Ok(()) } + #[cfg(test)] fn normalize_env_value(value: JsonValue) -> JsonValue { match value { JsonValue::Object(map) => JsonValue::Object( map.into_iter() - .map(|(key, value)| { - let value = if matches!( - key.as_str(), - "bid_param_overrides" - | "bid_param_zone_overrides" - | "bid_param_override_rules" - ) { - Self::normalize_opaque_json_value(value) - } else { - Self::normalize_env_value(value) - }; - (key, value) - }) + .map(|(key, value)| (key, Self::normalize_env_value(value))) .collect(), ), JsonValue::Array(items) => { @@ -220,20 +209,8 @@ impl IntegrationSettings { } } - fn normalize_opaque_json_value(value: JsonValue) -> JsonValue { - match value { - JsonValue::String(raw) => { - serde_json::from_str::(&raw).unwrap_or(JsonValue::String(raw)) - } - other => other, - } - } - - /// Normalizes all entries in place, converting JSON-encoded strings from - /// environment variables into their proper typed representations. - /// Called eagerly after deserialization so that TOML serialization in - /// build.rs preserves correct types. - pub fn normalize(&mut self) { + #[cfg(test)] + fn normalize_legacy_env(&mut self) { for value in self.entries.values_mut() { *value = Self::normalize_env_value(value.clone()); } @@ -2046,12 +2023,13 @@ impl Settings { .change_context(TrustedServerError::Configuration { message: "Failed to build configuration".to_string(), })?; - let settings: Self = + let mut settings: Self = config .try_deserialize() .change_context(TrustedServerError::Configuration { message: "Failed to deserialize configuration".to_string(), })?; + settings.integrations.normalize_legacy_env(); Self::finalize_deserialized(settings, "Build-time configuration") } @@ -2060,7 +2038,6 @@ impl Settings { mut settings: Self, validation_label: &str, ) -> Result> { - settings.integrations.normalize(); settings.proxy.normalize(); settings.image_optimizer.normalize(); settings.consent.validate(); @@ -3227,6 +3204,13 @@ origin_host_header_overide = "www.example.com""#, [integrations.prebid.bid_param_overrides.pubmatic] publisherId = "12345" adSlot = "67890" + +[integrations.prebid.bid_param_zone_overrides.pubmatic] +header = {{ placementId = "24680" }} + +[[integrations.prebid.bid_param_override_rules]] +when = {{ bidder = "pubmatic", zone = "in_content" }} +set = {{ placementId = "13579" }} "#, crate_test_settings_str() ); @@ -3249,112 +3233,15 @@ adSlot = "67890" json!("67890"), "should preserve numeric-looking adSlot as a string" ); - } - - #[test] - fn test_prebid_bid_param_overrides_override_with_json_env() { - let toml_str = crate_test_settings_str(); - let env_key = format!( - "{}{}INTEGRATIONS{}PREBID{}BID_PARAM_OVERRIDES", - ENVIRONMENT_VARIABLE_PREFIX, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR - ); - - let origin_key = format!( - "{}{}PUBLISHER{}ORIGIN_URL", - ENVIRONMENT_VARIABLE_PREFIX, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR - ); - temp_env::with_var( - origin_key, - Some("https://origin.test-publisher.com"), - || { - temp_env::with_var( - env_key, - Some(r#"{"criteo":{"networkId":99999,"pubid":"24680"}}"#), - || { - let settings = Settings::from_toml_and_env(&toml_str) - .expect("Settings should parse with bidder param override env"); - let cfg = settings - .integration_config::("prebid") - .expect("Prebid config query should succeed") - .expect("Prebid config should exist with env override"); - let cfg_json = - serde_json::to_value(&cfg).expect("should serialize config to JSON"); - - assert_eq!( - cfg_json["bid_param_overrides"]["criteo"]["networkId"], - json!(99999), - "should deserialize networkId override from env JSON" - ); - assert_eq!( - cfg_json["bid_param_overrides"]["criteo"]["pubid"], - json!("24680"), - "should preserve numeric-looking pubid override from env JSON as a string" - ); - }, - ); - }, - ); - } - - #[test] - fn test_prebid_bid_param_override_rules_override_with_json_env() { - let toml_str = crate_test_settings_str(); - let env_key = format!( - "{}{}INTEGRATIONS{}PREBID{}BID_PARAM_OVERRIDE_RULES", - ENVIRONMENT_VARIABLE_PREFIX, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR - ); - - let origin_key = format!( - "{}{}PUBLISHER{}ORIGIN_URL", - ENVIRONMENT_VARIABLE_PREFIX, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR + assert_eq!( + raw["bid_param_zone_overrides"]["pubmatic"]["header"]["placementId"], + json!("24680"), + "should preserve numeric-looking zone override parameters as strings" ); - temp_env::with_var( - origin_key, - Some("https://origin.test-publisher.com"), - || { - temp_env::with_var( - env_key, - Some( - r#"[{"when":{"bidder":"kargo","zone":"header"},"set":{"placementId":"server-header","keep":"yes"}}]"#, - ), - || { - let settings = Settings::from_toml_and_env(&toml_str) - .expect("Settings should parse canonical bidder param override rules"); - let cfg = settings - .integration_config::("prebid") - .expect("Prebid config query should succeed") - .expect("Prebid config should exist with env override"); - let cfg_json = - serde_json::to_value(&cfg).expect("should serialize config to JSON"); - - assert_eq!( - cfg_json["bid_param_override_rules"][0]["when"]["bidder"], - json!("kargo"), - "should deserialize bidder matcher from env JSON" - ); - assert_eq!( - cfg_json["bid_param_override_rules"][0]["when"]["zone"], - json!("header"), - "should deserialize zone matcher from env JSON" - ); - assert_eq!( - cfg_json["bid_param_override_rules"][0]["set"]["placementId"], - json!("server-header"), - "should deserialize set object from env JSON" - ); - }, - ); - }, + assert_eq!( + raw["bid_param_override_rules"][0]["set"]["placementId"], + json!("13579"), + "should preserve numeric-looking rule parameters as strings" ); } From e62ff9039301fcefc246cdb84e02fca7894b625a Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 27 Jul 2026 12:32:28 -0500 Subject: [PATCH 4/4] Preserve integration configuration scalar types --- CHANGELOG.md | 1 + .../src/integrations/prebid.rs | 85 ++++ crates/trusted-server-core/src/settings.rs | 427 +----------------- 3 files changed, 88 insertions(+), 425 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc49c80b..f19e0ee2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Breaking** — `bid_param_zone_overrides` inner values must now be JSON objects; previously non-object or empty values (`"header" = "x"`, `"header" = {}`) were accepted and silently produced a dead rule at runtime. They now fail at startup with a configuration error. Operators upgrading should audit their `bid_param_zone_overrides` config for non-object zone entries. +- **Breaking** — Integration configuration strings are no longer globally reinterpreted as JSON scalars. Operators upgrading should audit `[integrations.*]` settings and use native TOML/typed-config booleans and numbers (for example, `enabled = true`, not `enabled = "true"`); quoted numeric and boolean scalars now fail validation instead of silently converting. - **Breaking** — Sourcepoint browser module inclusion now requires explicit `[integrations.sourcepoint].enabled = true`; operators relying on the previous unconditional Sourcepoint module should enable the integration before upgrading. ### Security diff --git a/crates/trusted-server-core/src/integrations/prebid.rs b/crates/trusted-server-core/src/integrations/prebid.rs index c716933b0..73da261f3 100644 --- a/crates/trusted-server-core/src/integrations/prebid.rs +++ b/crates/trusted-server-core/src/integrations/prebid.rs @@ -5718,6 +5718,91 @@ set = { keywords = { genre = "news" } } ) } + #[test] + fn bid_param_override_numeric_strings_survive_runtime_config_roundtrip() { + let toml_str = format!( + r#"{} + +[integrations.prebid] +enabled = true +server_url = "https://prebid.example" +bidders = ["pubmatic"] + +[integrations.prebid.bid_param_overrides.pubmatic] +publisherId = "12345" +adSlot = "67890" + +[integrations.prebid.bid_param_zone_overrides.pubmatic] +header = {{ placementId = "24680" }} + +[[integrations.prebid.bid_param_override_rules]] +when = {{ bidder = "pubmatic", zone = "in_content" }} +set = {{ placementId = "13579" }} +"#, + TOML_BASE + ); + let settings = Settings::from_toml(&toml_str).expect("should parse TOML settings"); + + // This mirrors the typed data transition beneath `ts config push`: + // `BlobEnvelope` data is deserialized by `settings_from_config_blob`. + let serialized = serde_json::to_value(&settings).expect("should serialize settings"); + let runtime_settings = + Settings::from_json_value(serialized).expect("should parse runtime JSON settings"); + let config = runtime_settings + .integration_config::(PREBID_INTEGRATION_ID) + .expect("should parse Prebid config") + .expect("should enable Prebid config"); + + let static_request = make_auction_request(vec![make_ts_slot( + "ad-static-0", + &json!({ "pubmatic": { "keep": "client" } }), + None, + )]); + let static_ortb = call_to_openrtb(config.clone(), &static_request); + let static_params = bidder_params(&static_ortb); + assert_eq!( + static_params["pubmatic"]["publisherId"], + json!("12345"), + "static override publisherId should remain a string" + ); + assert_eq!( + static_params["pubmatic"]["adSlot"], + json!("67890"), + "static override adSlot should remain a string" + ); + assert_eq!( + static_params["pubmatic"]["keep"], + json!("client"), + "static override should preserve client parameters" + ); + + let header_request = make_auction_request(vec![make_ts_slot( + "ad-header-0", + &json!({ "pubmatic": {} }), + Some("header"), + )]); + let header_ortb = call_to_openrtb(config.clone(), &header_request); + let header_params = bidder_params(&header_ortb); + assert_eq!( + header_params["pubmatic"]["placementId"], + json!("24680"), + "zone override placementId should remain a string" + ); + + let in_content_request = make_auction_request(vec![make_ts_slot( + "ad-in-content-0", + &json!({ "pubmatic": {} }), + Some("in_content"), + )]); + let in_content_ortb = call_to_openrtb(config, &in_content_request); + let in_content_params = bidder_params(&in_content_ortb); + assert_eq!( + in_content_params["pubmatic"]["placementId"], + json!("13579"), + "canonical rule placementId should remain a string" + ); + } + #[test] fn zone_override_replaces_placement_id() { let mut config = base_config(); diff --git a/crates/trusted-server-core/src/settings.rs b/crates/trusted-server-core/src/settings.rs index 70c55cbcf..5961ffcae 100644 --- a/crates/trusted-server-core/src/settings.rs +++ b/crates/trusted-server-core/src/settings.rs @@ -187,35 +187,6 @@ impl IntegrationSettings { Ok(()) } - #[cfg(test)] - fn normalize_env_value(value: JsonValue) -> JsonValue { - match value { - JsonValue::Object(map) => JsonValue::Object( - map.into_iter() - .map(|(key, value)| (key, Self::normalize_env_value(value))) - .collect(), - ), - JsonValue::Array(items) => { - JsonValue::Array(items.into_iter().map(Self::normalize_env_value).collect()) - } - JsonValue::String(raw) => { - if let Ok(parsed) = serde_json::from_str::(&raw) { - parsed - } else { - JsonValue::String(raw) - } - } - other => other, - } - } - - #[cfg(test)] - fn normalize_legacy_env(&mut self) { - for value in self.entries.values_mut() { - *value = Self::normalize_env_value(value.clone()); - } - } - fn is_explicitly_disabled(raw: &JsonValue) -> bool { raw.as_object() .and_then(|map| map.get("enabled")) @@ -2028,13 +1999,12 @@ impl Settings { .change_context(TrustedServerError::Configuration { message: "Failed to build configuration".to_string(), })?; - let mut settings: Self = + let settings: Self = config .try_deserialize() .change_context(TrustedServerError::Configuration { message: "Failed to deserialize configuration".to_string(), })?; - settings.integrations.normalize_legacy_env(); Self::finalize_deserialized(settings, "Build-time configuration") } @@ -2600,12 +2570,8 @@ mod tests { use crate::auction::build_orchestrator; use crate::integrations::{ - IntegrationRegistry, - datadome::{DataDomeConfig, ProtectionMatcherConfig}, - gpt::GptConfig, - nextjs::NextJsIntegrationConfig, + IntegrationRegistry, gpt::GptConfig, nextjs::NextJsIntegrationConfig, prebid::PrebidIntegrationConfig, - testlight::TestlightConfig, }; use crate::redacted::Redacted; use crate::test_support::tests::{crate_test_settings_str, create_test_settings}; @@ -3201,278 +3167,6 @@ origin_host_header_overide = "www.example.com""#, ); } - #[test] - fn prebid_numeric_string_bid_param_overrides_survive_config_roundtrip() { - let toml_str = format!( - r#"{} - -[integrations.prebid.bid_param_overrides.pubmatic] -publisherId = "12345" -adSlot = "67890" - -[integrations.prebid.bid_param_zone_overrides.pubmatic] -header = {{ placementId = "24680" }} - -[[integrations.prebid.bid_param_override_rules]] -when = {{ bidder = "pubmatic", zone = "in_content" }} -set = {{ placementId = "13579" }} -"#, - crate_test_settings_str() - ); - let settings = Settings::from_toml(&toml_str).expect("should parse TOML settings"); - let serialized = serde_json::to_value(&settings).expect("should serialize settings"); - let runtime_settings = - Settings::from_json_value(serialized).expect("should parse runtime JSON settings"); - let raw = runtime_settings - .integrations - .get("prebid") - .expect("should contain Prebid settings"); - - assert_eq!( - raw["bid_param_overrides"]["pubmatic"]["publisherId"], - json!("12345"), - "should preserve numeric-looking publisherId as a string" - ); - assert_eq!( - raw["bid_param_overrides"]["pubmatic"]["adSlot"], - json!("67890"), - "should preserve numeric-looking adSlot as a string" - ); - assert_eq!( - raw["bid_param_zone_overrides"]["pubmatic"]["header"]["placementId"], - json!("24680"), - "should preserve numeric-looking zone override parameters as strings" - ); - assert_eq!( - raw["bid_param_override_rules"][0]["set"]["placementId"], - json!("13579"), - "should preserve numeric-looking rule parameters as strings" - ); - } - - #[test] - fn test_datadome_protection_scope_overrides_with_json_env() { - let toml_str = crate_test_settings_str(); - let separator = ENVIRONMENT_VARIABLE_SEPARATOR; - let origin_key = format!( - "{}{}PUBLISHER{}ORIGIN_URL", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator - ); - let enabled_key = format!( - "{}{}INTEGRATIONS{}DATADOME{}ENABLED", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator, separator - ); - let enable_protection_key = format!( - "{}{}INTEGRATIONS{}DATADOME{}ENABLE_PROTECTION", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator, separator - ); - let excluded_methods_key = format!( - "{}{}INTEGRATIONS{}DATADOME{}PROTECTION_EXCLUDED_METHODS", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator, separator - ); - let cidr_sources_key = format!( - "{}{}INTEGRATIONS{}DATADOME{}PROTECTION_EXCLUDED_IP_CIDR_SOURCES", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator, separator - ); - let rules_key = format!( - "{}{}INTEGRATIONS{}DATADOME{}PROTECTION_EXCLUSION_RULES", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator, separator - ); - - temp_env::with_vars( - [ - (origin_key, Some("https://origin.test-publisher.com")), - (enabled_key, Some("true")), - (enable_protection_key, Some("true")), - (excluded_methods_key, Some(r#"["OPTIONS","TRACE"]"#)), - ( - cidr_sources_key, - Some(r#"[{"config_store":"datadome-ip-bypass","key":"googlebot_ips"}]"#), - ), - ( - rules_key, - Some( - r#"[{"id":"legacy-static-get-head","methods":["GET","HEAD"],"type":"path_regex","patterns":["(?i)\\.(css|js)$"]},{"id":"next-rsc","type":"query_param_non_empty","names":["_rsc"]}]"#, - ), - ), - ], - || { - let settings = Settings::from_toml_and_env(&toml_str) - .expect("Settings should parse DataDome JSON env overrides"); - let cfg = settings - .integration_config::("datadome") - .expect("DataDome config query should succeed") - .expect("DataDome config should exist with env override"); - - assert!(cfg.enabled, "should parse enabled override as bool"); - assert!( - cfg.enable_protection, - "should parse enable_protection override as bool" - ); - assert_eq!( - cfg.protection_excluded_methods, - vec!["OPTIONS".to_string(), "TRACE".to_string()], - "should parse method list from JSON env override" - ); - assert_eq!( - cfg.protection_excluded_ip_cidr_sources[0].config_store, "datadome-ip-bypass", - "should parse CIDR source config_store from JSON env override" - ); - assert_eq!( - cfg.protection_excluded_ip_cidr_sources[0].key, "googlebot_ips", - "should parse CIDR source key from JSON env override" - ); - assert_eq!( - cfg.protection_exclusion_rules.len(), - 2, - "should parse all structured rules from JSON env override" - ); - assert!(matches!( - &cfg.protection_exclusion_rules[0].matcher, - ProtectionMatcherConfig::PathRegex { patterns } - if patterns == &vec!["(?i)\\.(css|js)$".to_string()] - )); - assert!(matches!( - &cfg.protection_exclusion_rules[1].matcher, - ProtectionMatcherConfig::QueryParamNonEmpty { names } - if names == &vec!["_rsc".to_string()] - )); - }, - ); - } - - #[test] - fn test_datadome_protection_scope_overrides_with_indexed_env() { - let toml_str = crate_test_settings_str(); - let separator = ENVIRONMENT_VARIABLE_SEPARATOR; - let datadome_prefix = format!( - "{}{}INTEGRATIONS{}DATADOME{}", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator, separator - ); - let origin_key = format!( - "{}{}PUBLISHER{}ORIGIN_URL", - ENVIRONMENT_VARIABLE_PREFIX, separator, separator - ); - - temp_env::with_vars( - [ - (origin_key, Some("https://origin.test-publisher.com")), - (format!("{datadome_prefix}ENABLED"), Some("true")), - (format!("{datadome_prefix}ENABLE_PROTECTION"), Some("true")), - ( - format!("{datadome_prefix}PROTECTION_EXCLUDED_METHODS{separator}0"), - Some("OPTIONS"), - ), - ( - format!("{datadome_prefix}PROTECTION_EXCLUDED_METHODS{separator}1"), - Some("TRACE"), - ), - ( - format!("{datadome_prefix}PROTECTION_EXCLUDED_ASNS{separator}0"), - Some("19750"), - ), - ( - format!("{datadome_prefix}PROTECTION_EXCLUDED_IP_CIDRS{separator}0"), - Some("198.51.100.0/24"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUDED_IP_CIDR_SOURCES{separator}0{separator}CONFIG_STORE" - ), - Some("datadome-ip-bypass"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUDED_IP_CIDR_SOURCES{separator}0{separator}KEY" - ), - Some("googlebot_ips"), - ), - ( - format!("{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}0{separator}ID"), - Some("legacy-static-get-head"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}0{separator}METHODS{separator}0" - ), - Some("GET"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}0{separator}METHODS{separator}1" - ), - Some("HEAD"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}0{separator}TYPE" - ), - Some("path_regex"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}0{separator}PATTERNS{separator}0" - ), - Some(r"(?i)\.(css|js)$"), - ), - ( - format!("{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}1{separator}ID"), - Some("next-rsc"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}1{separator}TYPE" - ), - Some("query_param_non_empty"), - ), - ( - format!( - "{datadome_prefix}PROTECTION_EXCLUSION_RULES{separator}1{separator}NAMES{separator}0" - ), - Some("_rsc"), - ), - ], - || { - let settings = Settings::from_toml_and_env(&toml_str) - .expect("Settings should parse DataDome indexed env overrides"); - let cfg = settings - .integration_config::("datadome") - .expect("DataDome config query should succeed") - .expect("DataDome config should exist with indexed env override"); - - assert_eq!( - cfg.protection_excluded_methods, - vec!["OPTIONS".to_string(), "TRACE".to_string()], - "should parse indexed method list" - ); - assert_eq!( - cfg.protection_excluded_asns, - vec![19750], - "should parse indexed ASN list" - ); - assert_eq!( - cfg.protection_excluded_ip_cidrs, - vec!["198.51.100.0/24".to_string()], - "should parse indexed IP CIDR list" - ); - assert_eq!( - cfg.protection_excluded_ip_cidr_sources[0].key, "googlebot_ips", - "should parse indexed CIDR source list" - ); - assert!(matches!( - &cfg.protection_exclusion_rules[0].matcher, - ProtectionMatcherConfig::PathRegex { patterns } - if patterns == &vec!["(?i)\\.(css|js)$".to_string()] - )); - assert!(matches!( - &cfg.protection_exclusion_rules[1].matcher, - ProtectionMatcherConfig::QueryParamNonEmpty { names } - if names == &vec!["_rsc".to_string()] - )); - }, - ); - } - #[test] fn test_handlers_override_with_env() { let toml_str = crate_test_settings_str(); @@ -3996,67 +3690,6 @@ set = {{ placementId = "13579" }} ); } - #[test] - fn test_integration_settings_from_env() { - use crate::integrations::testlight::TestlightConfig; - - let toml_str = crate_test_settings_str(); - - let origin_key = format!( - "{}{}PUBLISHER{}ORIGIN_URL", - ENVIRONMENT_VARIABLE_PREFIX, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR - ); - - let integration_prefix = format!( - "{}{}INTEGRATIONS{}TESTLIGHT{}", - ENVIRONMENT_VARIABLE_PREFIX, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR - ); - - let endpoint_key = format!("{}ENDPOINT", integration_prefix); - let timeout_key = format!("{}TIMEOUT_MS", integration_prefix); - let rewrite_key = format!("{}REWRITE_SCRIPTS", integration_prefix); - let enabled_key = format!("{}ENABLED", integration_prefix); - - temp_env::with_var( - origin_key, - Some("https://origin.test-publisher.com"), - || { - temp_env::with_var( - endpoint_key, - Some("https://testlight-env.test/auction"), - || { - temp_env::with_var(timeout_key, Some("2500"), || { - temp_env::with_var(rewrite_key, Some("true"), || { - temp_env::with_var(enabled_key, Some("true"), || { - let settings = Settings::from_toml_and_env(&toml_str) - .expect("Settings should load"); - - let config = settings - .integration_config::("testlight") - .expect("integration parsing should succeed") - .expect("integration should be enabled"); - - assert_eq!( - config.endpoint, - "https://testlight-env.test/auction" - ); - assert_eq!(config.timeout_ms, 2500); - assert!(config.rewrite_scripts); - assert!(config.enabled); - }); - }); - }); - }, - ); - }, - ); - } - #[test] fn test_disabled_integration_does_not_register() { use crate::integrations::testlight::TestlightConfig; @@ -4218,62 +3851,6 @@ set = {{ placementId = "13579" }} ); } - /// Tests the full build.rs round-trip: env vars are baked into Settings - /// at build time via `from_toml_and_env`, serialized to TOML, then parsed - /// back at runtime via `from_toml`. Verifies that env-sourced integration - /// values (strings like "true") are normalized to proper types so the - /// serialized TOML has correct types. - #[test] - fn test_env_var_roundtrip_normalizes_integration_types() { - let toml_str = crate_test_settings_str(); - - let integration_prefix = format!( - "{}{}INTEGRATIONS{}TESTLIGHT{}", - ENVIRONMENT_VARIABLE_PREFIX, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR, - ENVIRONMENT_VARIABLE_SEPARATOR, - ); - let enabled_key = format!("{}ENABLED", integration_prefix); - let endpoint_key = format!("{}ENDPOINT", integration_prefix); - - temp_env::with_var(enabled_key, Some("true"), || { - temp_env::with_var( - endpoint_key, - Some("https://testlight-env.test/auction"), - || { - // Step 1: Parse with env vars (what build.rs does) - let settings = - Settings::from_toml_and_env(&toml_str).expect("Settings should parse"); - - // Verify normalization converted "true" to bool - let raw = settings.integrations.get("testlight").unwrap(); - assert!( - raw.get("enabled").unwrap().is_boolean(), - "enabled should be normalized to bool, got: {:?}", - raw.get("enabled") - ); - - // Step 2: Serialize to TOML (what build.rs does) - let merged_toml = - toml::to_string_pretty(&settings).expect("Should serialize to TOML"); - - // Step 3: Parse back (what runtime does) - let runtime_settings = - Settings::from_toml(&merged_toml).expect("Runtime should parse"); - - let config = runtime_settings - .integration_config::("testlight") - .expect("should get config") - .expect("should be enabled"); - - assert_eq!(config.endpoint, "https://testlight-env.test/auction"); - assert!(config.enabled); - }, - ); - }); - } - /// Verifies that `from_toml` does NOT read environment variables. /// The runtime path should only use the pre-built TOML. #[test]