From 4a2cf42c8ba2865c33d9af316f26ff1f4ccd9cea Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:08:39 -0700 Subject: [PATCH 1/5] Added unit tests --- crates/common/src/gdpr.rs | 271 ++++++++++++++++++++++++++++++++++ crates/common/src/models.rs | 242 ++++++++++++++++++++++++++++++ crates/common/src/prebid.rs | 162 ++++++++++++++++++++ crates/common/src/settings.rs | 175 ++++++++++++++++++++++ 4 files changed, 850 insertions(+) diff --git a/crates/common/src/gdpr.rs b/crates/common/src/gdpr.rs index bb92a1e..b2d710b 100644 --- a/crates/common/src/gdpr.rs +++ b/crates/common/src/gdpr.rs @@ -124,3 +124,274 @@ pub fn handle_data_subject_request(_settings: &Settings, req: Request) -> Result } } } + +#[cfg(test)] +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(), + }, + } + } + + #[test] + fn test_gdpr_consent_default() { + let consent = GdprConsent::default(); + assert!(!consent.analytics); + assert!(!consent.advertising); + assert!(!consent.functional); + assert_eq!(consent.version, "1.0"); + assert!(consent.timestamp > 0); + } + + #[test] + fn test_user_data_default() { + let data = UserData::default(); + assert_eq!(data.visit_count, 0); + assert!(data.last_visit > 0); + assert!(data.ad_interactions.is_empty()); + assert!(data.consent_history.is_empty()); + } + + #[test] + fn test_gdpr_consent_serialization() { + let consent = GdprConsent { + analytics: true, + advertising: false, + functional: true, + timestamp: 1234567890, + version: "2.0".to_string(), + }; + + let json = serde_json::to_string(&consent).unwrap(); + assert!(json.contains("\"analytics\":true")); + assert!(json.contains("\"advertising\":false")); + assert!(json.contains("\"functional\":true")); + assert!(json.contains("\"timestamp\":1234567890")); + assert!(json.contains("\"version\":\"2.0\"")); + + let deserialized: GdprConsent = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.analytics, consent.analytics); + assert_eq!(deserialized.advertising, consent.advertising); + assert_eq!(deserialized.functional, consent.functional); + assert_eq!(deserialized.timestamp, consent.timestamp); + assert_eq!(deserialized.version, consent.version); + } + + #[test] + fn test_create_consent_cookie() { + let consent = GdprConsent { + analytics: true, + advertising: true, + functional: true, + timestamp: 1234567890, + version: "1.0".to_string(), + }; + + let cookie = create_consent_cookie(&consent); + assert!(cookie.starts_with("gdpr_consent=")); + assert!(cookie.contains("Domain=.auburndao.com")); + assert!(cookie.contains("Path=/")); + assert!(cookie.contains("Secure")); + assert!(cookie.contains("SameSite=Lax")); + assert!(cookie.contains("Max-Age=31536000")); + } + + #[test] + fn test_get_consent_from_request_no_cookie() { + let req = Request::get("https://example.com"); + let consent = get_consent_from_request(&req); + assert!(consent.is_none()); + } + + #[test] + fn test_get_consent_from_request_with_valid_cookie() { + let mut req = Request::get("https://example.com"); + let consent_data = GdprConsent { + analytics: true, + advertising: false, + functional: true, + timestamp: 1234567890, + version: "1.0".to_string(), + }; + let cookie_value = format!( + "gdpr_consent={}", + serde_json::to_string(&consent_data).unwrap() + ); + req.set_header(header::COOKIE, cookie_value); + + let consent = get_consent_from_request(&req); + assert!(consent.is_some()); + let consent = consent.unwrap(); + assert_eq!(consent.analytics, true); + assert_eq!(consent.advertising, false); + assert_eq!(consent.functional, true); + } + + #[test] + fn test_get_consent_from_request_with_invalid_cookie() { + let mut req = Request::get("https://example.com"); + req.set_header(header::COOKIE, "gdpr_consent=invalid-json"); + + let consent = get_consent_from_request(&req); + assert!(consent.is_none()); + } + + #[test] + fn test_handle_consent_request_get() { + let settings = create_test_settings(); + let req = Request::get("https://example.com/gdpr/consent"); + + let response = handle_consent_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::OK); + assert_eq!( + response.get_header_str(header::CONTENT_TYPE), + Some("application/json") + ); + + let body = response.into_body_str(); + let consent: GdprConsent = serde_json::from_str(&body).unwrap(); + assert!(!consent.analytics); // Default values + assert!(!consent.advertising); + assert!(!consent.functional); + } + + #[test] + fn test_handle_consent_request_post() { + let settings = create_test_settings(); + let consent_data = GdprConsent { + analytics: true, + advertising: true, + functional: false, + timestamp: 1234567890, + version: "1.0".to_string(), + }; + + let mut req = Request::post("https://example.com/gdpr/consent"); + req.set_body(Body::from(serde_json::to_string(&consent_data).unwrap())); + + let response = handle_consent_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::OK); + assert_eq!( + response.get_header_str(header::CONTENT_TYPE), + Some("application/json") + ); + + // Check Set-Cookie header + 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")); + + // Check response body + let body = response.into_body_str(); + let returned_consent: GdprConsent = serde_json::from_str(&body).unwrap(); + assert_eq!(returned_consent.analytics, true); + assert_eq!(returned_consent.advertising, true); + assert_eq!(returned_consent.functional, false); + } + + #[test] + fn test_handle_consent_request_invalid_method() { + let settings = create_test_settings(); + let req = Request::put("https://example.com/gdpr/consent"); + + let response = handle_consent_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!(response.into_body_str(), "Method not allowed"); + } + + #[test] + fn test_handle_data_subject_request_get_with_id() { + let settings = create_test_settings(); + let mut req = Request::get("https://example.com/gdpr/data"); + req.set_header("X-Subject-ID", "test-subject-123"); + + let response = handle_data_subject_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::OK); + assert_eq!( + response.get_header_str(header::CONTENT_TYPE), + Some("application/json") + ); + + let body = response.into_body_str(); + let data: HashMap = serde_json::from_str(&body).unwrap(); + assert!(data.contains_key("test-subject-123")); + assert_eq!(data["test-subject-123"].visit_count, 0); // Default value + } + + #[test] + fn test_handle_data_subject_request_get_without_id() { + let settings = create_test_settings(); + let req = Request::get("https://example.com/gdpr/data"); + + let response = handle_data_subject_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::BAD_REQUEST); + assert_eq!(response.into_body_str(), "Missing subject ID"); + } + + #[test] + fn test_handle_data_subject_request_delete_with_id() { + let settings = create_test_settings(); + let mut req = Request::delete("https://example.com/gdpr/data"); + req.set_header("X-Subject-ID", "test-subject-123"); + + let response = handle_data_subject_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::OK); + assert_eq!(response.into_body_str(), "Data deletion request processed"); + } + + #[test] + fn test_handle_data_subject_request_delete_without_id() { + let settings = create_test_settings(); + let req = Request::delete("https://example.com/gdpr/data"); + + let response = handle_data_subject_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::BAD_REQUEST); + assert_eq!(response.into_body_str(), "Missing subject ID"); + } + + #[test] + fn test_handle_data_subject_request_invalid_method() { + let settings = create_test_settings(); + let req = Request::post("https://example.com/gdpr/data"); + + let response = handle_data_subject_request(&settings, req).unwrap(); + assert_eq!(response.get_status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!(response.into_body_str(), "Method not allowed"); + } + + #[test] + fn test_user_data_serialization() { + let user_data = UserData { + visit_count: 5, + last_visit: 1234567890, + ad_interactions: vec!["click1".to_string(), "view2".to_string()], + consent_history: vec![GdprConsent::default()], + }; + + let json = serde_json::to_string(&user_data).unwrap(); + assert!(json.contains("\"visit_count\":5")); + assert!(json.contains("\"last_visit\":1234567890")); + assert!(json.contains("\"ad_interactions\":[\"click1\",\"view2\"]")); + + let deserialized: UserData = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.visit_count, user_data.visit_count); + assert_eq!(deserialized.last_visit, user_data.last_visit); + assert_eq!(deserialized.ad_interactions.len(), 2); + } +} diff --git a/crates/common/src/models.rs b/crates/common/src/models.rs index f2a1fee..9b14a07 100644 --- a/crates/common/src/models.rs +++ b/crates/common/src/models.rs @@ -23,3 +23,245 @@ pub struct Callback { pub callback_type: String, pub url: String, } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_callback_deserialization() { + let json_data = json!({ + "type": "impression", + "url": "https://example.com/track/impression" + }); + + let callback: Callback = serde_json::from_value(json_data).unwrap(); + assert_eq!(callback.callback_type, "impression"); + assert_eq!(callback.url, "https://example.com/track/impression"); + } + + #[test] + fn test_callback_type_field_rename() { + // Test that "type" is correctly renamed to callback_type + let json_str = r#"{ + "type": "click", + "url": "https://example.com/track/click" + }"#; + + let callback: Callback = serde_json::from_str(json_str).unwrap(); + assert_eq!(callback.callback_type, "click"); + assert_eq!(callback.url, "https://example.com/track/click"); + } + + #[test] + fn test_ad_response_full_deserialization() { + let json_data = json!({ + "networkId": "12345", + "siteId": "67890", + "pageId": "11111", + "formatId": "22222", + "advertiserId": "33333", + "campaignId": "44444", + "insertionId": "55555", + "creativeId": "66666", + "creativeUrl": "https://cdn.example.com/creative/12345.jpg", + "callbacks": [ + { + "type": "impression", + "url": "https://track.example.com/impression/12345" + }, + { + "type": "click", + "url": "https://track.example.com/click/12345" + }, + { + "type": "viewability", + "url": "https://track.example.com/viewability/12345" + } + ] + }); + + let ad_response: AdResponse = serde_json::from_value(json_data).unwrap(); + + assert_eq!(ad_response.network_id, "12345"); + assert_eq!(ad_response.site_id, "67890"); + assert_eq!(ad_response.page_id, "11111"); + assert_eq!(ad_response.format_id, "22222"); + assert_eq!(ad_response.advertiser_id, "33333"); + assert_eq!(ad_response.campaign_id, "44444"); + assert_eq!(ad_response.insertion_id, "55555"); + assert_eq!(ad_response.creative_id, "66666"); + assert_eq!( + ad_response.creative_url, + "https://cdn.example.com/creative/12345.jpg" + ); + + assert_eq!(ad_response.callbacks.len(), 3); + assert_eq!(ad_response.callbacks[0].callback_type, "impression"); + assert_eq!( + ad_response.callbacks[0].url, + "https://track.example.com/impression/12345" + ); + assert_eq!(ad_response.callbacks[1].callback_type, "click"); + assert_eq!(ad_response.callbacks[2].callback_type, "viewability"); + } + + #[test] + fn test_ad_response_empty_callbacks() { + let json_data = json!({ + "networkId": "12345", + "siteId": "67890", + "pageId": "11111", + "formatId": "22222", + "advertiserId": "33333", + "campaignId": "44444", + "insertionId": "55555", + "creativeId": "66666", + "creativeUrl": "https://cdn.example.com/creative/12345.jpg", + "callbacks": [] + }); + + let ad_response: AdResponse = serde_json::from_value(json_data).unwrap(); + assert_eq!(ad_response.callbacks.len(), 0); + } + + #[test] + fn test_ad_response_missing_field() { + // Missing required field should fail + let json_data = json!({ + "networkId": "12345", + "siteId": "67890", + // Missing pageId + "formatId": "22222", + "advertiserId": "33333", + "campaignId": "44444", + "insertionId": "55555", + "creativeId": "66666", + "creativeUrl": "https://cdn.example.com/creative/12345.jpg", + "callbacks": [] + }); + + let result: Result = serde_json::from_value(json_data); + assert!(result.is_err()); + } + + #[test] + fn test_ad_response_case_sensitivity() { + // Test camelCase to snake_case conversion + let json_str = r#"{ + "networkId": "net123", + "siteId": "site456", + "pageId": "page789", + "formatId": "format000", + "advertiserId": "adv111", + "campaignId": "camp222", + "insertionId": "ins333", + "creativeId": "cre444", + "creativeUrl": "https://example.com/creative.png", + "callbacks": [] + }"#; + + let ad_response: AdResponse = serde_json::from_str(json_str).unwrap(); + assert_eq!(ad_response.network_id, "net123"); + assert_eq!(ad_response.site_id, "site456"); + assert_eq!(ad_response.page_id, "page789"); + assert_eq!(ad_response.format_id, "format000"); + } + + #[test] + fn test_callback_missing_field() { + let json_data = json!({ + "type": "impression" + // Missing url field + }); + + let result: Result = serde_json::from_value(json_data); + assert!(result.is_err()); + } + + #[test] + fn test_callback_extra_fields() { + // Extra fields should be ignored + let json_data = json!({ + "type": "conversion", + "url": "https://example.com/track/conversion", + "extra": "ignored", + "another": 123 + }); + + let callback: Callback = serde_json::from_value(json_data).unwrap(); + assert_eq!(callback.callback_type, "conversion"); + assert_eq!(callback.url, "https://example.com/track/conversion"); + } + + #[test] + fn test_ad_response_debug_format() { + let callback = Callback { + callback_type: "test".to_string(), + url: "https://test.com".to_string(), + }; + + let ad_response = AdResponse { + network_id: "123".to_string(), + site_id: "456".to_string(), + page_id: "789".to_string(), + format_id: "000".to_string(), + advertiser_id: "111".to_string(), + campaign_id: "222".to_string(), + insertion_id: "333".to_string(), + creative_id: "444".to_string(), + creative_url: "https://example.com/ad.jpg".to_string(), + callbacks: vec![callback], + }; + + let debug_str = format!("{:?}", ad_response); + assert!(debug_str.contains("AdResponse")); + assert!(debug_str.contains("network_id")); + assert!(debug_str.contains("123")); + } + + #[test] + fn test_callback_debug_format() { + let callback = Callback { + callback_type: "debug_test".to_string(), + url: "https://debug.test.com".to_string(), + }; + + let debug_str = format!("{:?}", callback); + assert!(debug_str.contains("Callback")); + assert!(debug_str.contains("callback_type")); + assert!(debug_str.contains("debug_test")); + assert!(debug_str.contains("url")); + assert!(debug_str.contains("https://debug.test.com")); + } + + #[test] + fn test_various_callback_types() { + let callback_types = vec![ + "impression", + "click", + "viewability", + "conversion", + "engagement", + "complete", + "firstQuartile", + "midpoint", + "thirdQuartile", + ]; + + for cb_type in callback_types { + let json_data = json!({ + "type": cb_type, + "url": format!("https://example.com/track/{}", cb_type) + }); + + let callback: Callback = serde_json::from_value(json_data).unwrap(); + assert_eq!(callback.callback_type, cb_type); + assert_eq!( + callback.url, + format!("https://example.com/track/{}", cb_type) + ); + } + } +} diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs index 1d707ea..7db2d5f 100644 --- a/crates/common/src/prebid.rs +++ b/crates/common/src/prebid.rs @@ -183,3 +183,165 @@ impl PrebidRequest { Ok(resp) } } + +#[cfg(test)] +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(), + }, + } + } + + #[test] + fn test_prebid_request_new_with_full_headers() { + let settings = create_test_settings(); + let mut req = Request::get("https://example.com/test"); + req.set_header(SYNTHETIC_HEADER_TRUSTED_SERVER, "existing-synthetic-id"); + req.set_header(header::REFERER, "https://test-domain.com/page"); + req.set_header(header::ORIGIN, "https://test-domain.com"); + req.set_header("X-Forwarded-For", "192.168.1.1, 10.0.0.1"); + + let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); + + assert_eq!(prebid_req.synthetic_id, "existing-synthetic-id"); + assert_eq!(prebid_req.domain, "test-domain.com"); + assert_eq!(prebid_req.banner_sizes, vec![(728, 90)]); + assert_eq!(prebid_req.origin, "https://test-domain.com"); + // Note: client_ip extraction from X-Forwarded-For depends on Fastly runtime + } + + #[test] + fn test_prebid_request_new_without_synthetic_id() { + let settings = create_test_settings(); + let mut req = Request::get("https://example.com/test"); + req.set_header("User-Agent", "Mozilla/5.0"); + req.set_header(header::REFERER, "https://test-domain.com/page"); + + let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); + + // Should generate a new synthetic ID + assert!(!prebid_req.synthetic_id.is_empty()); + assert_eq!(prebid_req.domain, "test-domain.com"); + } + + #[test] + fn test_prebid_request_domain_from_origin() { + let settings = create_test_settings(); + let mut req = Request::get("https://example.com/test"); + req.set_header(header::ORIGIN, "https://origin-domain.com"); + // No referer header + + let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); + + assert_eq!(prebid_req.domain, "origin-domain.com"); + assert_eq!(prebid_req.origin, "https://origin-domain.com"); + } + + #[test] + fn test_prebid_request_domain_fallback() { + let settings = create_test_settings(); + let req = Request::get("https://example.com/test"); + // 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"); + } + + #[test] + fn test_prebid_request_invalid_url_in_referer() { + let settings = create_test_settings(); + let mut req = Request::get("https://example.com/test"); + 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"); + } + + #[test] + fn test_prebid_request_x_forwarded_for_parsing() { + let settings = create_test_settings(); + let mut req = Request::get("https://example.com/test"); + req.set_header("X-Forwarded-For", "192.168.1.1, 10.0.0.1, 172.16.0.1"); + + let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); + + // Should get the first IP from the list (if get_client_ip_addr returns None) + // The actual behavior depends on Fastly runtime + assert!(!prebid_req.client_ip.is_empty()); + } + + #[test] + fn test_prebid_request_struct_fields() { + let prebid_req = PrebidRequest { + synthetic_id: "test-id".to_string(), + domain: "test.com".to_string(), + banner_sizes: vec![(300, 250), (728, 90)], + client_ip: "192.168.1.1".to_string(), + origin: "https://test.com".to_string(), + }; + + assert_eq!(prebid_req.synthetic_id, "test-id"); + assert_eq!(prebid_req.domain, "test.com"); + assert_eq!(prebid_req.banner_sizes.len(), 2); + assert_eq!(prebid_req.banner_sizes[0], (300, 250)); + assert_eq!(prebid_req.banner_sizes[1], (728, 90)); + assert_eq!(prebid_req.client_ip, "192.168.1.1"); + assert_eq!(prebid_req.origin, "https://test.com"); + } + + #[test] + fn test_prebid_request_with_multiple_sizes() { + let mut prebid_req = PrebidRequest { + synthetic_id: "test-id".to_string(), + domain: "test.com".to_string(), + banner_sizes: vec![(300, 250), (728, 90), (160, 600)], + client_ip: "192.168.1.1".to_string(), + origin: "https://test.com".to_string(), + }; + + // Test modifying banner sizes + prebid_req.banner_sizes.push((970, 250)); + assert_eq!(prebid_req.banner_sizes.len(), 4); + assert_eq!(prebid_req.banner_sizes[3], (970, 250)); + } + + #[test] + fn test_prebid_request_edge_cases() { + let settings = create_test_settings(); + + // Test with empty X-Forwarded-For + let mut req = Request::get("https://example.com/test"); + req.set_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"); + req2.set_header(header::ORIGIN, "://invalid"); + let prebid_req2 = PrebidRequest::new(&settings, &req2).unwrap(); + assert_eq!(prebid_req2.domain, "auburndao.com"); + } + + // Note: Testing send_bid_request would require mocking the Fastly backend, + // which isn't available in unit tests. This would be covered in integration tests. + // The method constructs a proper OpenRTB request with all required fields. +} diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index a3b70c3..67a241a 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -45,3 +45,178 @@ impl Settings { s.try_deserialize() } } + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_settings(toml_str: &str) -> Result { + let s = Config::builder() + .add_source(File::from_str(toml_str, FileFormat::Toml)) + .build()?; + + s.try_deserialize() + } + + #[test] + fn test_settings_new() { + // Test that Settings::new() loads successfully + let settings = Settings::new(); + assert!(settings.is_ok(), "Settings should load from embedded TOML"); + + let settings = settings.unwrap(); + // Verify basic structure is loaded + assert!(!settings.ad_server.ad_partner_url.is_empty()); + assert!(!settings.ad_server.sync_url.is_empty()); + assert!(!settings.prebid.server_url.is_empty()); + assert!(!settings.synthetic.counter_store.is_empty()); + assert!(!settings.synthetic.opid_store.is_empty()); + assert!(!settings.synthetic.secret_key.is_empty()); + assert!(!settings.synthetic.template.is_empty()); + } + + #[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" + + [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 = create_test_settings(toml_str); + assert!(settings.is_ok()); + + let settings = settings.unwrap(); + assert_eq!( + settings.ad_server.ad_partner_url, + "https://example-ad.com/serve" + ); + assert_eq!(settings.ad_server.sync_url, "https://example-ad.com/sync"); + assert_eq!( + settings.prebid.server_url, + "https://prebid.example.com/openrtb2/auction" + ); + 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!(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 settings = create_test_settings(toml_str); + assert!( + settings.is_err(), + "Should fail when required fields are missing" + ); + } + + #[test] + fn test_settings_empty_toml() { + let toml_str = ""; + let settings = create_test_settings(toml_str); + assert!(settings.is_err(), "Should fail with empty TOML"); + } + + #[test] + fn test_settings_invalid_toml_syntax() { + let toml_str = r#" +[ad_server +ad_partner_url = "https://example-ad.com/serve" +"#; + let settings = create_test_settings(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 settings = create_test_settings(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 settings = create_test_settings(toml_str); + assert!(settings.is_ok(), "Extra fields should be ignored"); + } + + #[test] + fn test_ad_server_debug_format() { + let ad_server = AdServer { + ad_partner_url: "https://test.com".to_string(), + sync_url: "https://sync.test.com".to_string(), + }; + let debug_str = format!("{:?}", ad_server); + assert!(debug_str.contains("AdServer")); + assert!(debug_str.contains("https://test.com")); + } + + #[test] + fn test_prebid_debug_format() { + let prebid = Prebid { + server_url: "https://prebid.test.com".to_string(), + }; + let debug_str = format!("{:?}", prebid); + assert!(debug_str.contains("Prebid")); + assert!(debug_str.contains("https://prebid.test.com")); + } + + #[test] + fn test_synthetic_debug_format() { + let synthetic = Synthetic { + counter_store: "counter".to_string(), + opid_store: "opid".to_string(), + secret_key: "secret".to_string(), + template: "{{test}}".to_string(), + }; + let debug_str = format!("{:?}", synthetic); + assert!(debug_str.contains("Synthetic")); + assert!(debug_str.contains("counter")); + assert!(debug_str.contains("secret")); + } +} From 1accf493422e17326f466d7993c5bec383257199 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:10:56 -0700 Subject: [PATCH 2/5] Fixed clippy errors --- crates/common/src/gdpr.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/common/src/gdpr.rs b/crates/common/src/gdpr.rs index b2d710b..adf36bc 100644 --- a/crates/common/src/gdpr.rs +++ b/crates/common/src/gdpr.rs @@ -237,9 +237,9 @@ mod tests { let consent = get_consent_from_request(&req); assert!(consent.is_some()); let consent = consent.unwrap(); - assert_eq!(consent.analytics, true); - assert_eq!(consent.advertising, false); - assert_eq!(consent.functional, true); + assert!(consent.analytics); + assert!(!consent.advertising); + assert!(consent.functional); } #[test] @@ -300,9 +300,9 @@ mod tests { // Check response body let body = response.into_body_str(); let returned_consent: GdprConsent = serde_json::from_str(&body).unwrap(); - assert_eq!(returned_consent.analytics, true); - assert_eq!(returned_consent.advertising, true); - assert_eq!(returned_consent.functional, false); + assert!(returned_consent.analytics); + assert!(returned_consent.advertising); + assert!(!returned_consent.functional); } #[test] From 754768c21190aee899789cf30d9267c07b37dc18 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:58:21 -0700 Subject: [PATCH 3/5] Changed to use header constants --- Cargo.lock | 5 ++- crates/common/Cargo.toml | 1 + crates/common/src/constants.rs | 24 ++++++++++-- crates/common/src/gdpr.rs | 14 ++++--- crates/common/src/prebid.rs | 22 +++++------ crates/common/src/synthetic.rs | 25 ++++++------ crates/fastly/src/main.rs | 71 ++++++++++++++++++---------------- 7 files changed, 95 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab6bf08..ca57bff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -715,9 +715,9 @@ dependencies = [ [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -1533,6 +1533,7 @@ dependencies = [ "handlebars", "hex", "hmac", + "http", "log", "log-fastly", "serde", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index aa5c7fe..782f585 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -17,6 +17,7 @@ futures = "0.3" handlebars = "6.3.2" hex = "0.4.3" hmac = "0.12.1" +http = "1.3.1" log = "0.4.20" log-fastly = "0.10.0" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index de5edba..90e7555 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -1,3 +1,21 @@ -pub const SYNTHETIC_HEADER_FRESH: &str = "X-Synthetic-Fresh"; -pub const SYNTHETIC_HEADER_TRUSTED_SERVER: &str = "X-Synthetic-Trusted-Server"; -pub const SYNTHETIC_HEADER_PUB_USER_ID: &str = "X-Pub-User-ID"; +use http::header::HeaderName; + +pub const HEADER_SYNTHETIC_FRESH: HeaderName = HeaderName::from_static("x-synthetic-fresh"); +pub const HEADER_SYNTHETIC_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id"); +pub const HEADER_X_PUB_USER_ID: HeaderName = HeaderName::from_static("x-pub-user-id"); +pub const HEADER_SYNTHETIC_TRUSTED_SERVER: HeaderName = + HeaderName::from_static("x-synthetic-trusted-server"); +pub const HEADER_X_CONSENT_ADVERTISING: HeaderName = + HeaderName::from_static("x-consent-advertising"); +pub const HEADER_X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for"); +pub const HEADER_X_GEO_CITY: HeaderName = HeaderName::from_static("x-geo-city"); +pub const HEADER_X_GEO_CONTINENT: HeaderName = HeaderName::from_static("x-geo-continent"); +pub const HEADER_X_GEO_COORDINATES: HeaderName = HeaderName::from_static("x-geo-coordinates"); +pub const HEADER_X_GEO_COUNTRY: HeaderName = HeaderName::from_static("x-geo-country"); +pub const HEADER_X_GEO_INFO_AVAILABLE: HeaderName = HeaderName::from_static("x-geo-info-available"); +pub const HEADER_X_GEO_METRO_CODE: HeaderName = HeaderName::from_static("x-geo-metro-code"); +pub const HEADER_X_GEO_REGION: HeaderName = HeaderName::from_static("x-geo-region"); +pub const HEADER_X_SUBJECT_ID: HeaderName = HeaderName::from_static("x-subject-id"); +pub const HEADER_X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id"); +pub const HEADER_X_COMPRESS_HINT: HeaderName = HeaderName::from_static("x-compress-hint"); +pub const HEADER_X_DEBUG_FASTLY_POP: HeaderName = HeaderName::from_static("x-debug-fastly-pop"); \ No newline at end of file diff --git a/crates/common/src/gdpr.rs b/crates/common/src/gdpr.rs index adf36bc..86e53fc 100644 --- a/crates/common/src/gdpr.rs +++ b/crates/common/src/gdpr.rs @@ -1,10 +1,12 @@ -use crate::cookies; -use crate::settings::Settings; use fastly::http::{header, Method, StatusCode}; use fastly::{Error, Request, Response}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::constants::HEADER_X_SUBJECT_ID; +use crate::cookies; +use crate::settings::Settings; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GdprConsent { pub analytics: bool, @@ -93,7 +95,7 @@ pub fn handle_data_subject_request(_settings: &Settings, req: Request) -> Result match *req.get_method() { Method::GET => { // Handle data access request - if let Some(synthetic_id) = req.get_header("X-Subject-ID") { + if let Some(synthetic_id) = req.get_header(HEADER_X_SUBJECT_ID) { // Create a HashMap to store all user-related data let mut data: HashMap = HashMap::new(); @@ -110,7 +112,7 @@ pub fn handle_data_subject_request(_settings: &Settings, req: Request) -> Result } Method::DELETE => { // Handle right to erasure (right to be forgotten) - if let Some(_synthetic_id) = req.get_header("X-Subject-ID") { + if let Some(_synthetic_id) = req.get_header(HEADER_X_SUBJECT_ID) { // TODO: Implement data deletion from KV store Ok(Response::from_status(StatusCode::OK) .with_body("Data deletion request processed")) @@ -319,7 +321,7 @@ mod tests { fn test_handle_data_subject_request_get_with_id() { let settings = create_test_settings(); let mut req = Request::get("https://example.com/gdpr/data"); - req.set_header("X-Subject-ID", "test-subject-123"); + req.set_header(HEADER_X_SUBJECT_ID, "test-subject-123"); let response = handle_data_subject_request(&settings, req).unwrap(); assert_eq!(response.get_status(), StatusCode::OK); @@ -348,7 +350,7 @@ mod tests { fn test_handle_data_subject_request_delete_with_id() { let settings = create_test_settings(); let mut req = Request::delete("https://example.com/gdpr/data"); - req.set_header("X-Subject-ID", "test-subject-123"); + req.set_header(HEADER_X_SUBJECT_ID, "test-subject-123"); let response = handle_data_subject_request(&settings, req).unwrap(); assert_eq!(response.get_status(), StatusCode::OK); diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs index 7db2d5f..46d8846 100644 --- a/crates/common/src/prebid.rs +++ b/crates/common/src/prebid.rs @@ -2,7 +2,7 @@ use fastly::http::{header, Method}; use fastly::{Error, Request, Response}; use serde_json::json; -use crate::constants::{SYNTHETIC_HEADER_FRESH, SYNTHETIC_HEADER_TRUSTED_SERVER}; +use crate::constants::{HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_FORWARDED_FOR}; use crate::settings::Settings; use crate::synthetic::generate_synthetic_id; @@ -31,7 +31,7 @@ impl PrebidRequest { pub fn new(settings: &Settings, req: &Request) -> Result { // Get the Trusted Server ID from header (which we just set in handle_prebid_test) let synthetic_id = req - .get_header(SYNTHETIC_HEADER_TRUSTED_SERVER) + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()) .unwrap_or_else(|| generate_synthetic_id(settings, req)); @@ -41,7 +41,7 @@ impl PrebidRequest { .get_client_ip_addr() .map(|ip| ip.to_string()) .unwrap_or_else(|| { - req.get_header("X-Forwarded-For") + req.get_header(HEADER_X_FORWARDED_FOR) .and_then(|h| h.to_str().ok()) .unwrap_or("") .split(',') // X-Forwarded-For can be a comma-separated list @@ -98,7 +98,7 @@ impl PrebidRequest { // Get and store the POTSI ID value from the incoming request let id: String = incoming_req - .get_header(SYNTHETIC_HEADER_TRUSTED_SERVER) + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()) .unwrap_or_else(|| self.synthetic_id.clone()); @@ -167,10 +167,10 @@ impl PrebidRequest { }); req.set_header(header::CONTENT_TYPE, "application/json"); - req.set_header("X-Forwarded-For", &self.client_ip); + req.set_header(HEADER_X_FORWARDED_FOR, &self.client_ip); req.set_header(header::ORIGIN, &self.origin); - req.set_header(SYNTHETIC_HEADER_FRESH, &self.synthetic_id); - req.set_header(SYNTHETIC_HEADER_TRUSTED_SERVER, &id); + req.set_header(HEADER_SYNTHETIC_FRESH, &self.synthetic_id); + req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, &id); println!( "Sending prebid request with Fresh ID: {} and Trusted Server ID: {}", @@ -211,10 +211,10 @@ mod tests { fn test_prebid_request_new_with_full_headers() { let settings = create_test_settings(); let mut req = Request::get("https://example.com/test"); - req.set_header(SYNTHETIC_HEADER_TRUSTED_SERVER, "existing-synthetic-id"); + req.set_header(HEADER_SYNTHETIC_TRUSTED_SERVER, "existing-synthetic-id"); req.set_header(header::REFERER, "https://test-domain.com/page"); req.set_header(header::ORIGIN, "https://test-domain.com"); - req.set_header("X-Forwarded-For", "192.168.1.1, 10.0.0.1"); + req.set_header(HEADER_X_FORWARDED_FOR, "192.168.1.1, 10.0.0.1"); let prebid_req = PrebidRequest::new(&settings, &req).unwrap(); @@ -280,7 +280,7 @@ mod tests { fn test_prebid_request_x_forwarded_for_parsing() { let settings = create_test_settings(); let mut req = Request::get("https://example.com/test"); - req.set_header("X-Forwarded-For", "192.168.1.1, 10.0.0.1, 172.16.0.1"); + 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,7 +330,7 @@ mod tests { // Test with empty X-Forwarded-For let mut req = Request::get("https://example.com/test"); - req.set_header("X-Forwarded-For", ""); + 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()); diff --git a/crates/common/src/synthetic.rs b/crates/common/src/synthetic.rs index a22610c..6c54572 100644 --- a/crates/common/src/synthetic.rs +++ b/crates/common/src/synthetic.rs @@ -5,7 +5,7 @@ use hmac::{Hmac, Mac}; use serde_json::json; use sha2::Sha256; -use crate::constants::{SYNTHETIC_HEADER_PUB_USER_ID, SYNTHETIC_HEADER_TRUSTED_SERVER}; +use crate::constants::{HEADER_SYNTHETIC_PUB_USER_ID, HEADER_SYNTHETIC_TRUSTED_SERVER}; use crate::cookies::handle_request_cookies; use crate::settings::Settings; @@ -21,7 +21,7 @@ pub fn generate_synthetic_id(settings: &Settings, req: &Request) -> String { .map(|cookie| cookie.value().to_string()) }); let auth_user_id = req - .get_header(SYNTHETIC_HEADER_PUB_USER_ID) + .get_header(HEADER_SYNTHETIC_PUB_USER_ID) .map(|h| h.to_str().unwrap_or("anonymous")); let publisher_domain = req .get_header(header::HOST) @@ -61,7 +61,7 @@ pub fn generate_synthetic_id(settings: &Settings, req: &Request) -> String { pub fn get_or_generate_synthetic_id(settings: &Settings, req: &Request) -> String { // First try to get existing Trusted Server ID from header if let Some(synthetic_id) = req - .get_header(SYNTHETIC_HEADER_TRUSTED_SERVER) + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()) { @@ -96,9 +96,10 @@ pub fn get_or_generate_synthetic_id(settings: &Settings, req: &Request) -> Strin #[cfg(test)] mod tests { use super::*; - use fastly::http::HeaderValue; + use crate::constants::HEADER_X_PUB_USER_ID; + use fastly::http::{HeaderName, HeaderValue}; - fn create_test_request(headers: Vec<(&str, &str)>) -> Request { + fn create_test_request(headers: Vec<(HeaderName, &str)>) -> Request { let mut req = Request::new("GET", "http://example.com"); for (key, value) in headers { req.set_header(key, HeaderValue::from_str(value).unwrap()); @@ -129,11 +130,11 @@ mod tests { fn test_generate_synthetic_id() { let settings: Settings = create_settings(); let req = create_test_request(vec![ - (header::USER_AGENT.as_ref(), "Mozilla/5.0"), - (header::COOKIE.as_ref(), "pub_userid=12345"), - ("X-Pub-User-ID", "67890"), - (header::HOST.as_ref(), "example.com"), - (header::ACCEPT_LANGUAGE.as_ref(), "en-US,en;q=0.9"), + (header::USER_AGENT, "Mozilla/5.0"), + (header::COOKIE, "pub_userid=12345"), + (HEADER_X_PUB_USER_ID, "67890"), + (header::HOST, "example.com"), + (header::ACCEPT_LANGUAGE, "en-US,en;q=0.9"), ]); let synthetic_id = generate_synthetic_id(&settings, &req); @@ -148,7 +149,7 @@ mod tests { fn test_get_or_generate_synthetic_id_with_header() { let settings = create_settings(); let req = create_test_request(vec![( - SYNTHETIC_HEADER_TRUSTED_SERVER, + HEADER_SYNTHETIC_TRUSTED_SERVER, "existing_synthetic_id", )]); @@ -160,7 +161,7 @@ mod tests { fn test_get_or_generate_synthetic_id_with_cookie() { let settings = create_settings(); let req = create_test_request(vec![( - header::COOKIE.as_ref(), + header::COOKIE, "synthetic_id=existing_cookie_id", )]); diff --git a/crates/fastly/src/main.rs b/crates/fastly/src/main.rs index bd7dc84..cec06e8 100644 --- a/crates/fastly/src/main.rs +++ b/crates/fastly/src/main.rs @@ -6,7 +6,12 @@ use log::LevelFilter::Info; use serde_json::json; use std::env; -use trusted_server_common::constants::{SYNTHETIC_HEADER_FRESH, SYNTHETIC_HEADER_TRUSTED_SERVER}; +use trusted_server_common::constants::{ + HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_COMPRESS_HINT, + HEADER_X_CONSENT_ADVERTISING, HEADER_X_FORWARDED_FOR, HEADER_X_GEO_CITY, + HEADER_X_GEO_CONTINENT, HEADER_X_GEO_COORDINATES, HEADER_X_GEO_COUNTRY, + HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, +}; use trusted_server_common::cookies::create_synthetic_cookie; use trusted_server_common::gdpr::{ get_consent_from_request, handle_consent_request, handle_data_subject_request, @@ -41,15 +46,15 @@ fn main(req: Request) -> Result { (&Method::GET, "/privacy-policy") => Ok(Response::from_status(StatusCode::OK) .with_body(PRIVACY_TEMPLATE) .with_header(header::CONTENT_TYPE, "text/html") - .with_header("x-compress-hint", "on")), + .with_header(HEADER_X_COMPRESS_HINT, "on")), (&Method::GET, "/why-trusted-server") => Ok(Response::from_status(StatusCode::OK) .with_body(WHY_TEMPLATE) .with_header(header::CONTENT_TYPE, "text/html") - .with_header("x-compress-hint", "on")), + .with_header(HEADER_X_COMPRESS_HINT, "on")), _ => Ok(Response::from_status(StatusCode::NOT_FOUND) .with_body("Not Found") .with_header(header::CONTENT_TYPE, "text/plain") - .with_header("x-compress-hint", "on")), + .with_header(HEADER_X_COMPRESS_HINT, "on")), } }) } @@ -72,30 +77,30 @@ fn get_dma_code(req: &mut Request) -> Option { // Set all available geo information in headers let city = geo.city(); - req.set_header("X-Geo-City", city); + req.set_header(HEADER_X_GEO_CITY, city); println!(" City: {}", city); let country = geo.country_code(); - req.set_header("X-Geo-Country", country); + req.set_header(HEADER_X_GEO_COUNTRY, country); println!(" Country: {}", country); - req.set_header("X-Geo-Continent", format!("{:?}", geo.continent())); + req.set_header(HEADER_X_GEO_CONTINENT, format!("{:?}", geo.continent())); println!(" Continent: {:?}", geo.continent()); req.set_header( - "X-Geo-Coordinates", + HEADER_X_GEO_COORDINATES, format!("{},{}", geo.latitude(), geo.longitude()), ); println!(" Location: ({}, {})", geo.latitude(), geo.longitude()); // Get and set the metro code (DMA) let metro_code = geo.metro_code(); - req.set_header("X-Geo-Metro-Code", metro_code.to_string()); + req.set_header(HEADER_X_GEO_METRO_CODE, metro_code.to_string()); println!("Found DMA/Metro code: {}", metro_code); return Some(metro_code.to_string()); } else { println!("No geo information available for the request"); - req.set_header("X-Geo-Info-Available", "false"); + req.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false"); } // If no metro code is found, log all request headers for debugging @@ -142,7 +147,7 @@ fn handle_main_page(settings: &Settings, mut req: Request) -> Result Result Result Result Result Result Result Result Result Result Result { From d36b31edf986a6cfc2a65e12012d2433b6a30807 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:01:08 -0700 Subject: [PATCH 4/5] Fixed cargo fmt --- crates/common/src/constants.rs | 2 +- crates/common/src/prebid.rs | 4 +++- crates/common/src/synthetic.rs | 5 +---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index 90e7555..02ed380 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -18,4 +18,4 @@ pub const HEADER_X_GEO_REGION: HeaderName = HeaderName::from_static("x-geo-regio pub const HEADER_X_SUBJECT_ID: HeaderName = HeaderName::from_static("x-subject-id"); pub const HEADER_X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id"); pub const HEADER_X_COMPRESS_HINT: HeaderName = HeaderName::from_static("x-compress-hint"); -pub const HEADER_X_DEBUG_FASTLY_POP: HeaderName = HeaderName::from_static("x-debug-fastly-pop"); \ No newline at end of file +pub const HEADER_X_DEBUG_FASTLY_POP: HeaderName = HeaderName::from_static("x-debug-fastly-pop"); diff --git a/crates/common/src/prebid.rs b/crates/common/src/prebid.rs index 46d8846..8802b74 100644 --- a/crates/common/src/prebid.rs +++ b/crates/common/src/prebid.rs @@ -2,7 +2,9 @@ use fastly::http::{header, Method}; use fastly::{Error, Request, Response}; use serde_json::json; -use crate::constants::{HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_FORWARDED_FOR}; +use crate::constants::{ + HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_FORWARDED_FOR, +}; use crate::settings::Settings; use crate::synthetic::generate_synthetic_id; diff --git a/crates/common/src/synthetic.rs b/crates/common/src/synthetic.rs index 6c54572..ebabb11 100644 --- a/crates/common/src/synthetic.rs +++ b/crates/common/src/synthetic.rs @@ -160,10 +160,7 @@ mod tests { #[test] fn test_get_or_generate_synthetic_id_with_cookie() { let settings = create_settings(); - let req = create_test_request(vec![( - header::COOKIE, - "synthetic_id=existing_cookie_id", - )]); + let req = create_test_request(vec![(header::COOKIE, "synthetic_id=existing_cookie_id")]); let synthetic_id = get_or_generate_synthetic_id(&settings, &req); assert_eq!(synthetic_id, "existing_cookie_id"); From 8cc765026d4485507275c17c1d3663a28c4fb405 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:15:56 -0700 Subject: [PATCH 5/5] Added test for reading env variable --- Cargo.lock | 58 ++++++++++ crates/common/Cargo.toml | 3 + crates/common/src/settings.rs | 195 ++++++++++++++++++++-------------- 3 files changed, 176 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab6bf08..847d993 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -957,6 +957,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.25" @@ -1056,6 +1066,29 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "pathdiff" version = "0.2.3" @@ -1149,6 +1182,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "regex" version = "1.11.1" @@ -1219,6 +1261,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.217" @@ -1368,6 +1416,15 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "temp-env" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" +dependencies = [ + "parking_lot", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1538,6 +1595,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.8", + "temp-env", "tokio", "url", ] diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index aa5c7fe..42b84be 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -24,3 +24,6 @@ serde_json = "1.0.91" sha2 = "0.10.6" tokio = { version = "1.43", features = ["sync", "macros", "io-util", "rt", "time"] } url = "2.4.1" + +[dev-dependencies] +temp-env = "0.3.6" \ No newline at end of file diff --git a/crates/common/src/settings.rs b/crates/common/src/settings.rs index 67a241a..ae294a7 100644 --- a/crates/common/src/settings.rs +++ b/crates/common/src/settings.rs @@ -1,7 +1,8 @@ -use config::{Config, ConfigError, File, FileFormat}; -use serde::Deserialize; use std::str; +use config::{Config, ConfigError, Environment, File, FileFormat}; +use serde::Deserialize; + #[derive(Debug, Deserialize)] #[allow(unused)] pub struct AdServer { @@ -37,12 +38,22 @@ impl Settings { let toml_bytes = include_bytes!("../../../trusted-server.toml"); let toml_str = str::from_utf8(toml_bytes).unwrap(); - let s = Config::builder() - .add_source(File::from_str(toml_str, FileFormat::Toml)) + Self::from_toml(toml_str) + } + + pub fn from_toml(toml_str: &str) -> Result { + let environment = Environment::default() + .prefix("TRUSTED_SERVER") + .separator("__"); + + let toml = File::from_str(toml_str, FileFormat::Toml); + let config = Config::builder() + .add_source(toml) + .add_source(environment) .build()?; // You can deserialize (and thus freeze) the entire configuration as - s.try_deserialize() + config.try_deserialize() } } @@ -50,14 +61,6 @@ impl Settings { mod tests { use super::*; - fn create_test_settings(toml_str: &str) -> Result { - let s = Config::builder() - .add_source(File::from_str(toml_str, FileFormat::Toml)) - .build()?; - - s.try_deserialize() - } - #[test] fn test_settings_new() { // Test that Settings::new() loads successfully @@ -92,7 +95,7 @@ mod tests { template = "{{client_ip}}:{{user_agent}}:{{first_party_id}}:{{auth_user_id}}:{{publisher_domain}}:{{accept_language}}" "#; - let settings = create_test_settings(toml_str); + let settings = Settings::from_toml(toml_str); assert!(settings.is_ok()); let settings = settings.unwrap(); @@ -114,21 +117,21 @@ mod tests { #[test] fn test_settings_missing_required_fields() { let toml_str = r#" -[ad_server] -ad_partner_url = "https://example-ad.com/serve" -# Missing sync_url + [ad_server] + ad_partner_url = "https://example-ad.com/serve" + # Missing sync_url -[prebid] -server_url = "https://prebid.example.com/openrtb2/auction" + [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}}" -"#; + [synthetic] + counter_store = "test-counter-store" + opid_store = "test-opid-store" + secret_key = "test-secret-key" + template = "{{client_ip}}" + "#; - let settings = create_test_settings(toml_str); + let settings = Settings::from_toml(toml_str); assert!( settings.is_err(), "Should fail when required fields are missing" @@ -138,85 +141,117 @@ template = "{{client_ip}}" #[test] fn test_settings_empty_toml() { let toml_str = ""; - let settings = create_test_settings(toml_str); + let settings = Settings::from_toml(toml_str); + assert!(settings.is_err(), "Should fail with empty TOML"); } #[test] fn test_settings_invalid_toml_syntax() { let toml_str = r#" -[ad_server -ad_partner_url = "https://example-ad.com/serve" -"#; - let settings = create_test_settings(toml_str); + [ad_server + ad_partner_url = "https://example-ad.com/serve" + "#; + + 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 settings = create_test_settings(toml_str); + [ad_server] + ad_partner_url = "https://example-ad.com/serve" + sync_url = "https://example-ad.com/sync" + "#; + + 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 settings = create_test_settings(toml_str); + [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 settings = Settings::from_toml(toml_str); assert!(settings.is_ok(), "Extra fields should be ignored"); } #[test] - fn test_ad_server_debug_format() { - let ad_server = AdServer { - ad_partner_url: "https://test.com".to_string(), - sync_url: "https://sync.test.com".to_string(), - }; - let debug_str = format!("{:?}", ad_server); - assert!(debug_str.contains("AdServer")); - assert!(debug_str.contains("https://test.com")); - } + 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" - #[test] - fn test_prebid_debug_format() { - let prebid = Prebid { - server_url: "https://prebid.test.com".to_string(), - }; - let debug_str = format!("{:?}", prebid); - assert!(debug_str.contains("Prebid")); - assert!(debug_str.contains("https://prebid.test.com")); + [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}}" + "#; + + temp_env::with_var( + "TRUSTED_SERVER__AD_SERVER__AD_PARTNER_URL", + Some("https://change-ad.com/serve"), + || { + let settings = Settings::from_toml(toml_str); + + assert!(settings.is_ok(), "Settings should load from embedded TOML"); + assert_eq!( + settings.unwrap().ad_server.ad_partner_url, + "https://change-ad.com/serve" + ); + }, + ); } #[test] - fn test_synthetic_debug_format() { - let synthetic = Synthetic { - counter_store: "counter".to_string(), - opid_store: "opid".to_string(), - secret_key: "secret".to_string(), - template: "{{test}}".to_string(), - }; - let debug_str = format!("{:?}", synthetic); - assert!(debug_str.contains("Synthetic")); - assert!(debug_str.contains("counter")); - assert!(debug_str.contains("secret")); + 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}}" + "#; + + temp_env::with_var( + "TRUSTED_SERVER__AD_SERVER__AD_PARTNER_URL", + Some("https://change-ad.com/serve"), + || { + let settings = Settings::from_toml(toml_str); + + assert!(settings.is_ok(), "Settings should load from embedded TOML"); + assert_eq!( + settings.unwrap().ad_server.ad_partner_url, + "https://change-ad.com/serve" + ); + }, + ); } }