corteq 0.1.0

Enterprise-grade multi-tenant SaaS framework for Rust with security-first design
Documentation
//! Integration tests for tenant context middleware
//!
//! These tests verify the end-to-end flow of JWT extraction, validation,
//! tenant loading, caching, and context injection.

use actix_web::{test, web, App, HttpResponse};
use chrono::Utc;
use corteq::{CorteqApp, TenantExtractor};
use uuid::Uuid;

/// Test handler that requires tenant context
async fn protected_handler(tenant: TenantExtractor) -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({
        "tenant_id": tenant.0.tenant_id.to_string(),
        "tenant_slug": tenant.0.tenant_slug,
    }))
}

/// Get database URL from environment or use default
fn get_database_url() -> String {
    std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://postgres:postgres@172.24.0.3:5432/corteq_test".to_string())
}

/// Create a test CorteqApp instance
async fn create_test_app() -> CorteqApp {
    CorteqApp::builder()
        .with_database(&get_database_url())
        .with_jwt_secret("test-secret-key")
        .build()
        .await
        .expect("Failed to create test app")
}

/// Create a valid JWT token for testing
fn create_test_token(app: &CorteqApp, tenant_id: Uuid, user_id: Uuid) -> String {
    let claims = corteq::auth::Claims {
        sub: user_id,
        tenant_id,
        roles: vec!["user".to_string()],
        exp: (Utc::now() + chrono::Duration::hours(1)).timestamp(),
        iat: Utc::now().timestamp(),
    };

    app.jwt_service
        .encode(&claims)
        .expect("Failed to encode token")
}

#[actix_web::test]
async fn test_middleware_with_authorization_header() {
    let app_config = create_test_app().await;
    let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let user_id = Uuid::new_v4();
    let token = create_test_token(&app_config, tenant_id, user_id);

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get()
        .uri("/protected")
        .insert_header(("Authorization", format!("Bearer {token}")))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), 200);

    let body: serde_json::Value = test::read_body_json(resp).await;
    assert_eq!(body["tenant_id"], tenant_id.to_string());
    assert_eq!(body["tenant_slug"], "acme-corp");
}

#[actix_web::test]
async fn test_middleware_with_cookie() {
    let app_config = create_test_app().await;
    let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let user_id = Uuid::new_v4();
    let token = create_test_token(&app_config, tenant_id, user_id);

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get()
        .uri("/protected")
        .insert_header(("Cookie", format!("token={token}")))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), 200);

    let body: serde_json::Value = test::read_body_json(resp).await;
    assert_eq!(body["tenant_id"], tenant_id.to_string());
}

#[actix_web::test]
async fn test_middleware_cookie_priority_over_header() {
    let app_config = create_test_app().await;
    let tenant_id_1 = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let tenant_id_2 = Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap();
    let user_id = Uuid::new_v4();

    // Create tokens for different tenants
    let token_cookie = create_test_token(&app_config, tenant_id_1, user_id);
    let token_header = create_test_token(&app_config, tenant_id_2, user_id);

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get()
        .uri("/protected")
        .insert_header(("Cookie", format!("token={token_cookie}")))
        .insert_header(("Authorization", format!("Bearer {token_header}")))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), 200);

    let body: serde_json::Value = test::read_body_json(resp).await;
    // Should use tenant_id_1 from cookie, not tenant_id_2 from header
    assert_eq!(body["tenant_id"], tenant_id_1.to_string());
    assert_eq!(body["tenant_slug"], "acme-corp");
}

#[actix_web::test]
async fn test_middleware_rejects_missing_token() {
    let app_config = create_test_app().await;

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get().uri("/protected").to_request();

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

    // Middleware should return an error for missing token
    assert!(result.is_err());
    let err_msg = format!("{}", result.unwrap_err());
    assert!(err_msg.contains("Authentication required"));
}

#[actix_web::test]
async fn test_middleware_rejects_invalid_token() {
    let app_config = create_test_app().await;

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get()
        .uri("/protected")
        .insert_header(("Authorization", "Bearer invalid-token-xyz"))
        .to_request();

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

    // Middleware should return an error for invalid token
    assert!(result.is_err());
    let err_msg = format!("{}", result.unwrap_err());
    assert!(err_msg.contains("Invalid or expired token"));
}

#[actix_web::test]
async fn test_middleware_rejects_expired_token() {
    let app_config = create_test_app().await;
    let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let user_id = Uuid::new_v4();

    // Create an expired token (exp in the past)
    let claims = corteq::auth::Claims {
        sub: user_id,
        tenant_id,
        roles: vec!["user".to_string()],
        exp: (Utc::now() - chrono::Duration::hours(1)).timestamp(),
        iat: (Utc::now() - chrono::Duration::hours(2)).timestamp(),
    };

    let expired_token = app_config
        .jwt_service
        .encode(&claims)
        .expect("Failed to encode token");

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get()
        .uri("/protected")
        .insert_header(("Authorization", format!("Bearer {expired_token}")))
        .to_request();

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

    // Middleware should return an error for expired token
    assert!(result.is_err());
    let err_msg = format!("{}", result.unwrap_err());
    assert!(err_msg.contains("Invalid or expired token"));
}

#[actix_web::test]
async fn test_middleware_rejects_nonexistent_tenant() {
    let app_config = create_test_app().await;
    let nonexistent_tenant_id = Uuid::new_v4(); // Random UUID that doesn't exist in DB
    let user_id = Uuid::new_v4();
    let token = create_test_token(&app_config, nonexistent_tenant_id, user_id);

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get()
        .uri("/protected")
        .insert_header(("Authorization", format!("Bearer {token}")))
        .to_request();

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

    // Middleware should return an error for nonexistent tenant
    assert!(result.is_err());
    let err_msg = format!("{}", result.unwrap_err());
    assert!(err_msg.contains("Tenant not found"));
}

#[actix_web::test]
async fn test_middleware_caches_tenant_context() {
    let app_config = create_test_app().await;
    let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let user_id = Uuid::new_v4();
    let token = create_test_token(&app_config, tenant_id, user_id);

    // First request - should load from database
    assert!(app_config.tenant_cache.get(&tenant_id).await.is_none());

    let app = test::init_service(
        App::new()
            .app_data(web::Data::new(app_config.clone()))
            .wrap(corteq::middleware::TenantContextMiddleware)
            .route("/protected", web::get().to(protected_handler)),
    )
    .await;

    let req = test::TestRequest::get()
        .uri("/protected")
        .insert_header(("Authorization", format!("Bearer {token}")))
        .to_request();

    let resp = test::call_service(&app, req).await;
    assert_eq!(resp.status(), 200);

    // Second check - should now be in cache
    let cached = app_config.tenant_cache.get(&tenant_id).await;
    assert!(cached.is_some());
    assert_eq!(cached.unwrap().tenant_id, tenant_id);
}