autumn-web 0.5.0

An opinionated, convention-over-configuration web framework for Rust
Documentation
//! Integration tests for typed path helpers (issue #499).
//!
//! # RED phase
//! These tests assert the path helpers generated by route macros.
//! They compile and run against `__autumn_path_*` companion functions
//! that the route macros must emit alongside `__autumn_route_info_*`.

#![allow(dead_code)]

use autumn_web::paths::PathExt;
use autumn_web::{delete, get, patch, post, put};

// ── Handlers: no path params ─────────────────────────────────────────

#[get("/hello")]
async fn hello() -> &'static str {
    "hello"
}

#[get("/about")]
async fn about() -> &'static str {
    "about"
}

#[get("/")]
async fn root() -> &'static str {
    "root"
}

// ── Handlers: one path param ─────────────────────────────────────────

#[get("/posts/{id}")]
async fn show_post(autumn_web::Path(id): autumn_web::Path<i64>) -> String {
    format!("post {id}")
}

#[delete("/posts/{id}")]
async fn delete_post(autumn_web::Path(_id): autumn_web::Path<i64>) -> &'static str {
    "deleted"
}

#[put("/posts/{id}")]
async fn update_post(autumn_web::Path(_id): autumn_web::Path<i64>) -> &'static str {
    "updated"
}

#[post("/posts")]
async fn create_post() -> &'static str {
    "created"
}

// ── Handlers: two path params ────────────────────────────────────────

#[get("/posts/{year}/{slug}")]
async fn show_post_by_year_slug(
    autumn_web::Path((year, slug)): autumn_web::Path<(i32, String)>,
) -> String {
    format!("{year}/{slug}")
}

// ── Handlers: name = override ────────────────────────────────────────

#[get("/articles/{id}", name = "article")]
async fn show_article(autumn_web::Path(id): autumn_web::Path<i64>) -> String {
    format!("article {id}")
}

// ── Handlers: trailing slash ─────────────────────────────────────────

#[get("/with-slash/")]
async fn with_slash() -> &'static str {
    "slash"
}

// ── Handlers: patch method ───────────────────────────────────────────

#[patch("/posts/{id}")]
async fn patch_post(autumn_web::Path(_id): autumn_web::Path<i64>) -> &'static str {
    "patched"
}

// ════════════════════════════════════════════════════════════════════
// Tests: no-parameter helpers
// ════════════════════════════════════════════════════════════════════

#[test]
fn path_helper_no_params_hello() {
    assert_eq!(__autumn_path_hello(), "/hello");
}

#[test]
fn path_helper_no_params_about() {
    assert_eq!(__autumn_path_about(), "/about");
}

#[test]
fn path_helper_no_params_root() {
    assert_eq!(__autumn_path_root(), "/");
}

// ════════════════════════════════════════════════════════════════════
// Tests: single-param helpers
// ════════════════════════════════════════════════════════════════════

#[test]
fn path_helper_one_param_get() {
    assert_eq!(__autumn_path_show_post(42), "/posts/42");
}

#[test]
fn path_helper_one_param_delete() {
    assert_eq!(__autumn_path_delete_post(7), "/posts/7");
}

#[test]
fn path_helper_one_param_put() {
    assert_eq!(__autumn_path_update_post(99), "/posts/99");
}

#[test]
fn path_helper_no_param_post() {
    assert_eq!(__autumn_path_create_post(), "/posts");
}

// ════════════════════════════════════════════════════════════════════
// Tests: multi-param helpers
// ════════════════════════════════════════════════════════════════════

#[test]
fn path_helper_two_params() {
    assert_eq!(
        __autumn_path_show_post_by_year_slug(2024, "my-post"),
        "/posts/2024/my-post"
    );
}

// ════════════════════════════════════════════════════════════════════
// Tests: name = override
// ════════════════════════════════════════════════════════════════════

#[test]
fn path_helper_custom_name_override() {
    // `name = "article"` so helper is `__autumn_path_article`, not `__autumn_path_show_article`
    assert_eq!(__autumn_path_article(5), "/articles/5");
}

// ════════════════════════════════════════════════════════════════════
// Tests: trailing slash preserved
// ════════════════════════════════════════════════════════════════════

#[test]
fn path_helper_preserves_trailing_slash() {
    assert_eq!(__autumn_path_with_slash(), "/with-slash/");
}

// ════════════════════════════════════════════════════════════════════
// Tests: patch method emits helper
// ════════════════════════════════════════════════════════════════════

#[test]
fn path_helper_patch_method() {
    assert_eq!(__autumn_path_patch_post(3), "/posts/3");
}

// ════════════════════════════════════════════════════════════════════
// Tests: helper accepts any Display type
// ════════════════════════════════════════════════════════════════════

#[test]
fn path_helper_accepts_string_id() {
    assert_eq!(__autumn_path_show_post("hello-slug"), "/posts/hello-slug");
}

#[test]
fn path_helper_percent_encodes_reserved_segment_chars() {
    assert_eq!(__autumn_path_show_post("a/b"), "/posts/a%2Fb");
}

#[test]
fn path_helper_percent_encodes_spaces_and_non_ascii() {
    assert_eq!(
        __autumn_path_show_post_by_year_slug(2024, "hello world/é"),
        "/posts/2024/hello%20world%2F%C3%A9"
    );
}

#[test]
fn path_helper_accepts_u32_id() {
    let id: u32 = 10;
    assert_eq!(__autumn_path_show_post(id), "/posts/10");
}

// ════════════════════════════════════════════════════════════════════
// Tests: PathExt::with_query
// ════════════════════════════════════════════════════════════════════

#[test]
fn with_query_appends_first_param() {
    let path = __autumn_path_hello().with_query("page", 1);
    assert_eq!(path, "/hello?page=1");
}

#[test]
fn with_query_appends_second_param() {
    let path = __autumn_path_hello()
        .with_query("page", 1)
        .with_query("size", 20);
    assert_eq!(path, "/hello?page=1&size=20");
}

#[test]
fn with_query_percent_encodes_spaces() {
    let path = __autumn_path_hello().with_query("q", "hello world");
    assert_eq!(path, "/hello?q=hello%20world");
}

#[test]
fn with_query_percent_encodes_special_chars() {
    let path = __autumn_path_hello().with_query("filter", "a=b&c");
    assert_eq!(path, "/hello?filter=a%3Db%26c");
}

// ════════════════════════════════════════════════════════════════════
// Tests: paths![] module macro
// ════════════════════════════════════════════════════════════════════

mod my_app {
    use autumn_web::{get, post};

    #[get("/articles/{id}")]
    pub async fn show(autumn_web::Path(id): autumn_web::Path<i64>) -> String {
        format!("article {id}")
    }

    #[post("/articles")]
    pub async fn create() -> &'static str {
        "created"
    }

    autumn_web::paths![show, create];
}

#[test]
fn paths_module_exposes_no_param_helper() {
    assert_eq!(my_app::paths::create(), "/articles");
}

#[test]
fn paths_module_exposes_param_helper() {
    assert_eq!(my_app::paths::show(42), "/articles/42");
}

// ════════════════════════════════════════════════════════════════════
// Tests: paths![] works with private handlers (P1 fix)
// ════════════════════════════════════════════════════════════════════

mod private_handlers {
    use autumn_web::get;

    // No `pub` — private handler.
    #[get("/private/{id}")]
    async fn private_show(autumn_web::Path(_id): autumn_web::Path<i64>) -> &'static str {
        "private"
    }

    autumn_web::paths![private_show];
}

#[test]
fn paths_module_works_with_private_handler() {
    assert_eq!(private_handlers::paths::private_show(7), "/private/7");
}

// ════════════════════════════════════════════════════════════════════
// Tests: paths![] works with name = override via fn name alias (P2 fix)
// ════════════════════════════════════════════════════════════════════

mod named_override {
    use autumn_web::get;

    #[get("/items/{id}", name = "item")]
    pub async fn show_item(autumn_web::Path(_id): autumn_web::Path<i64>) -> &'static str {
        "item"
    }

    // Both `paths![show_item]` (fn name) and `paths![item]` (override) work.
    autumn_web::paths![show_item, item];
}

#[test]
fn paths_module_fn_name_alias_resolves_with_override() {
    // `show_item` resolves via the alias emitted alongside `__autumn_path_item`.
    assert_eq!(named_override::paths::show_item(3), "/items/3");
}

#[test]
fn paths_module_override_name_also_resolves() {
    assert_eq!(named_override::paths::item(3), "/items/3");
}

// ════════════════════════════════════════════════════════════════════
// Tests: Redirect re-export
// ════════════════════════════════════════════════════════════════════

#[test]
fn redirect_type_is_accessible() {
    // Just checking it compiles; axum::response::Redirect is re-exported
    use autumn_web::Redirect;
    let _r: Redirect = Redirect::to("/somewhere");
}