diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a8201d..d9d066b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added basic unit tests +- Added publisher config ### Changed - Upgrade to rust 1.87.0 diff --git a/Cargo.lock b/Cargo.lock index 90acdec..14603d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1543,6 +1543,7 @@ dependencies = [ "http", "log", "log-fastly", + "regex", "serde", "serde_json", "sha2 0.10.9", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 3b52f74..6f24dac 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -27,4 +27,5 @@ tokio = { version = "1.46", features = ["sync", "macros", "io-util", "rt", "time url = "2.4.1" [dev-dependencies] +regex = "1.1.1" temp-env = "0.3.6" diff --git a/crates/common/src/cookies.rs b/crates/common/src/cookies.rs index 3ec61fa..b4494d8 100644 --- a/crates/common/src/cookies.rs +++ b/crates/common/src/cookies.rs @@ -2,6 +2,8 @@ use cookie::{Cookie, CookieJar}; use fastly::http::header; use fastly::Request; +use crate::settings::Settings; + const COOKIE_MAX_AGE: i32 = 365 * 24 * 60 * 60; // 1 year // return empty cookie jar for unparsable cookies @@ -31,15 +33,17 @@ pub fn handle_request_cookies(req: &Request) -> Option { } } -pub fn create_synthetic_cookie(synthetic_id: &str) -> String { +pub fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> String { format!( - "synthetic_id={}; Domain=.auburndao.com; Path=/; Secure; SameSite=Lax; Max-Age={}", - synthetic_id, COOKIE_MAX_AGE, + "synthetic_id={}; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age={}", + synthetic_id, settings.publisher.cookie_domain, COOKIE_MAX_AGE, ) } #[cfg(test)] mod tests { + use crate::test_support::tests::create_test_settings; + use super::*; #[test] @@ -113,10 +117,14 @@ mod tests { #[test] fn test_create_synthetic_cookie() { - let result = create_synthetic_cookie("12345"); + let settings = create_test_settings(); + let result = create_synthetic_cookie(&settings, "12345"); assert_eq!( result, - "synthetic_id=12345; Domain=.auburndao.com; Path=/; Secure; SameSite=Lax; Max-Age=31536000" + format!( + "synthetic_id=12345; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age={}", + settings.publisher.cookie_domain, COOKIE_MAX_AGE, + ) ); } } diff --git a/crates/common/src/gdpr.rs b/crates/common/src/gdpr.rs index 86e53fc..48b3c51 100644 --- a/crates/common/src/gdpr.rs +++ b/crates/common/src/gdpr.rs @@ -58,14 +58,15 @@ pub fn get_consent_from_request(req: &Request) -> Option { None } -pub fn create_consent_cookie(consent: &GdprConsent) -> String { +pub fn create_consent_cookie(settings: &Settings, consent: &GdprConsent) -> String { format!( - "gdpr_consent={}; Domain=.auburndao.com; Path=/; Secure; SameSite=Lax; Max-Age=31536000", - serde_json::to_string(consent).unwrap_or_default() + "gdpr_consent={}; Domain={}; Path=/; Secure; SameSite=Lax; Max-Age=31536000", + serde_json::to_string(consent).unwrap_or_default(), + settings.publisher.cookie_domain, ) } -pub fn handle_consent_request(_settings: &Settings, req: Request) -> Result { +pub fn handle_consent_request(settings: &Settings, req: Request) -> Result { match *req.get_method() { Method::GET => { // Return current consent status @@ -81,7 +82,10 @@ pub fn handle_consent_request(_settings: &Settings, req: Request) -> Result { @@ -132,23 +136,7 @@ mod tests { use super::*; use fastly::{Body, Request}; - fn create_test_settings() -> Settings { - Settings { - ad_server: crate::settings::AdServer { - ad_partner_url: "https://test.com".to_string(), - sync_url: "https://sync.test.com".to_string(), - }, - prebid: crate::settings::Prebid { - server_url: "https://prebid.test.com".to_string(), - }, - synthetic: crate::settings::Synthetic { - counter_store: "test-counter".to_string(), - opid_store: "test-opid".to_string(), - secret_key: "test-secret".to_string(), - template: "{{test}}".to_string(), - }, - } - } + use crate::test_support::tests::create_test_settings; #[test] fn test_gdpr_consent_default() { @@ -196,6 +184,7 @@ mod tests { #[test] fn test_create_consent_cookie() { + let settings = create_test_settings(); let consent = GdprConsent { analytics: true, advertising: true, @@ -204,9 +193,9 @@ mod tests { version: "1.0".to_string(), }; - let cookie = create_consent_cookie(&consent); + let cookie = create_consent_cookie(&settings, &consent); assert!(cookie.starts_with("gdpr_consent=")); - assert!(cookie.contains("Domain=.auburndao.com")); + assert!(cookie.contains(format!("Domain={}", settings.publisher.cookie_domain).as_str())); assert!(cookie.contains("Path=/")); assert!(cookie.contains("Secure")); assert!(cookie.contains("SameSite=Lax")); @@ -297,7 +286,10 @@ mod tests { let set_cookie = response.get_header_str(header::SET_COOKIE); assert!(set_cookie.is_some()); assert!(set_cookie.unwrap().contains("gdpr_consent=")); - assert!(set_cookie.unwrap().contains("Domain=.auburndao.com")); + + assert!(set_cookie + .unwrap() + .contains(format!("Domain={}", settings.publisher.cookie_domain).as_str())); // Check response body let body = response.into_body_str(); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 7dff572..3fde24a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -7,4 +7,5 @@ pub mod privacy; pub mod settings; pub mod synthetic; pub mod templates; +pub mod test_support; pub mod why; diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs index d418f2a..d94a60c 100644 --- a/crates/common/src/prebid.rs +++ b/crates/common/src/prebid.rs @@ -64,7 +64,7 @@ impl PrebidRequest { .and_then(|o| url::Url::parse(o).ok()) .and_then(|u| u.host_str().map(|h| h.to_string())) }) - .unwrap_or_else(|| "auburndao.com".to_string()); + .unwrap_or_else(|| settings.publisher.domain.clone()); log::info!("Detected domain: {}", domain); @@ -192,23 +192,7 @@ mod tests { use super::*; use fastly::Request; - fn create_test_settings() -> Settings { - Settings { - ad_server: crate::settings::AdServer { - ad_partner_url: "https://test.com".to_string(), - sync_url: "https://sync.test.com".to_string(), - }, - prebid: crate::settings::Prebid { - server_url: "https://prebid.test.com/openrtb2/auction".to_string(), - }, - synthetic: crate::settings::Synthetic { - counter_store: "test-counter".to_string(), - opid_store: "test-opid".to_string(), - secret_key: "test-secret-key".to_string(), - template: "{{client_ip}}:{{user_agent}}:{{first_party_id}}:{{auth_user_id}}:{{publisher_domain}}:{{accept_language}}".to_string(), - }, - } - } + use crate::test_support::tests::create_test_settings; #[test] fn test_prebid_request_new_with_full_headers() { @@ -258,31 +242,34 @@ mod tests { #[test] fn test_prebid_request_domain_fallback() { let settings = create_test_settings(); - let req = Request::get("https://example.com/test"); + let url = format!("https://{}", settings.publisher.domain); + let req = Request::get(url.clone()); // No referer or origin headers let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); - assert_eq!(prebid_req.domain, "auburndao.com"); - assert_eq!(prebid_req.origin, "https://auburndao.com"); + assert_eq!(prebid_req.domain, settings.publisher.domain); + assert_eq!(prebid_req.origin, url); } #[test] fn test_prebid_request_invalid_url_in_referer() { let settings = create_test_settings(); - let mut req = Request::get("https://example.com/test"); + let url = format!("https://{}/test", settings.publisher.domain); + let mut req = Request::get(url); req.set_header(header::REFERER, "not-a-valid-url"); let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); // Should fallback to default domain - assert_eq!(prebid_req.domain, "auburndao.com"); + assert_eq!(prebid_req.domain, settings.publisher.domain); } #[test] fn test_prebid_request_x_forwarded_for_parsing() { let settings = create_test_settings(); - let mut req = Request::get("https://example.com/test"); + let url = format!("https://{}/test", settings.publisher.domain); + let mut req = Request::get(url); req.set_header(HEADER_X_FORWARDED_FOR, "192.168.1.1, 10.0.0.1, 172.16.0.1"); let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); @@ -330,18 +317,19 @@ mod tests { #[test] fn test_prebid_request_edge_cases() { let settings = create_test_settings(); + let url = format!("https://{}/test", settings.publisher.domain); // Test with empty X-Forwarded-For - let mut req = Request::get("https://example.com/test"); + let mut req = Request::get(url.clone()); req.set_header(HEADER_X_FORWARDED_FOR, ""); let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); assert!(!prebid_req.client_ip.is_empty() || prebid_req.client_ip.is_empty()); // Test with malformed origin - let mut req2 = Request::get("https://example.com/test"); + let mut req2 = Request::get(url.clone()); req2.set_header(header::ORIGIN, "://invalid"); let prebid_req2 = PrebidRequest::new(&settings, &req2).unwrap(); - assert_eq!(prebid_req2.domain, "auburndao.com"); + assert_eq!(prebid_req2.domain, settings.publisher.domain); } // Note: Testing send_bid_request would require mocking the Fastly backend, diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index ae294a7..9bccb8b 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -10,6 +10,14 @@ pub struct AdServer { pub sync_url: String, } +#[derive(Debug, Deserialize)] +#[allow(unused)] +pub struct Publisher { + pub domain: String, + pub cookie_domain: String, + pub origin_url: String, +} + #[derive(Debug, Deserialize)] #[allow(unused)] pub struct Prebid { @@ -29,6 +37,7 @@ pub struct Synthetic { #[allow(unused)] pub struct Settings { pub ad_server: AdServer, + pub publisher: Publisher, pub prebid: Prebid, pub synthetic: Synthetic, } @@ -60,6 +69,9 @@ impl Settings { #[cfg(test)] mod tests { use super::*; + use regex::Regex; + + use crate::test_support::tests::crate_test_settings_str; #[test] fn test_settings_new() { @@ -71,6 +83,9 @@ mod tests { // Verify basic structure is loaded assert!(!settings.ad_server.ad_partner_url.is_empty()); assert!(!settings.ad_server.sync_url.is_empty()); + assert!(!settings.publisher.domain.is_empty()); + assert!(!settings.publisher.cookie_domain.is_empty()); + assert!(!settings.publisher.origin_url.is_empty()); assert!(!settings.prebid.server_url.is_empty()); assert!(!settings.synthetic.counter_store.is_empty()); assert!(!settings.synthetic.opid_store.is_empty()); @@ -80,58 +95,43 @@ mod tests { #[test] fn test_settings_from_valid_toml() { - let toml_str = r#" - [ad_server] - ad_partner_url = "https://example-ad.com/serve" - sync_url = "https://example-ad.com/sync" - - [prebid] - server_url = "https://prebid.example.com/openrtb2/auction" + let toml_str = crate_test_settings_str(); + let settings: Result = Settings::from_toml(&toml_str); - [synthetic] - counter_store = "test-counter-store" - opid_store = "test-opid-store" - secret_key = "test-secret-key-1234567890" - template = "{{client_ip}}:{{user_agent}}:{{first_party_id}}:{{auth_user_id}}:{{publisher_domain}}:{{accept_language}}" - "#; - - let settings = Settings::from_toml(toml_str); assert!(settings.is_ok()); let settings = settings.unwrap(); assert_eq!( settings.ad_server.ad_partner_url, - "https://example-ad.com/serve" + "https://test-adpartner.com" + ); + assert_eq!( + settings.ad_server.sync_url, + "https://test-adpartner.com/synthetic_id={{synthetic_id}}" ); - assert_eq!(settings.ad_server.sync_url, "https://example-ad.com/sync"); assert_eq!( settings.prebid.server_url, - "https://prebid.example.com/openrtb2/auction" + "https://test-prebid.com/openrtb2/auction" + ); + assert_eq!(settings.publisher.domain, "test-publisher.com"); + assert_eq!(settings.publisher.cookie_domain, ".test-publisher.com"); + assert_eq!( + settings.publisher.origin_url, + "https://origin.test-publisher.com" ); assert_eq!(settings.synthetic.counter_store, "test-counter-store"); assert_eq!(settings.synthetic.opid_store, "test-opid-store"); - assert_eq!(settings.synthetic.secret_key, "test-secret-key-1234567890"); + assert_eq!(settings.synthetic.secret_key, "test-secret-key"); assert!(settings.synthetic.template.contains("{{client_ip}}")); } #[test] fn test_settings_missing_required_fields() { - let toml_str = r#" - [ad_server] - ad_partner_url = "https://example-ad.com/serve" - # Missing sync_url - - [prebid] - server_url = "https://prebid.example.com/openrtb2/auction" - - [synthetic] - counter_store = "test-counter-store" - opid_store = "test-opid-store" - secret_key = "test-secret-key" - template = "{{client_ip}}" - "#; + let re = Regex::new(r"ad_partner_url = .*").unwrap(); + let toml_str = crate_test_settings_str(); + let toml_str = re.replace(&toml_str, ""); - let settings = Settings::from_toml(toml_str); + let settings = Settings::from_toml(&toml_str); assert!( settings.is_err(), "Should fail when required fields are missing" @@ -148,71 +148,43 @@ mod tests { #[test] fn test_settings_invalid_toml_syntax() { - let toml_str = r#" - [ad_server - ad_partner_url = "https://example-ad.com/serve" - "#; + let re = Regex::new(r"\]").unwrap(); + let toml_str = crate_test_settings_str(); + let toml_str = re.replace(&toml_str, ""); - let settings = Settings::from_toml(toml_str); + let settings = Settings::from_toml(&toml_str); assert!(settings.is_err(), "Should fail with invalid TOML syntax"); } #[test] fn test_settings_partial_config() { - let toml_str = r#" - [ad_server] - ad_partner_url = "https://example-ad.com/serve" - sync_url = "https://example-ad.com/sync" - "#; + let re = Regex::new(r"\[ad_server\]").unwrap(); + let toml_str = crate_test_settings_str(); + let toml_str = re.replace(&toml_str, ""); - let settings = Settings::from_toml(toml_str); + let settings = Settings::from_toml(&toml_str); assert!(settings.is_err(), "Should fail when sections are missing"); } #[test] fn test_settings_extra_fields() { - let toml_str = r#" - [ad_server] - ad_partner_url = "https://example-ad.com/serve" - sync_url = "https://example-ad.com/sync" - extra_field = "should be ignored" - - [prebid] - server_url = "https://prebid.example.com/openrtb2/auction" - - [synthetic] - counter_store = "test-counter-store" - opid_store = "test-opid-store" - secret_key = "test-secret-key-1234567890" - template = "{{client_ip}}" - "#; + let toml_str = crate_test_settings_str() + "\nhello = 1"; - let settings = Settings::from_toml(toml_str); + let settings = Settings::from_toml(&toml_str); assert!(settings.is_ok(), "Extra fields should be ignored"); } #[test] fn test_set_env() { - let toml_str = r#" - [ad_server] - # ad_partner_url will be set by env variable - sync_url = "https://example-ad.com/sync" - - [prebid] - server_url = "https://prebid.example.com/openrtb2/auction" - - [synthetic] - counter_store = "test-counter-stor e" - opid_store = "test-opid-store" - secret_key = "test-secret-key-1234567890" - template = "{{client_ip}}" - "#; + let re = Regex::new(r"ad_partner_url = .*").unwrap(); + let toml_str = crate_test_settings_str(); + let toml_str = re.replace(&toml_str, ""); temp_env::with_var( "TRUSTED_SERVER__AD_SERVER__AD_PARTNER_URL", Some("https://change-ad.com/serve"), || { - let settings = Settings::from_toml(toml_str); + let settings = Settings::from_toml(&toml_str); assert!(settings.is_ok(), "Settings should load from embedded TOML"); assert_eq!( @@ -225,26 +197,13 @@ mod tests { #[test] fn test_override_env() { - let toml_str = r#" - [ad_server] - ad_partner_url = "https://example-ad.com/serve" - sync_url = "https://example-ad.com/sync" - - [prebid] - server_url = "https://prebid.example.com/openrtb2/auction" - - [synthetic] - counter_store = "test-counter-stor e" - opid_store = "test-opid-store" - secret_key = "test-secret-key-1234567890" - template = "{{client_ip}}" - "#; + let toml_str = crate_test_settings_str(); temp_env::with_var( "TRUSTED_SERVER__AD_SERVER__AD_PARTNER_URL", Some("https://change-ad.com/serve"), || { - let settings = Settings::from_toml(toml_str); + let settings = Settings::from_toml(&toml_str); assert!(settings.is_ok(), "Settings should load from embedded TOML"); assert_eq!( diff --git a/crates/common/src/synthetic.rs b/crates/common/src/synthetic.rs index 59fbce9..5d0f79f 100644 --- a/crates/common/src/synthetic.rs +++ b/crates/common/src/synthetic.rs @@ -96,9 +96,11 @@ pub fn get_or_generate_synthetic_id(settings: &Settings, req: &Request) -> Strin #[cfg(test)] mod tests { use super::*; - use crate::constants::HEADER_X_PUB_USER_ID; use fastly::http::{HeaderName, HeaderValue}; + use crate::constants::HEADER_X_PUB_USER_ID; + use crate::test_support::tests::create_test_settings; + fn create_test_request(headers: Vec<(HeaderName, &str)>) -> Request { let mut req = Request::new("GET", "http://example.com"); for (key, value) in headers { @@ -108,32 +110,14 @@ mod tests { req } - fn create_settings() -> Settings { - Settings { - ad_server: crate::settings::AdServer { - ad_partner_url: "https://example.com".to_string(), - sync_url: "https://example.com/synthetic_id={{synthetic_id}}".to_string(), - }, - prebid: crate::settings::Prebid { - server_url: "https://example.com".to_string(), - }, - synthetic: crate::settings::Synthetic { - counter_store: "https://example.com".to_string(), - opid_store: "https://example.com".to_string(), - secret_key: "secret_key".to_string(), - template: "{{ client_ip }}:{{ user_agent }}:{{ first_party_id }}:{{ auth_user_id }}:{{ publisher_domain }}:{{ accept_language }}".to_string(), - }, - } - } - #[test] fn test_generate_synthetic_id() { - let settings: Settings = create_settings(); + let settings: Settings = create_test_settings(); let req = create_test_request(vec![ (header::USER_AGENT, "Mozilla/5.0"), (header::COOKIE, "pub_userid=12345"), (HEADER_X_PUB_USER_ID, "67890"), - (header::HOST, "example.com"), + (header::HOST, settings.publisher.domain.as_str()), (header::ACCEPT_LANGUAGE, "en-US,en;q=0.9"), ]); @@ -141,13 +125,13 @@ mod tests { log::info!("Generated synthetic ID: {}", synthetic_id); assert_eq!( synthetic_id, - "07cd73bb8c7db39753ab6b10198b10c3237a3f5a6d2232c6ce578f2c2a623e56" + "a1748067b3908f2c9e0f6ea30a341328ba4b84de45448b13d1007030df14a98e" ) } #[test] fn test_get_or_generate_synthetic_id_with_header() { - let settings = create_settings(); + let settings = create_test_settings(); let req = create_test_request(vec![( HEADER_SYNTHETIC_TRUSTED_SERVER, "existing_synthetic_id", @@ -159,7 +143,7 @@ mod tests { #[test] fn test_get_or_generate_synthetic_id_with_cookie() { - let settings = create_settings(); + let settings = create_test_settings(); let req = create_test_request(vec![(header::COOKIE, "synthetic_id=existing_cookie_id")]); let synthetic_id = get_or_generate_synthetic_id(&settings, &req); @@ -168,7 +152,7 @@ mod tests { #[test] fn test_get_or_generate_synthetic_id_generate_new() { - let settings = create_settings(); + let settings = create_test_settings(); let req = create_test_request(vec![]); let synthetic_id = get_or_generate_synthetic_id(&settings, &req); diff --git a/crates/common/src/test_support.rs b/crates/common/src/test_support.rs new file mode 100644 index 0000000..67f7262 --- /dev/null +++ b/crates/common/src/test_support.rs @@ -0,0 +1,49 @@ +#[cfg(test)] +pub mod tests { + use crate::settings::{AdServer, Prebid, Publisher, Settings, Synthetic}; + + pub fn crate_test_settings_str() -> String { + r#" + [ad_server] + ad_partner_url = "https://test-adpartner.com" + sync_url = "https://test-adpartner.com/synthetic_id={{synthetic_id}}" + + [publisher] + domain = "test-publisher.com" + cookie_domain = ".test-publisher.com" + origin_url= "https://origin.test-publisher.com" + + [prebid] + server_url = "https://test-prebid.com/openrtb2/auction" + + [synthetic] + counter_store = "test-counter-store" + opid_store = "test-opid-store" + secret_key = "test-secret-key" + template = "{{client_ip}}:{{user_agent}}:{{first_party_id}}:{{auth_user_id}}:{{publisher_domain}}:{{accept_language}}" + "#.to_string() + } + + pub fn create_test_settings() -> Settings { + Settings { + ad_server: AdServer { + ad_partner_url: "https://test-adpartner.com".into(), + sync_url: "https://test-adpartner.com/synthetic_id={{synthetic_id}}".to_string(), + }, + publisher: Publisher { + domain: "test-publisher.com".to_string(), + cookie_domain: ".test-publisher.com".to_string(), + origin_url: "origin.test-publisher.com".to_string(), + }, + prebid: Prebid { + server_url: "https://test-prebid.com/openrtb2/auction".to_string(), + }, + synthetic: Synthetic { + counter_store: "test_counter_store".to_string(), + opid_store: "test-opid-store".to_string(), + secret_key: "test-secret-key".to_string(), + template: "{{client_ip}}:{{user_agent}}:{{first_party_id}}:{{auth_user_id}}:{{publisher_domain}}:{{accept_language}}".to_string(), + }, + } + } +} diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index 3e69c4b..5ea5267 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -182,7 +182,10 @@ fn handle_main_page(settings: &Settings, mut req: Request) -> Result