nestforge 1.9.0

NestJS-inspired modular backend framework for Rust
Documentation
use nestforge::{
    controller, routes, AuthIdentity, Container, ExceptionFilter, ModuleDefinition,
    NestForgeFactory, RequestContext,
};
use tower::ServiceExt;

#[controller("/admin")]
struct AdminController;

#[derive(Default)]
struct RewriteForbiddenFilter;

#[routes]
#[authenticated]
#[roles("admin")]
impl AdminController {
    #[nestforge::get("/dashboard")]
    async fn dashboard() -> nestforge::ApiResult<String> {
        Ok(axum::Json("ok".to_string()))
    }
}

impl ExceptionFilter for RewriteForbiddenFilter {
    fn catch(
        &self,
        exception: nestforge::HttpException,
        _ctx: &RequestContext,
    ) -> nestforge::HttpException {
        if exception.status == axum::http::StatusCode::FORBIDDEN {
            nestforge::HttpException::forbidden("filtered forbidden")
                .with_optional_request_id(exception.request_id)
        } else {
            exception
        }
    }
}

#[controller("/filtered")]
struct FilteredController;

#[routes]
#[use_exception_filter(RewriteForbiddenFilter)]
impl FilteredController {
    #[nestforge::get("/deny")]
    async fn deny() -> nestforge::ApiResult<String> {
        Err(nestforge::HttpException::forbidden("original forbidden"))
    }
}

struct AppModule;

impl ModuleDefinition for AppModule {
    fn register(_container: &Container) -> anyhow::Result<()> {
        Ok(())
    }

    fn controllers() -> Vec<axum::Router<Container>> {
        vec![
            <AdminController as nestforge::ControllerDefinition>::router(),
            <FilteredController as nestforge::ControllerDefinition>::router(),
        ]
    }
}

#[tokio::test]
async fn controller_level_authenticated_metadata_enforces_runtime_guards() {
    let app = NestForgeFactory::<AppModule>::create()
        .expect("factory should build")
        .into_router();

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .uri("/admin/dashboard")
                .body(axum::body::Body::empty())
                .expect("request should build"),
        )
        .await
        .expect("request should succeed");

    assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn controller_level_roles_metadata_enforces_role_checks() {
    let app = NestForgeFactory::<AppModule>::create()
        .expect("factory should build")
        .with_auth_resolver(|token, _container| async move {
            Ok(token.map(|_| AuthIdentity::new("demo-user").with_roles(["viewer"])))
        })
        .into_router();

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .uri("/admin/dashboard")
                .header(axum::http::header::AUTHORIZATION, "Bearer demo-token")
                .body(axum::body::Body::empty())
                .expect("request should build"),
        )
        .await
        .expect("request should succeed");

    assert_eq!(response.status(), axum::http::StatusCode::FORBIDDEN);
}

#[tokio::test]
async fn controller_level_roles_metadata_allows_matching_roles() {
    let app = NestForgeFactory::<AppModule>::create()
        .expect("factory should build")
        .with_auth_resolver(|token, _container| async move {
            Ok(token.map(|_| AuthIdentity::new("demo-user").with_roles(["admin"])))
        })
        .into_router();

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .uri("/admin/dashboard")
                .header(axum::http::header::AUTHORIZATION, "Bearer demo-token")
                .body(axum::body::Body::empty())
                .expect("request should build"),
        )
        .await
        .expect("request should succeed");

    assert_eq!(response.status(), axum::http::StatusCode::OK);
}

#[tokio::test]
async fn controller_level_exception_filters_rewrite_route_failures() {
    let app = NestForgeFactory::<AppModule>::create()
        .expect("factory should build")
        .into_router();

    let response = app
        .oneshot(
            axum::http::Request::builder()
                .uri("/filtered/deny")
                .body(axum::body::Body::empty())
                .expect("request should build"),
        )
        .await
        .expect("request should succeed");

    assert_eq!(response.status(), axum::http::StatusCode::FORBIDDEN);
    let body = axum::body::to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("response body should read");
    let payload: serde_json::Value =
        serde_json::from_slice(&body).expect("response body should be json");
    assert_eq!(payload["message"], "filtered forbidden");
}