diff --git a/crates/trusted-server-core/src/cookies.rs b/crates/trusted-server-core/src/cookies.rs index 963bf125..cbda1be5 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 2720e88b..1f637fac 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 08a32cec..8aa58af0 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 4921a9bb..e93458df 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", + ); + } +}