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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 28 additions & 22 deletions crates/integration-tests/tests/parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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:?}"
);
}

Expand All @@ -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:?}"
);
}

Expand All @@ -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})"
);
}
}
Expand Down
223 changes: 43 additions & 180 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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");
}
Loading
Loading