diff --git a/crates/trusted-server-adapter-fastly/src/route_tests.rs b/crates/trusted-server-adapter-fastly/src/route_tests.rs index 0fd0113f..2be7bc98 100644 --- a/crates/trusted-server-adapter-fastly/src/route_tests.rs +++ b/crates/trusted-server-adapter-fastly/src/route_tests.rs @@ -3,9 +3,10 @@ use std::sync::Arc; use edgezero_core::key_value_store::NoopKvStore; use error_stack::Report; -use fastly::http::StatusCode; +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, AuctionOrchestrator}; use trusted_server_core::integrations::IntegrationRegistry; use trusted_server_core::platform::{ ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, @@ -162,6 +163,46 @@ fn create_test_settings() -> Settings { settings } +fn create_auction_test_settings_without_consent_store(providers: &str) -> Settings { + let config = format!( + r#" + [[handlers]] + path = "^/admin" + username = "admin" + password = "admin-pass" + + [publisher] + domain = "test-publisher.com" + cookie_domain = ".test-publisher.com" + origin_url = "https://origin.test-publisher.com" + proxy_secret = "unit-test-proxy-secret" + + [edge_cookie] + secret_key = "test-secret-key" + + [request_signing] + enabled = false + config_store_id = "test-config-store-id" + secret_store_id = "test-secret-store-id" + + [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)) @@ -178,6 +219,45 @@ 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); + 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, + &services, + req, + )) + .expect("should route auction request") +} + +fn valid_banner_ad_unit_body() -> Vec { + serde_json::to_vec(&json!({ + "adUnits": [ + { + "code": "div-gpt-ad-1", + "mediaTypes": { + "banner": { + "sizes": [[300, 250]] + } + }, + "bids": [ + { + "bidder": "missing-provider", + "params": {} + } + ] + } + ] + })) + .expect("should serialize valid auction route test body") +} + #[test] fn configured_missing_consent_store_only_breaks_consent_routes() { let settings = create_test_settings(); @@ -249,3 +329,72 @@ fn configured_missing_consent_store_only_breaks_consent_routes() { "should scope consent store failures to the consent-dependent routes" ); } + +#[test] +fn malformed_auction_json_returns_bad_request() { + let settings = create_auction_test_settings_without_consent_store(r#"["missing-provider"]"#); + + 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_without_consent_store(r#"["missing-provider"]"#); + 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 valid_auction_request_with_no_providers_returns_bad_gateway() { + let settings = create_auction_test_settings_without_consent_store("[]"); + + 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 valid_auction_request_with_unregistered_provider_returns_bad_gateway() { + let settings = create_auction_test_settings_without_consent_store(r#"["missing-provider"]"#); + + let response = route_auction(&settings, valid_banner_ad_unit_body()); + + assert_eq!( + response.get_status(), + StatusCode::BAD_GATEWAY, + "should fail when configured providers cannot be launched" + ); +} diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index 0430f08b..a34b2d5c 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -37,7 +37,7 @@ pub async fn handle_auction( ) -> Result> { // Parse request body let body: AdRequest = serde_json::from_slice(&req.take_body_bytes()).change_context( - TrustedServerError::Auction { + TrustedServerError::BadRequest { message: "Failed to parse auction request body".to_string(), }, )?; diff --git a/crates/trusted-server-core/src/auction/orchestrator.rs b/crates/trusted-server-core/src/auction/orchestrator.rs index 9cbcd2b9..e43c928f 100644 --- a/crates/trusted-server-core/src/auction/orchestrator.rs +++ b/crates/trusted-server-core/src/auction/orchestrator.rs @@ -362,6 +362,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)); log::info!( "Launched {} concurrent requests, waiting for responses (timeout: {}ms)...",