From 48a297ec597341fe42a60aa7df99269b8926f79e Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 13 May 2026 13:35:49 -0500 Subject: [PATCH 1/4] Add auction endpoint request coverage --- .../src/route_tests.rs | 221 +++++++++++++++++- .../src/auction/endpoints.rs | 2 +- 2 files changed, 215 insertions(+), 8 deletions(-) diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index 9aa4c54a..29a93a67 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -3,10 +3,16 @@ use std::sync::Arc; use edgezero_core::key_value_store::NoopKvStore; use error_stack::Report; -use fastly::http::StatusCode; +use fastly::http::request::PendingRequest; +use fastly::http::{header, StatusCode}; use fastly::Request; -use trusted_server_core::auction::build_orchestrator; +use serde_json::json; +use trusted_server_core::auction::{ + build_orchestrator, AuctionContext, AuctionOrchestrator, AuctionProvider, AuctionRequest, + AuctionResponse, +}; use trusted_server_core::ec::registry::PartnerRegistry; +use trusted_server_core::error::TrustedServerError; use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::{ ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, @@ -118,9 +124,44 @@ impl PlatformGeo for NoopGeo { } } -fn create_test_settings() -> Settings { - let settings = Settings::from_toml( - r#" +struct DisabledRouteProvider; + +impl AuctionProvider for DisabledRouteProvider { + fn provider_name(&self) -> &'static str { + "disabled-route" + } + + fn request_bids( + &self, + _request: &AuctionRequest, + _context: &AuctionContext<'_>, + ) -> Result> { + Err(Report::new(TrustedServerError::Auction { + message: "disabled route provider should not launch requests".to_string(), + })) + } + + fn parse_response( + &self, + _response: fastly::Response, + _response_time_ms: u64, + ) -> Result> { + Err(Report::new(TrustedServerError::Auction { + message: "disabled route provider should not parse responses".to_string(), + })) + } + + fn timeout_ms(&self) -> u32 { + 2000 + } + + fn is_enabled(&self) -> bool { + false + } +} + +fn base_route_settings_toml() -> &'static str { + r#" [[handlers]] path = "^/_ts/admin" username = "admin" @@ -139,18 +180,32 @@ fn create_test_settings() -> Settings { enabled = false config_store_id = "test-config-store-id" secret_store_id = "test-secret-store-id" + "# +} +fn prebid_integration_toml() -> &'static str { + r#" [integrations.prebid] enabled = true server_url = "https://test-prebid.com/openrtb2/auction" + "# +} + +fn create_test_settings() -> Settings { + let base = base_route_settings_toml(); + let prebid = prebid_integration_toml(); + let config = format!( + r#"{base} + +{prebid} [auction] enabled = true providers = ["prebid"] timeout_ms = 2000 "#, - ) - .expect("should parse adapter route test settings"); + ); + let settings = Settings::from_toml(&config).expect("should parse adapter route test settings"); assert_eq!( JWKS_CONFIG_STORE_NAME, "jwks_store", @@ -160,6 +215,32 @@ fn create_test_settings() -> Settings { settings } +fn create_auction_test_settings(providers: &str) -> Settings { + let base = base_route_settings_toml(); + let prebid = prebid_integration_toml(); + let config = format!( + r#"{base} + +{prebid} + + [auction] + enabled = true + providers = {providers} + timeout_ms = 2000 + "#, + ); + + Settings::from_toml(&config).expect("should parse adapter auction route test settings") +} + +fn build_route_stack(settings: &Settings) -> (AuctionOrchestrator, IntegrationRegistry) { + let orchestrator = build_orchestrator(settings).expect("should build auction orchestrator"); + let integration_registry = + IntegrationRegistry::new(settings).expect("should create integration registry"); + + (orchestrator, integration_registry) +} + fn test_runtime_services(req: &Request) -> RuntimeServices { RuntimeServices::builder() .config_store(Arc::new(StubJwksConfigStore)) @@ -176,6 +257,54 @@ fn test_runtime_services(req: &Request) -> RuntimeServices { .build() } +fn route_auction(settings: &Settings, body: impl Into>) -> fastly::Response { + let (orchestrator, integration_registry) = build_route_stack(settings); + + route_auction_with_stack(settings, &orchestrator, &integration_registry, body) +} + +fn route_auction_with_stack( + settings: &Settings, + orchestrator: &AuctionOrchestrator, + integration_registry: &IntegrationRegistry, + body: impl Into>, +) -> fastly::Response { + let partner_registry = + PartnerRegistry::from_config(&settings.ec.partners).expect("should build partner registry"); + let req = Request::post("https://test.com/auction") + .with_header(header::CONTENT_TYPE, "application/json") + .with_body(body.into()); + let services = test_runtime_services(&req); + + futures::executor::block_on(route_request( + settings, + orchestrator, + integration_registry, + &partner_registry, + &services, + req, + )) + .expect("should route auction request") + .response + .expect("should buffer auction response in tests") +} + +fn valid_banner_ad_unit_body() -> Vec { + serde_json::to_vec(&json!({ + "adUnits": [ + { + "code": "div-gpt-ad-1", + "mediaTypes": { + "banner": { + "sizes": [[300, 250]] + } + } + } + ] + })) + .expect("should serialize valid auction route test body") +} + #[test] fn routes_use_request_local_consent() { let settings = create_test_settings(); @@ -228,3 +357,81 @@ fn routes_use_request_local_consent() { // Routes no longer depend on a separate consent KV store. Live consent is // request-local, and EC lifecycle state uses the EC identity store only. } + +#[test] +fn malformed_auction_json_returns_bad_request() { + let settings = create_auction_test_settings(r#"["prebid"]"#); + + let mut response = route_auction(&settings, "{not-json"); + + assert_eq!( + response.get_status(), + StatusCode::BAD_REQUEST, + "should reject malformed JSON as a client request error" + ); + assert!( + response.take_body_str().contains("Bad request"), + "should return a client-facing bad request message" + ); +} + +#[test] +fn invalid_auction_banner_size_returns_bad_request() { + let settings = create_auction_test_settings(r#"["prebid"]"#); + let body = serde_json::to_vec(&json!({ + "adUnits": [ + { + "code": "div-gpt-ad-1", + "mediaTypes": { + "banner": { + "sizes": [[300]] + } + } + } + ] + })) + .expect("should serialize invalid auction route test body"); + + let response = route_auction(&settings, body); + + assert_eq!( + response.get_status(), + StatusCode::BAD_REQUEST, + "should reject semantically invalid banner sizes as a client request error" + ); +} + +#[test] +fn auction_request_with_empty_provider_list_returns_bad_gateway() { + let settings = create_auction_test_settings("[]"); + + let response = route_auction(&settings, valid_banner_ad_unit_body()); + + assert_eq!( + response.get_status(), + StatusCode::BAD_GATEWAY, + "should surface no-provider orchestration failures as gateway errors" + ); +} + +#[test] +fn auction_request_with_disabled_provider_returns_bad_gateway() { + let settings = create_auction_test_settings(r#"["disabled-route"]"#); + let mut orchestrator = AuctionOrchestrator::new(settings.auction.clone()); + orchestrator.register_provider(Arc::new(DisabledRouteProvider)); + let integration_registry = + IntegrationRegistry::new(&settings).expect("should create integration registry"); + + let response = route_auction_with_stack( + &settings, + &orchestrator, + &integration_registry, + valid_banner_ad_unit_body(), + ); + + assert_eq!( + response.get_status(), + StatusCode::BAD_GATEWAY, + "should map skipped-provider launch failures to gateway errors" + ); +} diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index d175be9f..d08cb622 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -71,7 +71,7 @@ pub async fn handle_auction( return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE)); } let body: AdRequest = - serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Auction { + serde_json::from_slice(&body_bytes).change_context(TrustedServerError::BadRequest { message: "Failed to parse auction request body".to_string(), })?; From 2e861291ccc927c2a3136c36ac8024836916e862 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 13 May 2026 14:06:54 -0500 Subject: [PATCH 2/4] Fail auction when no providers launch --- crates/trusted-server-adapter-fastly/src/route_tests.rs | 4 ++++ crates/trusted-server-core/src/auction/orchestrator.rs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index 29a93a67..c84d4e16 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -7,12 +7,16 @@ use fastly::http::request::PendingRequest; use fastly::http::{header, StatusCode}; use fastly::Request; use serde_json::json; +<<<<<<< HEAD use trusted_server_core::auction::{ build_orchestrator, AuctionContext, AuctionOrchestrator, AuctionProvider, AuctionRequest, AuctionResponse, }; use trusted_server_core::ec::registry::PartnerRegistry; use trusted_server_core::error::TrustedServerError; +======= +use trusted_server_core::auction::{build_orchestrator, AuctionOrchestrator}; +>>>>>>> 680579cf (Fail auction when no providers launch) use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::{ ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index fd59c778..497ffb9e 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -384,6 +384,12 @@ impl AuctionOrchestrator { } } + if pending_requests.is_empty() { + return Err(Report::new(TrustedServerError::Auction { + message: "No provider requests launched".to_string(), + })); + } + let deadline = Duration::from_millis(u64::from(context.timeout_ms)); // lgtm[rust/cleartext-logging] // This info log reports request counts and timeout budget only; no secret settings are logged. From 8f0803c60bd0d5915d854e9e4792edf0eeef871e Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 28 May 2026 11:54:42 -0500 Subject: [PATCH 3/4] Validate configured auction providers at startup --- .../src/route_tests.rs | 4 - crates/trusted-server-core/src/auction/mod.rs | 79 ++++++++++++++ .../src/auction/orchestrator.rs | 101 +++++++++++++++++- 3 files changed, 177 insertions(+), 7 deletions(-) diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index c84d4e16..29a93a67 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -7,16 +7,12 @@ use fastly::http::request::PendingRequest; use fastly::http::{header, StatusCode}; use fastly::Request; use serde_json::json; -<<<<<<< HEAD use trusted_server_core::auction::{ build_orchestrator, AuctionContext, AuctionOrchestrator, AuctionProvider, AuctionRequest, AuctionResponse, }; use trusted_server_core::ec::registry::PartnerRegistry; use trusted_server_core::error::TrustedServerError; -======= -use trusted_server_core::auction::{build_orchestrator, AuctionOrchestrator}; ->>>>>>> 680579cf (Fail auction when no providers launch) use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::{ ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, diff --git a/crates/trusted-server-core/src/auction/mod.rs b/crates/trusted-server-core/src/auction/mod.rs index acf6d520..8de90000 100644 --- a/crates/trusted-server-core/src/auction/mod.rs +++ b/crates/trusted-server-core/src/auction/mod.rs @@ -73,6 +73,8 @@ pub fn build_orchestrator( } } + orchestrator.validate_configured_provider_names()?; + log::info!( "Auction orchestrator built with {} providers", orchestrator.provider_count() @@ -80,3 +82,80 @@ pub fn build_orchestrator( Ok(orchestrator) } + +#[cfg(test)] +mod tests { + use crate::settings::Settings; + use crate::test_support::tests::crate_test_settings_str; + + use super::build_orchestrator; + + fn settings_with_auction_config(auction_config: &str) -> Settings { + let settings_str = format!("{}\n{auction_config}", crate_test_settings_str()); + Settings::from_toml(&settings_str) + .expect("should parse auction provider validation test settings") + } + + fn assert_orchestrator_error_contains(settings: &Settings, expected: &str) { + let err = match build_orchestrator(settings) { + Ok(_) => panic!("build_orchestrator should reject invalid auction providers"), + Err(err) => err, + }; + assert!( + err.to_string().contains(expected), + "should include expected validation message: {expected}" + ); + } + + #[test] + fn configured_unregistered_provider_fails_startup() { + let settings = settings_with_auction_config( + r#" + [auction] + enabled = true + providers = ["missing-provider"] + timeout_ms = 2000 + "#, + ); + + assert_orchestrator_error_contains( + &settings, + "Auction provider `missing-provider` is configured but not registered", + ); + } + + #[test] + fn mixed_registered_and_unregistered_providers_fail_startup() { + let settings = settings_with_auction_config( + r#" + [auction] + enabled = true + providers = ["prebid", "missing-provider"] + timeout_ms = 2000 + "#, + ); + + assert_orchestrator_error_contains( + &settings, + "Auction provider `missing-provider` is configured but not registered", + ); + } + + #[test] + fn configured_unregistered_mediator_fails_startup() { + let settings = settings_with_auction_config( + r#" + [auction] + enabled = true + providers = ["prebid"] + mediator = "missing-mediator" + timeout_ms = 2000 + "#, + ); + + assert_orchestrator_error_contains( + &settings, + "Auction provider `missing-mediator` is configured but not registered", + ); + } +} diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index 497ffb9e..09591ea0 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -89,6 +89,32 @@ impl AuctionOrchestrator { self.providers.len() } + /// Validate that every configured provider name has a registered provider. + pub(crate) fn validate_configured_provider_names( + &self, + ) -> Result<(), Report> { + if !self.config.enabled { + return Ok(()); + } + + for provider_name in self + .config + .providers + .iter() + .chain(self.config.mediator.iter()) + { + if !self.providers.contains_key(provider_name) { + return Err(Report::new(TrustedServerError::Configuration { + message: format!( + "Auction provider `{provider_name}` is configured but not registered" + ), + })); + } + } + + Ok(()) + } + /// Execute an auction using the auto-detected strategy. /// /// Strategy is determined by mediator configuration: @@ -386,7 +412,10 @@ impl AuctionOrchestrator { if pending_requests.is_empty() { return Err(Report::new(TrustedServerError::Auction { - message: "No provider requests launched".to_string(), + message: format!( + "All {} configured provider(s) skipped or failed to launch", + provider_names.len() + ), })); } @@ -653,15 +682,19 @@ impl OrchestrationResult { #[cfg(test)] mod tests { use crate::auction::config::AuctionConfig; + use crate::auction::provider::AuctionProvider; use crate::auction::test_support::create_test_auction_context; use crate::auction::types::{ - AdFormat, AdSlot, AuctionRequest, Bid, BidStatus, MediaType, PublisherInfo, UserInfo, + AdFormat, AdSlot, AuctionContext, AuctionRequest, AuctionResponse, Bid, BidStatus, + MediaType, PublisherInfo, UserInfo, }; use crate::error::TrustedServerError; use crate::test_support::tests::crate_test_settings_str; use error_stack::Report; - use fastly::Request; + use fastly::http::request::PendingRequest; + use fastly::{Request, Response}; use std::collections::{HashMap, HashSet}; + use std::sync::Arc; use super::AuctionOrchestrator; @@ -712,6 +745,42 @@ mod tests { crate::settings::Settings::from_toml(&settings_str).expect("should parse test settings") } + struct LaunchFailingProvider; + + impl AuctionProvider for LaunchFailingProvider { + fn provider_name(&self) -> &'static str { + "launch-failing" + } + + fn request_bids( + &self, + _request: &AuctionRequest, + _context: &AuctionContext<'_>, + ) -> Result> { + Err(Report::new(TrustedServerError::Auction { + message: "launch failed in test provider".to_string(), + })) + } + + fn parse_response( + &self, + _response: Response, + _response_time_ms: u64, + ) -> Result> { + Err(Report::new(TrustedServerError::Auction { + message: "launch-failing provider should not parse responses".to_string(), + })) + } + + fn timeout_ms(&self) -> u32 { + 2000 + } + + fn backend_name(&self, _timeout_ms: u32) -> Option { + Some("launch-failing-backend".to_string()) + } + } + #[test] fn provider_error_response_includes_diagnostic_metadata() { let error = Report::new(TrustedServerError::Auction { @@ -883,6 +952,32 @@ mod tests { assert!(format!("{}", err).contains("No providers configured")); } + #[tokio::test] + async fn provider_launch_failures_error_when_no_requests_launch() { + let config = AuctionConfig { + enabled: true, + providers: vec!["launch-failing".to_string()], + timeout_ms: 2000, + ..Default::default() + }; + let mut orchestrator = AuctionOrchestrator::new(config); + orchestrator.register_provider(Arc::new(LaunchFailingProvider)); + + let request = create_test_auction_request(); + let settings = create_test_settings(); + let req = Request::get("https://test.com/test"); + let context = create_test_auction_context(&settings, &req, 2000); + + let result = orchestrator.run_auction(&request, &context).await; + + let err = result.expect_err("should fail when every provider launch fails"); + assert!( + err.to_string() + .contains("All 1 configured provider(s) skipped or failed to launch"), + "should explain that no configured provider request launched" + ); + } + #[test] fn test_orchestrator_is_enabled() { let config = AuctionConfig { From efaaba61965d9426836f9e3248fa8a07a0d7734e Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 5 Jun 2026 14:54:26 -0500 Subject: [PATCH 4/4] Address auction review nits --- crates/trusted-server-core/src/auction/mod.rs | 6 +++--- crates/trusted-server-core/src/auction/orchestrator.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/trusted-server-core/src/auction/mod.rs b/crates/trusted-server-core/src/auction/mod.rs index 8de90000..ac61a4fa 100644 --- a/crates/trusted-server-core/src/auction/mod.rs +++ b/crates/trusted-server-core/src/auction/mod.rs @@ -120,7 +120,7 @@ mod tests { assert_orchestrator_error_contains( &settings, - "Auction provider `missing-provider` is configured but not registered", + "Auction provider `missing-provider` is listed in [auction] but no enabled integration provides it", ); } @@ -137,7 +137,7 @@ mod tests { assert_orchestrator_error_contains( &settings, - "Auction provider `missing-provider` is configured but not registered", + "Auction provider `missing-provider` is listed in [auction] but no enabled integration provides it", ); } @@ -155,7 +155,7 @@ mod tests { assert_orchestrator_error_contains( &settings, - "Auction provider `missing-mediator` is configured but not registered", + "Auction provider `missing-mediator` is listed in [auction] but no enabled integration provides it", ); } } diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index 09591ea0..30d99429 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -89,7 +89,7 @@ impl AuctionOrchestrator { self.providers.len() } - /// Validate that every configured provider name has a registered provider. + /// Validate that every configured provider name has an enabled provider integration. pub(crate) fn validate_configured_provider_names( &self, ) -> Result<(), Report> { @@ -106,7 +106,7 @@ impl AuctionOrchestrator { if !self.providers.contains_key(provider_name) { return Err(Report::new(TrustedServerError::Configuration { message: format!( - "Auction provider `{provider_name}` is configured but not registered" + "Auction provider `{provider_name}` is listed in [auction] but no enabled integration provides it" ), })); }