tandem-server 0.6.2

HTTP server for Tandem engine APIs
use super::*;

#[tokio::test]
async fn mcp_inventory_redacts_and_filters_connections_by_actor() {
    let state = test_state().await;
    let (endpoint, server) = spawn_fake_hosted_mcp_oauth_server().await;
    state
        .mcp
        .add_or_update("notion".to_string(), endpoint, HashMap::new(), true)
        .await;
    assert!(state.mcp.set_auth_kind("notion", "oauth".to_string()).await);

    let app = app_router(state.clone());
    let alice_resp = app
        .clone()
        .oneshot(tenant_request(
            "POST",
            "/mcp/notion/connect",
            "org-a",
            "workspace-a",
            "alice",
        ))
        .await
        .expect("alice connect response");
    assert_eq!(alice_resp.status(), StatusCode::OK);

    let alice_tenant =
        tandem_types::TenantContext::explicit_user_workspace("org-a", "workspace-a", None, "alice");
    let alice_session = state
        .oauth
        .mcp_sessions()
        .read()
        .await
        .values()
        .find(|candidate| candidate.tenant_context == alice_tenant)
        .cloned()
        .expect("alice mcp oauth session");

    let bob_resp = app
        .clone()
        .oneshot(tenant_request(
            "POST",
            "/mcp/notion/connect",
            "org-a",
            "workspace-a",
            "bob",
        ))
        .await
        .expect("bob connect response");
    assert_eq!(bob_resp.status(), StatusCode::OK);

    let bob_tenant =
        tandem_types::TenantContext::explicit_user_workspace("org-a", "workspace-a", None, "bob");
    let bob_session = state
        .oauth
        .mcp_sessions()
        .read()
        .await
        .values()
        .find(|candidate| candidate.tenant_context == bob_tenant)
        .cloned()
        .expect("bob mcp oauth session");

    let alice_inventory_resp = app
        .oneshot(tenant_request(
            "GET",
            "/mcp",
            "org-a",
            "workspace-a",
            "alice",
        ))
        .await
        .expect("alice inventory response");
    assert_eq!(alice_inventory_resp.status(), StatusCode::OK);
    let alice_inventory_body = to_bytes(alice_inventory_resp.into_body(), usize::MAX)
        .await
        .expect("alice inventory body");
    let alice_inventory_payload: Value =
        serde_json::from_slice(&alice_inventory_body).expect("alice inventory json");
    let alice_inventory_connections = alice_inventory_payload
        .pointer("/notion/connections")
        .and_then(Value::as_array)
        .expect("alice inventory connections");

    assert!(
        alice_inventory_connections.iter().any(|connection| {
            connection.get("connection_id").and_then(Value::as_str)
                == Some(alice_session.connection_id.as_str())
        }),
        "alice should see her own pending MCP connection"
    );
    assert!(
        alice_inventory_connections.iter().all(|connection| {
            connection.get("connection_id").and_then(Value::as_str)
                != Some(bob_session.connection_id.as_str())
        }),
        "alice must not see bob's pending MCP connection"
    );
    assert!(
        !String::from_utf8_lossy(&alice_inventory_body).contains("client_secret"),
        "public MCP inventory must not expose OAuth client secrets"
    );

    drop(server);
}