Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 151 additions & 2 deletions crates/trusted-server-adapter-fastly/src/route_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand All @@ -178,6 +219,45 @@ fn test_runtime_services(req: &Request) -> RuntimeServices {
.build()
}

fn route_auction(settings: &Settings, body: impl Into<Vec<u8>>) -> 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<u8> {
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();
Expand Down Expand Up @@ -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() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench — Missing auction provider should fail startup, not become request-time 502

This test codifies auction.providers = ["missing-provider"] as a successful startup followed by 502 Bad Gateway on /auction. That conflicts with the project rule that invalid enabled integrations/providers must not be silently logged and disabled during startup or registration. With the current behavior, a deployment can start with a typo or missing provider config and only fail when traffic hits /auction.

Fix: validate configured auction providers after registration in build_orchestrator, including the mediator if configured, and return a startup/configuration error when a configured name was not registered. Keep the new No provider requests launched runtime guard for true launch-time failures, but update this route test to expect startup failure or add a core unit test with a registered fake provider whose request_bids returns Err.

if settings.auction.enabled {
    for provider_name in settings
        .auction
        .providers
        .iter()
        .chain(settings.auction.mediator.iter())
    {
        if !registered_provider_names.contains(provider_name) {
            return Err(Report::new(TrustedServerError::Configuration {
                message: format!("Auction provider `{provider_name}` is configured but not registered"),
            }));
        }
    }
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree.

I'll make a proposal to remove our misconfiguration fails startup, and move to config validation before config persistence in or after pr #669.

tooling should be in place to prevent these issues, but hosing a production site should probably be unacceptable vs failing with good observability.

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"
);
}
2 changes: 1 addition & 1 deletion crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub async fn handle_auction(
) -> Result<Response, Report<TrustedServerError>> {
// 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(),
},
)?;
Expand Down
6 changes: 6 additions & 0 deletions crates/trusted-server-core/src/auction/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)...",
Expand Down
Loading