From 96f198cb812e5eb9ec841f1c3b710dc8132ec1a2 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 27 May 2026 13:50:28 -0500 Subject: [PATCH] Tighten cross-adapter verification tests --- .github/workflows/test.yml | 2 +- crates/integration-tests/tests/parity.rs | 50 ++-- .../tests/routes.rs | 223 ++++-------------- .../tests/routes.rs | 131 ++-------- 4 files changed, 98 insertions(+), 308 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c45b8cc..0b651437 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -127,7 +127,7 @@ jobs: run: cargo test --manifest-path crates/integration-tests/Cargo.toml --test parity - name: Clippy (parity test crate) - run: cargo clippy --manifest-path crates/integration-tests/Cargo.toml -- -D warnings + run: cargo clippy --manifest-path crates/integration-tests/Cargo.toml --all-targets -- -D warnings test-typescript: name: vitest diff --git a/crates/integration-tests/tests/parity.rs b/crates/integration-tests/tests/parity.rs index 6bcf8d49..e6732817 100644 --- a/crates/integration-tests/tests/parity.rs +++ b/crates/integration-tests/tests/parity.rs @@ -104,6 +104,14 @@ async fn cf_post_headers(uri: &str, body: &str) -> (u16, HeaderMap) { (s, h) } +fn header_value<'a>(headers: &'a HeaderMap, name: &str, adapter: &str) -> &'a str { + headers + .get(name) + .unwrap_or_else(|| panic!("{adapter} response must include {name}")) + .to_str() + .unwrap_or_else(|_| panic!("{adapter} {name} header must be valid UTF-8")) +} + // --------------------------------------------------------------------------- // Route parity: same route → same status on both adapters // --------------------------------------------------------------------------- @@ -215,18 +223,15 @@ async fn admin_rotate_unauthenticated_parity() { "both adapters must return the same status for unauthenticated admin route" ); - assert!( - axum_headers.contains_key("www-authenticate"), - "Axum 401 must include WWW-Authenticate header" + let axum_www_auth = header_value(&axum_headers, "www-authenticate", "Axum"); + let cf_www_auth = header_value(&cf_headers, "www-authenticate", "Cloudflare"); + assert_eq!( + axum_www_auth, cf_www_auth, + "WWW-Authenticate values must match across adapters" ); - let cf_www_auth = cf_headers - .get("www-authenticate") - .expect("should have www-authenticate header on 401") - .to_str() - .expect("should be valid UTF-8"); assert!( - cf_www_auth.starts_with("Basic realm="), - "Cloudflare 401 WWW-Authenticate must be Basic scheme: {cf_www_auth:?}" + axum_www_auth.starts_with("Basic realm="), + "WWW-Authenticate must be Basic scheme: {axum_www_auth:?}" ); } @@ -249,13 +254,15 @@ async fn admin_deactivate_unauthenticated_parity() { "both adapters must return the same status for unauthenticated admin/keys/deactivate" ); - assert!( - axum_headers.contains_key("www-authenticate"), - "Axum 401 on admin/keys/deactivate must include WWW-Authenticate header" + let axum_www_auth = header_value(&axum_headers, "www-authenticate", "Axum"); + let cf_www_auth = header_value(&cf_headers, "www-authenticate", "Cloudflare"); + assert_eq!( + axum_www_auth, cf_www_auth, + "WWW-Authenticate values must match across adapters" ); assert!( - cf_headers.contains_key("www-authenticate"), - "Cloudflare 401 on admin/keys/deactivate must include WWW-Authenticate header" + axum_www_auth.starts_with("Basic realm="), + "WWW-Authenticate must be Basic scheme: {axum_www_auth:?}" ); } @@ -279,13 +286,12 @@ async fn geo_header_parity_on_all_responses() { cf_post_headers(path, body).await }; - assert!( - axum_headers.contains_key("x-geo-info-available"), - "Axum: {method} {path} (status={axum_status}) must have X-Geo-Info-Available" - ); - assert!( - cf_headers.contains_key("x-geo-info-available"), - "Cloudflare: {method} {path} (status={cf_status}) must have X-Geo-Info-Available" + let axum_geo = header_value(&axum_headers, "x-geo-info-available", "Axum"); + let cf_geo = header_value(&cf_headers, "x-geo-info-available", "Cloudflare"); + assert_eq!( + axum_geo, cf_geo, + "X-Geo-Info-Available values must match for {method} {path} \ + (axum_status={axum_status} cf_status={cf_status})" ); } } diff --git a/crates/trusted-server-adapter-axum/tests/routes.rs b/crates/trusted-server-adapter-axum/tests/routes.rs index 736b3d7c..b2ab1736 100644 --- a/crates/trusted-server-adapter-axum/tests/routes.rs +++ b/crates/trusted-server-adapter-axum/tests/routes.rs @@ -15,117 +15,59 @@ fn make_service() -> EdgeZeroAxumService { EdgeZeroAxumService::new(TrustedServerApp::routes()) } +fn assert_route_registered( + router: &edgezero_core::router::RouterService, + method: &str, + path: &str, +) { + assert!( + router + .routes() + .iter() + .any(|route| route.method().as_str() == method && route.path() == path), + "{method} {path} must be explicitly registered before the wildcard fallback" + ); +} + // --------------------------------------------------------------------------- // Route smoke tests — verify routing (not business logic correctness) // --------------------------------------------------------------------------- #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn discovery_endpoint_is_routed() { - // Verifies the route exists — 5xx from missing signing keys is acceptable; - // 404 is not (that would mean the route was not registered). - let mut svc = make_service(); - - let req = Request::builder() - .method("GET") - .uri("/.well-known/trusted-server.json") - .body(AxumBody::empty()) - .expect("should build request"); - - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - - assert_ne!( - resp.status().as_u16(), - 404, - "discovery endpoint must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "GET", "/.well-known/trusted-server.json"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn verify_signature_endpoint_is_routed() { - let mut svc = make_service(); - - let req = Request::builder() - .method("POST") - .uri("/verify-signature") - .header("content-type", "application/json") - .body(AxumBody::from("{}")) - .expect("should build request"); - - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - - assert_ne!( - resp.status().as_u16(), - 404, - "verify-signature must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/verify-signature"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn admin_rotate_key_is_routed() { - let mut svc = make_service(); - - let req = Request::builder() - .method("POST") - .uri("/admin/keys/rotate") - .header("content-type", "application/json") - .body(AxumBody::from("{}")) - .expect("should build request"); - - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - - assert_ne!( - resp.status().as_u16(), - 404, - "admin/keys/rotate must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/admin/keys/rotate"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn admin_deactivate_key_is_routed() { - let mut svc = make_service(); - - let req = Request::builder() - .method("POST") - .uri("/admin/keys/deactivate") - .header("content-type", "application/json") - .body(AxumBody::from("{}")) - .expect("should build request"); - - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/admin/keys/deactivate"); +} - assert_ne!( - resp.status().as_u16(), - 404, - "admin/keys/deactivate must be routed" - ); +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auction_endpoint_is_routed() { + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/auction"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn admin_rotate_key_returns_non_5xx() { + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/admin/keys/rotate"); + // Admin routes return 501 Not Implemented on the Axum dev server (store // writes are unsupported). Auth middleware may short-circuit with 4xx // before reaching the handler. Either way, no panic or unhandled 500. @@ -351,6 +293,9 @@ async fn auction_endpoint_does_not_require_auth() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn admin_route_returns_non_404_non_5xx() { + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/admin/keys/rotate"); + let mut svc = make_service(); let req = Request::builder() @@ -434,112 +379,30 @@ async fn admin_deactivate_key_auth_fail_returns_401() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_proxy_is_routed() { - let mut svc = make_service(); - let req = Request::builder() - .method("GET") - .uri("/first-party/proxy") - .body(AxumBody::empty()) - .expect("should build request"); - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - assert_ne!( - resp.status().as_u16(), - 404, - "/first-party/proxy must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "GET", "/first-party/proxy"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_click_is_routed() { - let mut svc = make_service(); - let req = Request::builder() - .method("GET") - .uri("/first-party/click") - .body(AxumBody::empty()) - .expect("should build request"); - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - assert_ne!( - resp.status().as_u16(), - 404, - "/first-party/click must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "GET", "/first-party/click"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_sign_get_is_routed() { - let mut svc = make_service(); - let req = Request::builder() - .method("GET") - .uri("/first-party/sign") - .body(AxumBody::empty()) - .expect("should build request"); - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - assert_ne!( - resp.status().as_u16(), - 404, - "GET /first-party/sign must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "GET", "/first-party/sign"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_sign_post_is_routed() { - let mut svc = make_service(); - let req = Request::builder() - .method("POST") - .uri("/first-party/sign") - .header("content-type", "application/json") - .body(AxumBody::from("{}")) - .expect("should build request"); - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - assert_ne!( - resp.status().as_u16(), - 404, - "POST /first-party/sign must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/first-party/sign"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_proxy_rebuild_is_routed() { - let mut svc = make_service(); - let req = Request::builder() - .method("POST") - .uri("/first-party/proxy-rebuild") - .header("content-type", "application/json") - .body(AxumBody::from("{}")) - .expect("should build request"); - let resp = svc - .ready() - .await - .expect("should be ready") - .call(req) - .await - .expect("should respond"); - assert_ne!( - resp.status().as_u16(), - 404, - "/first-party/proxy-rebuild must be routed" - ); + let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/first-party/proxy-rebuild"); } diff --git a/crates/trusted-server-adapter-cloudflare/tests/routes.rs b/crates/trusted-server-adapter-cloudflare/tests/routes.rs index c6cfdb25..4baa1752 100644 --- a/crates/trusted-server-adapter-cloudflare/tests/routes.rs +++ b/crates/trusted-server-adapter-cloudflare/tests/routes.rs @@ -15,6 +15,20 @@ fn routes_build_without_panic() { let _router = TrustedServerApp::routes(); } +fn assert_route_registered( + router: &edgezero_core::router::RouterService, + method: &str, + path: &str, +) { + assert!( + router + .routes() + .iter() + .any(|route| route.method().as_str() == method && route.path() == path), + "{method} {path} must be explicitly registered before the wildcard fallback" + ); +} + // --------------------------------------------------------------------------- // Middleware regression tests — verify FinalizeResponseMiddleware and // AuthMiddleware are wired so they cannot be removed silently. @@ -48,8 +62,9 @@ async fn auth_middleware_runs_in_chain_for_protected_routes() { // // CI settings may not have basic_auth configured, so this test does not // assert 401 — it asserts that both middleware layers ran (X-Geo-Info-Available - // present) and that the route is actually reached (status != 404). + // present) and separately verifies the explicit auction route registration. let router = TrustedServerApp::routes(); + assert_route_registered(&router, "POST", "/auction"); let req = request_builder() .method("POST") @@ -74,7 +89,7 @@ async fn auth_middleware_runs_in_chain_for_protected_routes() { } // --------------------------------------------------------------------------- -// Route smoke tests — verify all adapter routes are registered and do not 5xx +// Route smoke tests — verify all adapter routes are explicitly registered // --------------------------------------------------------------------------- #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -95,149 +110,55 @@ async fn tsjs_route_is_routed_not_5xx() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn verify_signature_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("POST") - .uri("/verify-signature") - .header("content-type", "application/json") - .body(edgezero_core::body::Body::from("{}")) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!( - resp.status().as_u16(), - 404, - "/verify-signature must be routed" - ); + assert_route_registered(&router, "POST", "/verify-signature"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn admin_rotate_key_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("POST") - .uri("/admin/keys/rotate") - .header("content-type", "application/json") - .body(edgezero_core::body::Body::from("{}")) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!( - resp.status().as_u16(), - 404, - "/admin/keys/rotate must be routed" - ); + assert_route_registered(&router, "POST", "/admin/keys/rotate"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn admin_deactivate_key_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("POST") - .uri("/admin/keys/deactivate") - .header("content-type", "application/json") - .body(edgezero_core::body::Body::from("{}")) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!( - resp.status().as_u16(), - 404, - "/admin/keys/deactivate must be routed" - ); + assert_route_registered(&router, "POST", "/admin/keys/deactivate"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn auction_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("POST") - .uri("/auction") - .header("content-type", "application/json") - .body(edgezero_core::body::Body::from(r#"{"adUnits":[]}"#)) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!(resp.status().as_u16(), 404, "/auction must be routed"); + assert_route_registered(&router, "POST", "/auction"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_proxy_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("GET") - .uri("/first-party/proxy") - .body(edgezero_core::body::Body::empty()) - .expect("should build request"); - let resp = router.oneshot(req).await; - // Handlers require valid outbound proxy settings; they may return 4xx/5xx in CI. - // The assertion is routing only: the path must not fall through to the 404 not-found handler. - assert_ne!( - resp.status().as_u16(), - 404, - "/first-party/proxy must be routed" - ); + assert_route_registered(&router, "GET", "/first-party/proxy"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_click_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("GET") - .uri("/first-party/click") - .body(edgezero_core::body::Body::empty()) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!( - resp.status().as_u16(), - 404, - "/first-party/click must be routed" - ); + assert_route_registered(&router, "GET", "/first-party/click"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_sign_get_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("GET") - .uri("/first-party/sign") - .body(edgezero_core::body::Body::empty()) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!( - resp.status().as_u16(), - 404, - "GET /first-party/sign must be routed" - ); + assert_route_registered(&router, "GET", "/first-party/sign"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_sign_post_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("POST") - .uri("/first-party/sign") - .header("content-type", "application/json") - .body(edgezero_core::body::Body::from("{}")) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!( - resp.status().as_u16(), - 404, - "POST /first-party/sign must be routed" - ); + assert_route_registered(&router, "POST", "/first-party/sign"); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_party_proxy_rebuild_is_routed() { let router = TrustedServerApp::routes(); - let req = request_builder() - .method("POST") - .uri("/first-party/proxy-rebuild") - .header("content-type", "application/json") - .body(edgezero_core::body::Body::from("{}")) - .expect("should build request"); - let resp = router.oneshot(req).await; - assert_ne!( - resp.status().as_u16(), - 404, - "/first-party/proxy-rebuild must be routed" - ); + assert_route_registered(&router, "POST", "/first-party/proxy-rebuild"); } // ---------------------------------------------------------------------------