road-runner-common 0.12.1

Shared Rust utilities for exchange ecosystem backend services.
Documentation
#![cfg(feature = "web")]

use actix_web::{test, web, App, HttpMessage, HttpRequest, HttpResponse};
use futures_util::future::{ready, FutureExt, LocalBoxFuture};
use road_runner_common::auth::{PrincipalKind, UserContext};
use road_runner_common::error::AppError;
use road_runner_common::middleware::UserContextMiddleware;

fn ctx() -> UserContext {
    UserContext {
        subject: "sub-1".to_string(),
        principal: PrincipalKind::User,
        email: None,
        username: None,
        roles: vec!["admin".to_string()],
        scopes: vec![],
        capabilities: vec![],
        api_key_id: None,
        acr: None,
    }
}

async fn subject(req: HttpRequest) -> HttpResponse {
    let subject = req.extensions().get::<UserContext>().map(|ctx| ctx.subject.clone());
    match subject {
        Some(subject) => HttpResponse::Ok().body(subject),
        None => HttpResponse::NoContent().finish(),
    }
}

#[actix_web::test]
async fn user_context_middleware_inserts_resolved_context() {
    // Arrange
    let resolver = |req| ready((req, Ok(Some(ctx())))).boxed_local();
    let app = test::init_service(
        App::new().wrap(UserContextMiddleware::new(resolver)).route("/", web::get().to(subject)),
    )
    .await;
    let req = test::TestRequest::get().uri("/").to_request();

    // Act
    let response = test::call_service(&app, req).await;
    let body = test::read_body(response).await;

    // Assert
    assert_eq!(body, "sub-1");
}

#[actix_web::test]
async fn user_context_middleware_allows_anonymous_when_not_required() {
    // Arrange
    let resolver = |req| ready((req, Ok(None))).boxed_local();
    let app = test::init_service(
        App::new().wrap(UserContextMiddleware::new(resolver)).route("/", web::get().to(subject)),
    )
    .await;
    let req = test::TestRequest::get().uri("/").to_request();

    // Act
    let response = test::call_service(&app, req).await;

    // Assert
    assert_eq!(response.status(), actix_web::http::StatusCode::NO_CONTENT);
}

#[actix_web::test]
async fn user_context_middleware_rejects_missing_context_when_required() {
    // Arrange
    let resolver = |req| ready((req, Ok(None))).boxed_local();
    let app = test::init_service(
        App::new().wrap(UserContextMiddleware::new(resolver).required(true)).route("/", web::get().to(subject)),
    )
    .await;
    let req = test::TestRequest::get().uri("/").to_request();

    // Act
    let result = test::try_call_service(&app, req).await;

    // Assert
    assert!(result.is_err());
}

#[actix_web::test]
async fn user_context_middleware_returns_resolver_error() {
    // Arrange
    let resolver = |req| -> LocalBoxFuture<'static, _> {
        ready((
            req,
            Err(AppError::Forbidden { message: "blocked".to_string() }),
        ))
        .boxed_local()
    };
    let app = test::init_service(
        App::new().wrap(UserContextMiddleware::new(resolver)).route("/", web::get().to(subject)),
    )
    .await;
    let req = test::TestRequest::get().uri("/").to_request();

    // Act
    let result = test::try_call_service(&app, req).await;

    // Assert
    assert!(result.is_err());
}