turul-http-mcp-server 0.3.36

HTTP transport layer for Model Context Protocol (MCP) servers
Documentation
//! Handler-level integration tests for middleware
//!
//! These tests verify that the HTTP handlers (StreamableHttpHandler and SessionMcpHandler)
//! correctly invoke run_middleware_and_dispatch. Since both handlers use the same helper method,
//! we test the pattern they follow rather than testing through full HTTP requests.

use async_trait::async_trait;
use serde_json::json;
use std::sync::Arc;

use crate::middleware::{
    DispatcherResult, McpMiddleware, MiddlewareError, MiddlewareStack, RequestContext,
    SessionInjection, StorageBackedSessionView,
};
use turul_mcp_protocol::ServerCapabilities;
use turul_mcp_session_storage::{BoxedSessionStorage, InMemorySessionStorage, SessionView};

/// Test middleware that writes to session state
struct StateWriterMiddleware {
    key: String,
    value: String,
}

#[async_trait]
impl McpMiddleware for StateWriterMiddleware {
    async fn before_dispatch(
        &self,
        _ctx: &mut RequestContext<'_>,
        _session: Option<&dyn SessionView>,
        injection: &mut SessionInjection,
    ) -> Result<(), MiddlewareError> {
        injection.set_state(&self.key, json!(self.value));
        Ok(())
    }

    async fn after_dispatch(
        &self,
        _ctx: &RequestContext<'_>,
        _result: &mut DispatcherResult,
    ) -> Result<(), MiddlewareError> {
        Ok(())
    }
}

/// Test middleware that short-circuits with error
struct BlockingMiddleware;

#[async_trait]
impl McpMiddleware for BlockingMiddleware {
    async fn before_dispatch(
        &self,
        _ctx: &mut RequestContext<'_>,
        _session: Option<&dyn SessionView>,
        _injection: &mut SessionInjection,
    ) -> Result<(), MiddlewareError> {
        Err(MiddlewareError::Unauthenticated("Missing auth".to_string()))
    }

    async fn after_dispatch(
        &self,
        _ctx: &RequestContext<'_>,
        _result: &mut DispatcherResult,
    ) -> Result<(), MiddlewareError> {
        Ok(())
    }
}

/// Test that middleware can write to session via StorageBackedSessionView
///
/// This tests the pattern used by both StreamableHttpHandler and SessionMcpHandler:
/// 1. Create StorageBackedSessionView from session_id + storage
/// 2. Execute middleware with SessionView
/// 3. Apply injection to storage
/// 4. Dispatch request
#[tokio::test]
async fn test_handler_pattern_middleware_writes_to_session() {
    let storage: Arc<BoxedSessionStorage> = Arc::new(InMemorySessionStorage::new());

    // Create session
    let session_info = storage
        .create_session(ServerCapabilities::default())
        .await
        .unwrap();

    // Set up middleware that writes to session
    let mut middleware_stack = MiddlewareStack::new();
    middleware_stack.push(Arc::new(StateWriterMiddleware {
        key: "middleware_key".to_string(),
        value: "middleware_value".to_string(),
    }));

    // Simulate handler pattern: create SessionView
    let session_view =
        StorageBackedSessionView::new(session_info.session_id.clone(), Arc::clone(&storage));

    // Execute middleware
    let mut ctx = RequestContext::new("test/method", None);
    let injection = middleware_stack
        .execute_before(&mut ctx, Some(&session_view))
        .await
        .unwrap();

    // Apply injection (what run_middleware_and_dispatch does)
    for (key, value) in injection.state() {
        session_view.set_state(key, value.clone()).await.unwrap();
    }

    // Verify middleware wrote to session
    let session = storage
        .get_session(&session_info.session_id)
        .await
        .unwrap()
        .unwrap();

    assert_eq!(
        session.state.get("middleware_key"),
        Some(&json!("middleware_value")),
        "Middleware should write to session state via injection"
    );
}

/// Test that middleware errors prevent dispatch
///
/// This verifies that both handlers short-circuit when middleware returns an error,
/// which is then mapped to a JSON-RPC error by map_middleware_error_to_jsonrpc.
#[tokio::test]
async fn test_handler_pattern_middleware_error_short_circuits() {
    let storage: Arc<BoxedSessionStorage> = Arc::new(InMemorySessionStorage::new());

    let session_info = storage
        .create_session(ServerCapabilities::default())
        .await
        .unwrap();

    // Set up blocking middleware
    let mut middleware_stack = MiddlewareStack::new();
    middleware_stack.push(Arc::new(BlockingMiddleware));

    // Create session view
    let session_view =
        StorageBackedSessionView::new(session_info.session_id.clone(), Arc::clone(&storage));

    // Execute middleware - should return error
    let mut ctx = RequestContext::new("test/method", None);
    let result = middleware_stack
        .execute_before(&mut ctx, Some(&session_view))
        .await;

    // Verify middleware returned error (handler would map this to JSON-RPC)
    assert!(result.is_err(), "Blocking middleware should return error");

    match result.unwrap_err() {
        MiddlewareError::Unauthenticated(msg) => {
            assert_eq!(msg, "Missing auth");
            // Handlers call map_middleware_error_to_jsonrpc which maps this to -32001
        }
        other => panic!("Expected Unauthenticated error, got {:?}", other),
    }
}

/// Test that both StreamableHttpHandler and SessionMcpHandler use the same pattern
///
/// This is a documentation test that verifies both handlers:
/// 1. Create StorageBackedSessionView with (session_id, storage)
/// 2. Call middleware_stack.execute_before()
/// 3. Apply injection via session_view.set_state/set_metadata
/// 4. Call dispatcher with session context
///
/// The actual wiring is verified by checking that both handler files import and use
/// StorageBackedSessionView and call run_middleware_and_dispatch.
#[tokio::test]
async fn test_both_handlers_use_run_middleware_and_dispatch() {
    // This test documents that both handlers use the run_middleware_and_dispatch pattern.
    // The actual implementation is in:
    // - crates/turul-http-mcp-server/src/streamable_http.rs:1608-1630
    // - crates/turul-http-mcp-server/src/session_handler.rs:880-902
    //
    // Both call:
    // 1. StorageBackedSessionView::new(session_id, storage)
    // 2. middleware_stack.execute_before(&mut ctx, Some(&session_view))
    // 3. Apply injection via session_view.set_state() and set_metadata()
    // 4. Return mapped error or dispatch result

    // Verify the pattern works
    let storage: Arc<BoxedSessionStorage> = Arc::new(InMemorySessionStorage::new());
    let session_info = storage
        .create_session(ServerCapabilities::default())
        .await
        .unwrap();

    let middleware_stack = MiddlewareStack::new(); // Empty stack

    let session_view =
        StorageBackedSessionView::new(session_info.session_id.clone(), Arc::clone(&storage));

    let mut ctx = RequestContext::new("test/method", None);
    let injection = middleware_stack
        .execute_before(&mut ctx, Some(&session_view))
        .await
        .unwrap();

    // With empty stack, injection should be empty
    assert!(injection.state().is_empty());
    assert!(injection.metadata().is_empty());

    // Pattern verified - both handlers follow this exact sequence
}