← Files Dataiku HeadlessARCHIVED FILE
tests/test_http_middleware.py
7.19 KB · Nov 3, 2026 · 19:02 UTC
# Copyright 2026 Dataiku SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The HTTP middleware exchanges a DSS token only for DSS-backed tools."""
import asyncio
from types import SimpleNamespace
import pytest
import dataiku_mcp.server as server
from dataiku_mcp.config import request
def _call_middleware(
monkeypatch,
tool_name: str,
*,
tool_tags: set[str] | None = None,
tool_resolves: bool = True,
include_fastmcp_context: bool = True,
):
events = []
access_token = SimpleNamespace(
token="mcp-token",
claims={"iss": "https://idp.example", "sub": "alice"},
)
middleware = server.RequestContextMiddleware()
monkeypatch.setattr(server, "get_access_token", lambda: access_token)
monkeypatch.setattr(
request,
"bind_http_identity",
lambda issuer, subject: (
events.append(("identity", issuer, subject)) or "identity"
),
)
monkeypatch.setattr(
request,
"pin_current_instance",
lambda: events.append(("pin",)) or "instance",
)
monkeypatch.setattr(
request,
"reset_pinned_instance",
lambda token: events.append(("reset_instance", token)),
)
monkeypatch.setattr(
request,
"reset_http_identity",
lambda token: events.append(("reset_identity", token)),
)
monkeypatch.setattr(
server,
"exchange_http_token",
lambda token: _exchange(events, token),
)
monkeypatch.setattr(
request,
"bind_http_dss_token",
lambda token: events.append(("set_dss", token)) or "dss",
)
monkeypatch.setattr(
request,
"reset_http_dss_token",
lambda token: events.append(("reset_dss", token)),
)
async def call_next(_context):
events.append(("tool",))
return "result"
class FakeFastMCP:
async def get_tool(self, name):
assert name == tool_name
if not tool_resolves:
return None
return SimpleNamespace(tags=tool_tags or set())
fastmcp_context = None
if include_fastmcp_context:
fastmcp_context = SimpleNamespace(fastmcp=FakeFastMCP())
context = SimpleNamespace(
message=SimpleNamespace(name=tool_name),
fastmcp_context=fastmcp_context,
)
assert asyncio.run(middleware.on_call_tool(context, call_next)) == "result"
return events
async def _exchange(events, token):
events.append(("exchange", token))
return "dss-token"
@pytest.mark.parametrize(
"claims",
[
{"sub": "alice"},
{"iss": "https://idp.example"},
{"iss": None, "sub": "alice"},
{"iss": 42, "sub": "alice"},
{"iss": [], "sub": "alice"},
{"iss": "https://idp.example", "sub": None},
{"iss": "https://idp.example", "sub": 42},
{"iss": "https://idp.example", "sub": []},
],
)
def test_http_invalid_identity_claims_fail_before_request_setup(monkeypatch, claims):
events = []
middleware = server.RequestContextMiddleware()
access_token = SimpleNamespace(token="mcp-token", claims=claims)
monkeypatch.setattr(server, "get_access_token", lambda: access_token)
monkeypatch.setattr(
request, "pin_current_instance", lambda: events.append(("pin",))
)
monkeypatch.setattr(
server, "exchange_http_token", lambda _: events.append(("exchange",))
)
async def call_next(_context):
events.append(("tool",))
context = SimpleNamespace(message=SimpleNamespace(name="list_projects"))
with pytest.raises(ValueError, match="non-empty string iss and sub claims"):
asyncio.run(middleware.on_call_tool(context, call_next))
assert events == []
def test_dss_independent_tools_are_tagged():
async def get_tagged_tool_names():
tools = await server.mcp.list_tools(run_middleware=False)
return {
tool.name for tool in tools if server.DSS_INDEPENDENT_TOOL_TAG in tool.tags
}
assert asyncio.run(get_tagged_tool_names()) == {
"list_instances",
"switch_instance",
"delete_instance",
"configure_instance",
"get_cobuild_turn_status",
"list_cobuild_conversations",
}
def test_http_local_only_tool_skips_dss_token_exchange(monkeypatch):
events = _call_middleware(
monkeypatch,
"switch_instance",
tool_tags={server.DSS_INDEPENDENT_TOOL_TAG},
)
assert ("exchange", "mcp-token") not in events
assert ("set_dss", "dss-token") not in events
assert events[-2:] == [
("reset_instance", "instance"),
("reset_identity", "identity"),
]
def test_http_dss_tool_exchanges_and_resets_its_dss_token(monkeypatch):
events = _call_middleware(monkeypatch, "list_projects")
assert ("exchange", "mcp-token") in events
assert ("set_dss", "dss-token") in events
assert events[-3:] == [
("reset_dss", "dss"),
("reset_instance", "instance"),
("reset_identity", "identity"),
]
def test_http_current_instance_exchanges_a_dss_token(monkeypatch):
events = _call_middleware(monkeypatch, "get_current_instance")
assert ("exchange", "mcp-token") in events
assert ("set_dss", "dss-token") in events
@pytest.mark.parametrize(
("tool_resolves", "include_fastmcp_context"),
[(False, True), (True, False)],
)
def test_http_unknown_tool_policy_requires_dss_token_exchange(
monkeypatch, tool_resolves, include_fastmcp_context
):
events = _call_middleware(
monkeypatch,
"unknown_tool",
tool_resolves=tool_resolves,
include_fastmcp_context=include_fastmcp_context,
)
assert ("exchange", "mcp-token") in events
def test_http_identity_is_reset_when_instance_pinning_fails(monkeypatch):
events = []
access_token = SimpleNamespace(
token="mcp-token",
claims={"iss": "https://idp.example", "sub": "alice"},
)
middleware = server.RequestContextMiddleware()
monkeypatch.setattr(server, "get_access_token", lambda: access_token)
monkeypatch.setattr(request, "bind_http_identity", lambda *_: "identity")
def fail_pinning():
raise ValueError("invalid settings")
monkeypatch.setattr(
request,
"pin_current_instance",
fail_pinning,
)
monkeypatch.setattr(
request,
"reset_http_identity",
lambda token: events.append(("reset_identity", token)),
)
async def call_next(_context):
pytest.fail("The tool must not run after pinning fails.")
context = SimpleNamespace(message=SimpleNamespace(name="list_projects"))
with pytest.raises(ValueError, match="invalid settings"):
asyncio.run(middleware.on_call_tool(context, call_next))
assert events == [("reset_identity", "identity")]
SHA-256: ba5894f3d2da7251742409d20906e46ccdd671798a55bea98ce8178cd6b55991