Skip to content

Commit bee7a39

Browse files
committed
Merge feature/edgezero-pr18-phase5-verification into spin adapter branch
Resolve conflict in parity.rs: preserve axum_www_auth binding from base branch (required by assert_eq! below) and move spin WWW-Authenticate presence check to end of function, consistent with deactivate test pattern.
2 parents 4c4f1b6 + 70def67 commit bee7a39

3 files changed

Lines changed: 85 additions & 169 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ jobs:
157157
run: cargo test --manifest-path crates/integration-tests/Cargo.toml --test parity
158158

159159
- name: Clippy (parity test crate)
160-
run: cargo clippy --manifest-path crates/integration-tests/Cargo.toml -- -D warnings
160+
run: cargo clippy --manifest-path crates/integration-tests/Cargo.toml --all-targets -- -D warnings
161161

162162
test-typescript:
163163
name: vitest

crates/integration-tests/tests/parity.rs

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -289,22 +289,27 @@ async fn admin_rotate_unauthenticated_parity() {
289289
"Cloudflare and Spin must return the same status for unauthenticated admin route"
290290
);
291291

292-
assert!(
293-
axum_headers.contains_key("www-authenticate"),
294-
"Axum 401 must include WWW-Authenticate header"
295-
);
296-
assert!(
297-
spin_headers.contains_key("www-authenticate"),
298-
"Spin 401 must include WWW-Authenticate header"
299-
);
292+
let axum_www_auth = axum_headers
293+
.get("www-authenticate")
294+
.expect("Axum 401 must include WWW-Authenticate")
295+
.to_str()
296+
.expect("should be valid UTF-8");
300297
let cf_www_auth = cf_headers
301298
.get("www-authenticate")
302-
.expect("should have www-authenticate header on 401")
299+
.expect("Cloudflare 401 must include WWW-Authenticate")
303300
.to_str()
304301
.expect("should be valid UTF-8");
302+
assert_eq!(
303+
axum_www_auth, cf_www_auth,
304+
"WWW-Authenticate header value must match across adapters for /admin/keys/rotate"
305+
);
305306
assert!(
306-
cf_www_auth.starts_with("Basic realm="),
307-
"Cloudflare 401 WWW-Authenticate must be Basic scheme: {cf_www_auth:?}"
307+
axum_www_auth.starts_with("Basic"),
308+
"WWW-Authenticate must use Basic scheme: {axum_www_auth:?}"
309+
);
310+
assert!(
311+
spin_headers.contains_key("www-authenticate"),
312+
"Spin 401 must include WWW-Authenticate header"
308313
);
309314
}
310315

@@ -336,13 +341,23 @@ async fn admin_deactivate_unauthenticated_parity() {
336341
"Cloudflare and Spin must return the same status for unauthenticated admin/keys/deactivate"
337342
);
338343

339-
assert!(
340-
axum_headers.contains_key("www-authenticate"),
341-
"Axum 401 on admin/keys/deactivate must include WWW-Authenticate header"
344+
let axum_www_auth = axum_headers
345+
.get("www-authenticate")
346+
.expect("Axum 401 on admin/keys/deactivate must include WWW-Authenticate")
347+
.to_str()
348+
.expect("should be valid UTF-8");
349+
let cf_www_auth = cf_headers
350+
.get("www-authenticate")
351+
.expect("Cloudflare 401 on admin/keys/deactivate must include WWW-Authenticate")
352+
.to_str()
353+
.expect("should be valid UTF-8");
354+
assert_eq!(
355+
axum_www_auth, cf_www_auth,
356+
"WWW-Authenticate header value must match across adapters for /admin/keys/deactivate"
342357
);
343358
assert!(
344-
cf_headers.contains_key("www-authenticate"),
345-
"Cloudflare 401 on admin/keys/deactivate must include WWW-Authenticate header"
359+
axum_www_auth.starts_with("Basic"),
360+
"WWW-Authenticate must use Basic scheme: {axum_www_auth:?}"
346361
);
347362
assert!(
348363
spin_headers.contains_key("www-authenticate"),
@@ -375,13 +390,20 @@ async fn geo_header_parity_on_all_responses() {
375390
spin_post_headers(path, body).await
376391
};
377392

378-
assert!(
379-
axum_headers.contains_key("x-geo-info-available"),
380-
"Axum: {method} {path} (status={axum_status}) must have X-Geo-Info-Available"
381-
);
382-
assert!(
383-
cf_headers.contains_key("x-geo-info-available"),
384-
"Cloudflare: {method} {path} (status={cf_status}) must have X-Geo-Info-Available"
393+
let axum_geo = axum_headers
394+
.get("x-geo-info-available")
395+
.unwrap_or_else(|| panic!("Axum: {method} {path} (status={axum_status}) must have X-Geo-Info-Available"))
396+
.to_str()
397+
.expect("should be valid UTF-8");
398+
let cf_geo = cf_headers
399+
.get("x-geo-info-available")
400+
.unwrap_or_else(|| panic!("Cloudflare: {method} {path} (status={cf_status}) must have X-Geo-Info-Available"))
401+
.to_str()
402+
.expect("should be valid UTF-8");
403+
assert_eq!(
404+
axum_geo, cf_geo,
405+
"{method} {path}: X-Geo-Info-Available value must match across adapters \
406+
(axum={axum_geo:?} cf={cf_geo:?})"
385407
);
386408
assert!(
387409
spin_headers.contains_key("x-geo-info-available"),

crates/trusted-server-adapter-cloudflare/tests/routes.rs

Lines changed: 39 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ use edgezero_core::app::Hooks as _;
88
use edgezero_core::http::request_builder;
99
use trusted_server_adapter_cloudflare::app::TrustedServerApp;
1010

11+
/// Return the set of (METHOD, path) pairs explicitly registered on the router.
12+
fn registered_routes() -> Vec<(String, String)> {
13+
TrustedServerApp::routes()
14+
.routes()
15+
.into_iter()
16+
.map(|r| (r.method().to_string(), r.path().to_string()))
17+
.collect()
18+
}
19+
20+
fn assert_route_registered(method: &str, path: &str) {
21+
let routes = registered_routes();
22+
assert!(
23+
routes.iter().any(|(m, p)| m == method && p == path),
24+
"{method} {path} must be explicitly registered; registered routes: {routes:?}"
25+
);
26+
}
27+
1128
#[test]
1229
fn routes_build_without_panic() {
1330
// build_state() may fail (no real settings in CI) — startup_error_router
@@ -92,152 +109,29 @@ async fn tsjs_route_is_routed_not_5xx() {
92109
assert!(status < 500, "tsjs route must not 5xx: got {status}");
93110
}
94111

95-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
96-
async fn verify_signature_is_routed() {
97-
let router = TrustedServerApp::routes();
98-
let req = request_builder()
99-
.method("POST")
100-
.uri("/verify-signature")
101-
.header("content-type", "application/json")
102-
.body(edgezero_core::body::Body::from("{}"))
103-
.expect("should build request");
104-
let resp = router.oneshot(req).await;
105-
assert_ne!(
106-
resp.status().as_u16(),
107-
404,
108-
"/verify-signature must be routed"
109-
);
110-
}
111-
112-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
113-
async fn admin_rotate_key_is_routed() {
114-
let router = TrustedServerApp::routes();
115-
let req = request_builder()
116-
.method("POST")
117-
.uri("/admin/keys/rotate")
118-
.header("content-type", "application/json")
119-
.body(edgezero_core::body::Body::from("{}"))
120-
.expect("should build request");
121-
let resp = router.oneshot(req).await;
122-
assert_ne!(
123-
resp.status().as_u16(),
124-
404,
125-
"/admin/keys/rotate must be routed"
126-
);
127-
}
128-
129-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
130-
async fn admin_deactivate_key_is_routed() {
131-
let router = TrustedServerApp::routes();
132-
let req = request_builder()
133-
.method("POST")
134-
.uri("/admin/keys/deactivate")
135-
.header("content-type", "application/json")
136-
.body(edgezero_core::body::Body::from("{}"))
137-
.expect("should build request");
138-
let resp = router.oneshot(req).await;
139-
assert_ne!(
140-
resp.status().as_u16(),
141-
404,
142-
"/admin/keys/deactivate must be routed"
143-
);
144-
}
145-
146-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
147-
async fn auction_is_routed() {
148-
let router = TrustedServerApp::routes();
149-
let req = request_builder()
150-
.method("POST")
151-
.uri("/auction")
152-
.header("content-type", "application/json")
153-
.body(edgezero_core::body::Body::from(r#"{"adUnits":[]}"#))
154-
.expect("should build request");
155-
let resp = router.oneshot(req).await;
156-
assert_ne!(resp.status().as_u16(), 404, "/auction must be routed");
157-
}
158-
159-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
160-
async fn first_party_proxy_is_routed() {
161-
let router = TrustedServerApp::routes();
162-
let req = request_builder()
163-
.method("GET")
164-
.uri("/first-party/proxy")
165-
.body(edgezero_core::body::Body::empty())
166-
.expect("should build request");
167-
let resp = router.oneshot(req).await;
168-
// Handlers require valid outbound proxy settings; they may return 4xx/5xx in CI.
169-
// The assertion is routing only: the path must not fall through to the 404 not-found handler.
170-
assert_ne!(
171-
resp.status().as_u16(),
172-
404,
173-
"/first-party/proxy must be routed"
174-
);
175-
}
176-
177-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
178-
async fn first_party_click_is_routed() {
179-
let router = TrustedServerApp::routes();
180-
let req = request_builder()
181-
.method("GET")
182-
.uri("/first-party/click")
183-
.body(edgezero_core::body::Body::empty())
184-
.expect("should build request");
185-
let resp = router.oneshot(req).await;
186-
assert_ne!(
187-
resp.status().as_u16(),
188-
404,
189-
"/first-party/click must be routed"
190-
);
191-
}
192-
193-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
194-
async fn first_party_sign_get_is_routed() {
195-
let router = TrustedServerApp::routes();
196-
let req = request_builder()
197-
.method("GET")
198-
.uri("/first-party/sign")
199-
.body(edgezero_core::body::Body::empty())
200-
.expect("should build request");
201-
let resp = router.oneshot(req).await;
202-
assert_ne!(
203-
resp.status().as_u16(),
204-
404,
205-
"GET /first-party/sign must be routed"
206-
);
207-
}
208-
209-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
210-
async fn first_party_sign_post_is_routed() {
211-
let router = TrustedServerApp::routes();
212-
let req = request_builder()
213-
.method("POST")
214-
.uri("/first-party/sign")
215-
.header("content-type", "application/json")
216-
.body(edgezero_core::body::Body::from("{}"))
217-
.expect("should build request");
218-
let resp = router.oneshot(req).await;
219-
assert_ne!(
220-
resp.status().as_u16(),
221-
404,
222-
"POST /first-party/sign must be routed"
223-
);
224-
}
112+
/// Verify that every expected explicit route is registered in the route table.
113+
///
114+
/// Uses [`RouterService::routes()`] for introspection rather than checking
115+
/// response status codes — wildcards (`/{*rest}`) can return non-404 even when
116+
/// an explicit registration is missing, making status-based checks false positives.
117+
#[test]
118+
fn all_explicit_routes_are_registered() {
119+
let expected: &[(&str, &str)] = &[
120+
("GET", "/.well-known/trusted-server.json"),
121+
("POST", "/verify-signature"),
122+
("POST", "/admin/keys/rotate"),
123+
("POST", "/admin/keys/deactivate"),
124+
("POST", "/auction"),
125+
("GET", "/first-party/proxy"),
126+
("GET", "/first-party/click"),
127+
("GET", "/first-party/sign"),
128+
("POST", "/first-party/sign"),
129+
("POST", "/first-party/proxy-rebuild"),
130+
];
225131

226-
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
227-
async fn first_party_proxy_rebuild_is_routed() {
228-
let router = TrustedServerApp::routes();
229-
let req = request_builder()
230-
.method("POST")
231-
.uri("/first-party/proxy-rebuild")
232-
.header("content-type", "application/json")
233-
.body(edgezero_core::body::Body::from("{}"))
234-
.expect("should build request");
235-
let resp = router.oneshot(req).await;
236-
assert_ne!(
237-
resp.status().as_u16(),
238-
404,
239-
"/first-party/proxy-rebuild must be routed"
240-
);
132+
for (method, path) in expected {
133+
assert_route_registered(method, path);
134+
}
241135
}
242136

243137
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)