use crate::runtime_support as common;
use camber::http::{self, Request, Response, Router};
use camber::runtime;
use std::num::NonZeroUsize;
use std::time::Duration;
const GENERATED_ROUTE_CASES: u64 = 48;
const ROUTE_MATRIX_BOUND: NonZeroUsize = NonZeroUsize::new(5).unwrap();
const CAPTURE_BOUND: NonZeroUsize = NonZeroUsize::new(4).unwrap();
const CAPTURES: [&str; 4] = ["alpha", "bravo", "charlie", "delta"];
fn route_matrix_router() -> Router {
let mut router = Router::new();
router.get("/matrix/*tail", |req: &Request| {
let tail = req.param("tail").unwrap_or("missing").to_owned();
async move { Response::text(200, &format!("wildcard:{tail}")) }
});
router.get("/matrix/:item/:leaf", |req: &Request| {
let item = req.param("item").unwrap_or("missing").to_owned();
let leaf = req.param("leaf").unwrap_or("missing").to_owned();
async move { Response::text(200, &format!("pair:{item}:{leaf}")) }
});
router.get("/matrix/:item/detail", |req: &Request| {
let item = req.param("item").unwrap_or("missing").to_owned();
async move { Response::text(200, &format!("param:{item}")) }
});
router.get("/matrix/fixed/detail", |_req: &Request| async {
Response::text(200, "static")
});
router
}
fn generated_route(case: &mut crate::deterministic::DeterministicCase) -> (String, String) {
let first = CAPTURES[case.bounded(CAPTURE_BOUND)];
let second = CAPTURES[case.bounded(CAPTURE_BOUND)];
match case.bounded(ROUTE_MATRIX_BOUND) {
0 => ("/matrix/fixed/detail".to_owned(), "static".to_owned()),
1 => (format!("/matrix/{first}/detail"), format!("param:{first}")),
2 => (
format!("/matrix/{first}/{second}"),
format!("pair:{first}:{second}"),
),
3 => (
format!("/matrix/fixed/{second}"),
format!("pair:fixed:{second}"),
),
_ => (
format!("/matrix/{first}/{second}/extra"),
format!("wildcard:{first}/{second}/extra"),
),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn generated_route_precedence_and_capture_matrix_is_stable() {
let server =
crate::http::spawn_server_ready(route_matrix_router(), Duration::from_secs(2)).unwrap();
let generator = crate::deterministic::DeterministicGenerator::stable();
for index in 0..GENERATED_ROUTE_CASES {
let mut case = generator.case(index);
let (path, expected) = generated_route(&mut case);
let response = http::get(&format!("http://{}{path}", server.local_addr()))
.await
.unwrap();
assert_eq!(response.status(), 200, "{case}: path={path}");
assert_eq!(response.body(), expected, "{case}: path={path}");
}
server.shutdown_bounded(Duration::from_secs(2)).unwrap();
}
#[camber::test]
async fn route_extracts_single_path_param() {
let mut router = Router::new();
router.get("/users/:id", |req: &Request| {
let id = req.param("id").unwrap_or("missing").to_owned();
async move { Response::text(200, &id) }
});
let addr = common::spawn_server(router);
let resp = http::get(&format!("http://{addr}/users/42")).await.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.body(), "42");
runtime::request_shutdown();
}
#[camber::test]
async fn route_extracts_multiple_path_params() {
let mut router = Router::new();
router.get("/users/:user_id/posts/:post_id", |req: &Request| {
let user_id = req.param("user_id").unwrap_or("?").to_owned();
let post_id = req.param("post_id").unwrap_or("?").to_owned();
async move { Response::text(200, &format!("{user_id}:{post_id}")) }
});
let addr = common::spawn_server(router);
let resp = http::get(&format!("http://{addr}/users/7/posts/99"))
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.body(), "7:99");
runtime::request_shutdown();
}
#[camber::test]
async fn dispatch_wildcard_route_captures_remainder() {
let mut router = Router::new();
router.get("/files/*path", |req: &Request| {
let path = req.param("path").unwrap_or("missing").to_owned();
async move { Response::text(200, &path) }
});
let addr = common::spawn_server(router);
let resp = http::get(&format!("http://{addr}/files/a/b/c"))
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.body(), "a/b/c");
runtime::request_shutdown();
}
#[camber::test]
async fn static_routes_still_match_exactly() {
let mut router = Router::new();
router.get("/users/me", |_req: &Request| async {
Response::text(200, "me-handler")
});
router.get("/users/:id", |req: &Request| {
let id = req.param("id").unwrap_or("missing").to_owned();
async move { Response::text(200, &id) }
});
let addr = common::spawn_server(router);
let resp_static = http::get(&format!("http://{addr}/users/me")).await.unwrap();
assert_eq!(resp_static.status(), 200);
assert_eq!(resp_static.body(), "me-handler");
let resp_param = http::get(&format!("http://{addr}/users/42")).await.unwrap();
assert_eq!(resp_param.status(), 200);
assert_eq!(resp_param.body(), "42");
runtime::request_shutdown();
}