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
155 changes: 145 additions & 10 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 @@ -117,9 +118,8 @@ impl PlatformGeo for NoopGeo {
}
}

fn create_test_settings() -> Settings {
let settings = Settings::from_toml(
r#"
fn base_route_settings_toml() -> &'static str {
r#"
[[handlers]]
path = "^/admin"
username = "admin"
Expand All @@ -138,21 +138,35 @@ fn create_test_settings() -> Settings {
enabled = false
config_store_id = "test-config-store-id"
secret_store_id = "test-secret-store-id"
"#
}

[consent]
consent_store = "missing-consent-store"

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}

[consent]
consent_store = "missing-consent-store"

{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",
Expand All @@ -162,6 +176,32 @@ fn create_test_settings() -> Settings {
settings
}

fn create_auction_test_settings_without_consent_store(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")
}
Comment thread
ChristianPavilonis marked this conversation as resolved.

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 +218,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": [
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.

nitpick — This bids array references bidder "missing-provider" that is never asserted on, and the only caller runs with providers = [], so it's irrelevant to the assertion. For a helper named valid_banner_ad_unit_body it's a bit confusing — dropping the bids block would read cleaner.

{
"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 +328,59 @@ 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#"["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_without_consent_store(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_without_consent_store("[]");

let response = route_auction(&settings, valid_banner_ad_unit_body());

assert_eq!(
response.get_status(),
StatusCode::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.

🤔 thinking — This test uses providers = [], which hits the pre-existing "No providers configured" guard in orchestrator.rs (run_providers_parallel, ~line 284), not the new pending_requests.is_empty() branch (~line 391) this PR adds. Both return TrustedServerError::Auction → 502, so the route-level mapping is verified — but the new branch's end-to-end route mapping isn't covered here (only the LaunchFailingProvider orchestrator unit test exercises it).

Consider a route test with a registered-but-disabled provider so the request reaches the new branch end-to-end.

"should surface no-provider orchestration failures as gateway errors"
);
}
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
79 changes: 79 additions & 0 deletions crates/trusted-server-core/src/auction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,89 @@ pub fn build_orchestrator(
}
}

orchestrator.validate_configured_provider_names()?;

log::info!(
"Auction orchestrator built with {} providers",
orchestrator.provider_count()
);

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",
);
}
}
Loading