From f65a391ca3a298fb4473ad5e3730a4607e78ef7e Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 20 Apr 2026 15:38:50 -0500 Subject: [PATCH 1/2] Add test coverage for error mapping and tsjs helpers --- crates/trusted-server-core/src/cookies.rs | 24 +++++ crates/trusted-server-core/src/error.rs | 108 ++++++++++++++++++++++ crates/trusted-server-core/src/models.rs | 17 +--- crates/trusted-server-core/src/tsjs.rs | 93 +++++++++++++++++++ 4 files changed, 227 insertions(+), 15 deletions(-) diff --git a/crates/trusted-server-core/src/cookies.rs b/crates/trusted-server-core/src/cookies.rs index 963bf1256..cbda1be52 100644 --- a/crates/trusted-server-core/src/cookies.rs +++ b/crates/trusted-server-core/src/cookies.rs @@ -268,6 +268,8 @@ pub fn expire_ec_cookie(settings: &Settings, response: &mut fastly::Response) { #[cfg(test)] mod tests { + use fastly::http::HeaderValue; + use crate::test_support::tests::create_test_settings; use super::*; @@ -347,6 +349,28 @@ mod tests { assert!(jar.iter().count() == 0); } + #[test] + fn test_handle_request_cookies_invalid_utf8_header_value() { + let mut req = Request::get("http://example.com"); + req.set_header( + header::COOKIE, + HeaderValue::from_bytes(b"session=\xFF") + .expect("should allow non-UTF-8 cookie header bytes"), + ); + + let error = handle_request_cookies(&req).expect_err("should reject invalid UTF-8"); + let trusted_server_error = error.current_context(); + + assert!( + matches!( + trusted_server_error, + TrustedServerError::InvalidHeaderValue { message } + if message == "Cookie header contains invalid UTF-8" + ), + "should return InvalidHeaderValue for invalid UTF-8 cookie headers", + ); + } + #[test] fn test_set_ec_cookie() { let settings = create_test_settings(); diff --git a/crates/trusted-server-core/src/error.rs b/crates/trusted-server-core/src/error.rs index 2720e88b5..1f637fac1 100644 --- a/crates/trusted-server-core/src/error.rs +++ b/crates/trusted-server-core/src/error.rs @@ -136,8 +136,116 @@ impl IntoHttpResponse for TrustedServerError { #[cfg(test)] mod tests { + use http::StatusCode; + use super::*; + #[test] + fn status_code_maps_each_error_variant_to_expected_http_response() { + let cases = [ + ( + TrustedServerError::BadRequest { + message: "missing field".into(), + }, + StatusCode::BAD_REQUEST, + ), + ( + TrustedServerError::Configuration { + message: "invalid config".into(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + TrustedServerError::Auction { + message: "bid timeout".into(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Gam { + message: "upstream error".into(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::GdprConsent { + message: "missing consent string".into(), + }, + StatusCode::BAD_REQUEST, + ), + ( + TrustedServerError::InvalidUtf8 { + message: "byte 0xff".into(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + TrustedServerError::InvalidHeaderValue { + message: "non-ascii".into(), + }, + StatusCode::BAD_REQUEST, + ), + ( + TrustedServerError::KvStore { + store_name: "users".into(), + message: "timeout".into(), + }, + StatusCode::SERVICE_UNAVAILABLE, + ), + ( + TrustedServerError::Prebid { + message: "adapter error".into(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Integration { + integration: "foo".into(), + message: "connection refused".into(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Proxy { + message: "upstream refused".into(), + }, + StatusCode::BAD_GATEWAY, + ), + ( + TrustedServerError::Forbidden { + message: "missing credentials".into(), + }, + StatusCode::FORBIDDEN, + ), + ( + TrustedServerError::AllowlistViolation { + host: "blocked.example.com".into(), + }, + StatusCode::FORBIDDEN, + ), + ( + TrustedServerError::Settings { + message: "parse failed".into(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ( + TrustedServerError::Ec { + message: "seed missing".into(), + }, + StatusCode::INTERNAL_SERVER_ERROR, + ), + ]; + + for (error, expected_status) in cases { + assert_eq!( + error.status_code(), + expected_status, + "should map {error:?} to the expected HTTP status", + ); + } + } + #[test] fn server_errors_return_generic_message() { let cases = [ diff --git a/crates/trusted-server-core/src/models.rs b/crates/trusted-server-core/src/models.rs index 08a32cecb..8aa58af0a 100644 --- a/crates/trusted-server-core/src/models.rs +++ b/crates/trusted-server-core/src/models.rs @@ -55,7 +55,7 @@ mod tests { use serde_json::json; #[test] - fn test_callback_deserialization() { + fn test_callback_deserialization_renames_type_field() { let json_data = json!({ "type": "impression", "url": "https://example.com/track/impression" @@ -63,24 +63,11 @@ mod tests { let callback: Callback = serde_json::from_value(json_data).expect("should deserialize callback"); + 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).expect("should deserialize callback from str"); - 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!({ diff --git a/crates/trusted-server-core/src/tsjs.rs b/crates/trusted-server-core/src/tsjs.rs index 4921a9bba..e93458dfb 100644 --- a/crates/trusted-server-core/src/tsjs.rs +++ b/crates/trusted-server-core/src/tsjs.rs @@ -64,3 +64,96 @@ pub fn tsjs_deferred_script_tags(module_ids: &[&str]) -> String { .map(|id| tsjs_deferred_script_tag(id)) .collect::() } + +#[cfg(test)] +mod tests { + use trusted_server_js::{all_module_ids, concatenated_hash, single_module_hash}; + + use super::*; + + #[test] + fn tsjs_script_src_formats_unified_bundle_url_with_hash() { + let module_ids = ["core", "creative"]; + let expected_hash = concatenated_hash(&module_ids); + + assert_eq!( + tsjs_script_src(&module_ids), + format!("/static/tsjs=tsjs-unified.min.js?v={expected_hash}"), + "should include the unified bundle path and cache-busting hash", + ); + } + + #[test] + fn tsjs_script_tag_wraps_source_in_a_single_tag() { + let module_ids = ["core", "creative"]; + let expected_src = tsjs_script_src(&module_ids); + + assert_eq!( + tsjs_script_tag(&module_ids), + format!(""), + "should render the injected trustedserver script tag", + ); + } + + #[test] + fn tsjs_unified_helpers_use_all_module_ids() { + let all_ids = all_module_ids(); + let expected_src = format!( + "/static/tsjs=tsjs-unified.min.js?v={}", + concatenated_hash(&all_ids) + ); + + assert_eq!( + tsjs_unified_script_src(), + expected_src, + "should hash all compiled modules for the unified bundle", + ); + assert_eq!( + tsjs_unified_script_tag(), + format!(""), + "should wrap the unified bundle source in the standard script tag", + ); + } + + #[test] + fn tsjs_deferred_helpers_format_single_module_urls_and_tags() { + let module_id = "prebid"; + let expected_hash = single_module_hash(module_id).expect("should hash known module"); + let expected_src = format!("/static/tsjs=tsjs-{module_id}.min.js?v={expected_hash}"); + + assert_eq!( + tsjs_deferred_script_src(module_id), + expected_src, + "should include the deferred module path and hash", + ); + assert_eq!( + tsjs_deferred_script_tag(module_id), + format!(""), + "should render a deferred script tag for the module", + ); + } + + #[test] + fn tsjs_deferred_script_src_uses_empty_hash_for_unknown_module() { + assert_eq!( + tsjs_deferred_script_src("unknown-module"), + "/static/tsjs=tsjs-unknown-module.min.js?v=", + "should fall back to an empty hash for unknown deferred modules", + ); + } + + #[test] + fn tsjs_deferred_script_tags_concatenates_tags_in_input_order() { + let module_ids = ["prebid", "creative"]; + let expected = module_ids + .iter() + .map(|id| tsjs_deferred_script_tag(id)) + .collect::(); + + assert_eq!( + tsjs_deferred_script_tags(&module_ids), + expected, + "should concatenate one deferred script tag per module in order", + ); + } +} From 02ac32f270d72c1817f90e228096f7640d51c6ff Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 5 May 2026 09:28:44 -0500 Subject: [PATCH 2/2] Address PR review feedback on test coverage --- crates/trusted-server-core/src/cookies.rs | 2 +- crates/trusted-server-core/src/error.rs | 20 +++++++++ crates/trusted-server-core/src/tsjs.rs | 53 ++++++++++++++--------- 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/crates/trusted-server-core/src/cookies.rs b/crates/trusted-server-core/src/cookies.rs index cbda1be52..0658c07f8 100644 --- a/crates/trusted-server-core/src/cookies.rs +++ b/crates/trusted-server-core/src/cookies.rs @@ -355,7 +355,7 @@ mod tests { req.set_header( header::COOKIE, HeaderValue::from_bytes(b"session=\xFF") - .expect("should allow non-UTF-8 cookie header bytes"), + .expect("should construct HeaderValue from non-UTF-8 bytes"), ); let error = handle_request_cookies(&req).expect_err("should reject invalid UTF-8"); diff --git a/crates/trusted-server-core/src/error.rs b/crates/trusted-server-core/src/error.rs index 1f637fac1..f51417f1f 100644 --- a/crates/trusted-server-core/src/error.rs +++ b/crates/trusted-server-core/src/error.rs @@ -142,6 +142,26 @@ mod tests { #[test] fn status_code_maps_each_error_variant_to_expected_http_response() { + // Compile-time guard: adding a TrustedServerError variant without + // updating this test will fail to compile. + let _exhaustive = |error: &TrustedServerError| match error { + TrustedServerError::BadRequest { .. } + | TrustedServerError::Configuration { .. } + | TrustedServerError::Auction { .. } + | TrustedServerError::Gam { .. } + | TrustedServerError::GdprConsent { .. } + | TrustedServerError::InvalidUtf8 { .. } + | TrustedServerError::InvalidHeaderValue { .. } + | TrustedServerError::KvStore { .. } + | TrustedServerError::Prebid { .. } + | TrustedServerError::Integration { .. } + | TrustedServerError::Proxy { .. } + | TrustedServerError::Forbidden { .. } + | TrustedServerError::AllowlistViolation { .. } + | TrustedServerError::Settings { .. } + | TrustedServerError::Ec { .. } => (), + }; + let cases = [ ( TrustedServerError::BadRequest { diff --git a/crates/trusted-server-core/src/tsjs.rs b/crates/trusted-server-core/src/tsjs.rs index e93458dfb..50ab7150c 100644 --- a/crates/trusted-server-core/src/tsjs.rs +++ b/crates/trusted-server-core/src/tsjs.rs @@ -67,25 +67,40 @@ pub fn tsjs_deferred_script_tags(module_ids: &[&str]) -> String { #[cfg(test)] mod tests { - use trusted_server_js::{all_module_ids, concatenated_hash, single_module_hash}; - use super::*; + fn assert_sha256_hex_hash(src: &str) { + let hash = src + .rsplit_once("?v=") + .expect("should have cache-busting query parameter") + .1; + + assert_eq!(hash.len(), 64, "should use a SHA-256 hex hash"); + assert!( + hash.chars().all(|character| character.is_ascii_hexdigit()), + "should use only ASCII hex digits", + ); + } + #[test] fn tsjs_script_src_formats_unified_bundle_url_with_hash() { - let module_ids = ["core", "creative"]; - let expected_hash = concatenated_hash(&module_ids); + let creative_src = tsjs_script_src(&["creative"]); + let creative_prebid_src = tsjs_script_src(&["creative", "prebid"]); - assert_eq!( - tsjs_script_src(&module_ids), - format!("/static/tsjs=tsjs-unified.min.js?v={expected_hash}"), - "should include the unified bundle path and cache-busting hash", + assert!( + creative_src.starts_with("/static/tsjs=tsjs-unified.min.js?v="), + "should include the unified bundle path and cache-busting parameter", + ); + assert_sha256_hex_hash(&creative_src); + assert_ne!( + creative_src, creative_prebid_src, + "should generate different cache-busting URLs for different module sets", ); } #[test] fn tsjs_script_tag_wraps_source_in_a_single_tag() { - let module_ids = ["core", "creative"]; + let module_ids = ["creative"]; let expected_src = tsjs_script_src(&module_ids); assert_eq!( @@ -97,20 +112,16 @@ mod tests { #[test] fn tsjs_unified_helpers_use_all_module_ids() { - let all_ids = all_module_ids(); - let expected_src = format!( - "/static/tsjs=tsjs-unified.min.js?v={}", - concatenated_hash(&all_ids) - ); + let src = tsjs_unified_script_src(); - assert_eq!( - tsjs_unified_script_src(), - expected_src, - "should hash all compiled modules for the unified bundle", + assert!( + src.starts_with("/static/tsjs=tsjs-unified.min.js?v="), + "should include the unified bundle path and cache-busting parameter", ); + assert_sha256_hex_hash(&src); assert_eq!( tsjs_unified_script_tag(), - format!(""), + format!(""), "should wrap the unified bundle source in the standard script tag", ); } @@ -134,11 +145,11 @@ mod tests { } #[test] - fn tsjs_deferred_script_src_uses_empty_hash_for_unknown_module() { + fn tsjs_deferred_script_src_documents_unregistered_module_fallback() { assert_eq!( tsjs_deferred_script_src("unknown-module"), "/static/tsjs=tsjs-unknown-module.min.js?v=", - "should fall back to an empty hash for unknown deferred modules", + "should preserve the current fallback until callers validate registered modules", ); }