Skip to content
Draft
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
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ ts --help

# Create local config, then edit placeholders before validation
ts config init
# Edit trusted-server.toml
# Edit trusted-server.toml. Server auctions use map-shaped
# [auction.providers.<id>] and [auction.bidders.<id>] tables.
ts config validate

# Audit a public page with Chrome/Chromium to bootstrap a draft config
Expand Down
48 changes: 26 additions & 22 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,29 +54,35 @@ curl -X POST http://localhost:7676/auction \
- Logs showing: `"Using legacy Prebid flow"`
- Direct Prebid Server call (backward compatible)

##Configuration
## Configuration

Edit `trusted-server.toml` to customize the auction:

```toml
# Enable/disable orchestrator
[auction]
enabled = true
providers = ["prebid", "aps"]
mediator = "adserver_mock" # If set: mediation, if omitted: highest bid wins
timeout_ms = 2000
mediator = "adserver_mock"

# APS OpenRTB provider. The built-in production endpoint is used when
# endpoint is omitted; use only an account authorized for test traffic.
[integrations.aps]
enabled = true
account_id = "example-account"
timeout_ms = 800
debug = false
[auction.providers.pbs-main]
protocol = "openrtb-2.6"
profile = "prebid-server"
endpoint = "https://prebid.example.com/openrtb2/auction"
routing = "explicit"

[auction.providers.aps-main]
protocol = "openrtb-2.6"
profile = "aps"
endpoint = "https://aps.example.com/e/pb/bid"
routing = "all_eligible"
profile_config = { account_id = "example-aps-account", debug = false }

[auction.bidders.example-server]
provider = "pbs-main"

[integrations.adserver_mock]
enabled = true
endpoint = "http://localhost:6767/adserver/mediate"
endpoint = "https://mediator.example.com/mediate"
timeout_ms = 500
```

Expand All @@ -87,8 +93,7 @@ timeout_ms = 500
```toml
[auction]
enabled = true
providers = ["prebid", "aps"]
mediator = "adserver_mock" # Mediator configured = parallel mediation strategy
mediator = "adserver_mock" # Providers come from [auction.providers.*] maps
```

**Expected Flow:**
Expand All @@ -102,25 +107,24 @@ mediator = "adserver_mock" # Mediator configured = parallel mediation strategy
```toml
[auction]
enabled = true
providers = ["prebid", "aps"]
# No mediator = parallel only strategy
# Configured [auction.providers.*] run without a mediator
```

**Expected Flow:**
1. Prebid and APS run in parallel
2. Highest bid wins automatically
3. No mediation

### Scenario 3: Legacy Mode (Backward Compatible)
### Scenario 3: Auction Disabled

**Config:**

```toml
[auction]
enabled = false
```

**Expected Flow:**
- Original Prebid-only behavior
- No orchestration overhead
**Expected Flow:** no auction provider dispatch.

## Debugging

Expand Down Expand Up @@ -149,10 +153,10 @@ INFO: Registering auction provider: adserver_mock
### Common Issues

**Issue:** `"Provider 'aps' not registered"`
**Fix:** Make sure `[integrations.aps]` is configured in `trusted-server.toml`
**Fix:** Make sure an `[auction.providers.<id>]` entry selects `profile = "aps"`

**Issue:** `"No providers configured"`
**Fix:** Make sure `providers = ["prebid", "aps"]` is set in `[auction]`
**Fix:** Make sure map-shaped `[auction.providers.<id>]` entries are configured

**Issue:** Tests fail with WASM errors
**Explanation:** Async tests don't work in WASM test environment. Integration tests via HTTP work fine!
Expand Down
10 changes: 7 additions & 3 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use edgezero_core::http::{
use edgezero_core::router::RouterService;
use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::auction::{
AuctionOrchestrator, build_orchestrator_with_plan, compile_auction_plan,
};
use trusted_server_core::ec::EcContext;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
Expand Down Expand Up @@ -69,8 +71,10 @@ fn build_state() -> Result<Arc<AppState>, Report<TrustedServerError>> {
fn build_state_with_settings(
settings: Settings,
) -> Result<Arc<AppState>, Report<TrustedServerError>> {
let orchestrator = build_orchestrator(&settings)?;
let registry = IntegrationRegistry::new(&settings)?;
let plan = Arc::new(compile_auction_plan(&settings)?);
plan.validate_for_target(trusted_server_core::platform::AuctionTargetId::Axum)?;
let orchestrator = build_orchestrator_with_plan(Arc::clone(&plan), &settings)?;
let registry = IntegrationRegistry::with_plan(&settings, plan)?;

Ok(Arc::new(AppState {
settings: Arc::new(settings),
Expand Down
74 changes: 54 additions & 20 deletions crates/trusted-server-adapter-axum/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ use async_trait::async_trait;
use edgezero_core::http::{HeaderMap, HeaderName, HeaderValue, header};
use error_stack::{Report, ResultExt as _};
use trusted_server_core::platform::{
ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError,
PlatformGeo, PlatformHttpClient, PlatformHttpRequest, PlatformPendingRequest, PlatformResponse,
PlatformSecretStore, PlatformSelectResult, RuntimeServices, StoreId, StoreName,
BackendNamingPolicy, ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec,
PlatformConfigStore, PlatformError, PlatformGeo, PlatformHttpClient, PlatformHttpRequest,
PlatformPendingRequest, PlatformResponse, PlatformSecretStore, PlatformSelectResult,
RuntimeServices, StoreId, StoreName,
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -154,24 +155,15 @@ impl PlatformSecretStore for AxumPlatformSecretStore {
pub struct AxumPlatformBackend;

impl PlatformBackend for AxumPlatformBackend {
fn naming_policy(&self) -> BackendNamingPolicy {
BackendNamingPolicy::Axum
}

fn predict_name(&self, spec: &PlatformBackendSpec) -> Result<String, Report<PlatformError>> {
let port = spec
.port
.unwrap_or(if spec.scheme == "https" { 443 } else { 80 });
// Keep two providers that share an origin on distinct names so auction
// response correlation cannot cross providers.
let discriminator = spec
.discriminator
.as_deref()
.map(|d| format!("_p_{}", normalize_env_segment(d)))
.unwrap_or_default();
Ok(format!(
"{}_{}_{}{}",
normalize_env_segment(&spec.scheme),
normalize_env_segment(&spec.host),
port,
discriminator,
))
self.naming_policy()
.predict(spec)
.map(|prediction| prediction.name)
.change_context(PlatformError::Backend)
}

fn ensure(&self, spec: &PlatformBackendSpec) -> Result<String, Report<PlatformError>> {
Expand Down Expand Up @@ -601,6 +593,21 @@ mod tests {
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};

#[test]
fn auction_http_capabilities_are_explicit() {
let client = AxumPlatformHttpClient::new();
let capabilities = trusted_server_core::platform::AuctionTargetId::Axum
.descriptor()
.capabilities();
assert!(client.supports_concurrent_fanout());
assert!(capabilities.supports_concurrent_provider_fanout());
assert!(!client.has_enforceable_total_request_deadline());
assert!(
!capabilities.has_enforceable_total_request_deadline(),
"reqwest's transport timeout is not an adapter-enforced auction deadline"
);
}

#[test]
fn config_store_reads_from_env_var() {
temp_env::with_var(
Expand Down Expand Up @@ -693,6 +700,33 @@ mod tests {
assert!(with_ip.is_none(), "should return None for any IP");
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_client_surfaces_redirect_without_following() {
let url = serve_raw_response(
b"HTTP/1.1 302 Found\r\nLocation: https://redirect.example/next\r\nContent-Length: 0\r\n\r\n",
)
.await;
let request = edgezero_core::http::request_builder()
.uri(url)
.body(EdgeBody::empty())
.expect("should build outbound request");

let response = AxumPlatformHttpClient::new()
.send(PlatformHttpRequest::new(request, "test_backend"))
.await
.expect("should surface redirect")
.response;

assert_eq!(response.status().as_u16(), 302);
assert_eq!(
response
.headers()
.get(edgezero_core::http::header::LOCATION)
.and_then(|value| value.to_str().ok()),
Some("https://redirect.example/next")
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn http_client_strips_hop_by_hop_response_headers() {
let url = serve_raw_response(
Expand Down
46 changes: 42 additions & 4 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const LEGACY_ADMIN_DENY_METHODS: &[&str] =
/// The settings baked into the binary contain placeholder secrets that
/// `get_settings()` rejects by design, which would turn every route into a
/// startup error page (and its route table into the fallback-only set).
fn test_router() -> edgezero_core::router::RouterService {
let settings = trusted_server_core::settings::Settings::from_toml(
fn test_settings() -> trusted_server_core::settings::Settings {
trusted_server_core::settings::Settings::from_toml(
r#"
[[handlers]]
path = "^/_ts/admin"
Expand All @@ -36,9 +36,11 @@ fn test_router() -> edgezero_core::router::RouterService {
passphrase = "test-secret-key-32-bytes-minimum"
"#,
)
.expect("should parse route test settings");
.expect("should parse route test settings")
}

TrustedServerApp::routes_with_settings(settings)
fn test_router() -> edgezero_core::router::RouterService {
TrustedServerApp::routes_with_settings(test_settings())
.expect("should build router from test settings")
}

Expand All @@ -62,6 +64,42 @@ fn assert_route_registered(method: &str, path: &str) {
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn aps_profile_serves_renderer_through_adapter_fallback() {
let mut settings = test_settings();
settings.auction.providers.insert(
"aps-main".parse().expect("should parse APS provider ID"),
trusted_server_core::auction::ProviderConfig {
protocol: "openrtb-2.6".to_string(),
profile: "aps".to_string(),
endpoint: "https://aps.example/e/pb/bid".to_string(),
timeout_ms: None,
routing: trusted_server_core::auction::RoutingMode::AllEligible,
notifications: trusted_server_core::auction::NotificationConfig::default(),
profile_config: "{\"account_id\":\"example-account\"}"
.parse()
.expect("should parse APS profile config"),
},
);
let router = TrustedServerApp::routes_with_settings(settings)
.expect("should build router with APS profile");
let mut service = EdgeZeroAxumService::new(router);
let request = Request::builder()
.method("GET")
.uri("/integrations/aps/renderer")
.body(AxumBody::empty())
.expect("should build APS renderer request");

let response = service
.ready()
.await
.expect("should be ready")
.call(request)
.await
.expect("should serve APS renderer");
assert_eq!(response.status().as_u16(), 200);
}

/// Verify that every expected explicit route is registered in the route table.
///
/// Uses [`RouterService::routes()`] for introspection rather than checking
Expand Down
Loading