|
1 | 1 | from types import SimpleNamespace |
2 | 2 |
|
3 | 3 | import pytest |
| 4 | +from langflow.api.utils.core import extract_global_variables_from_headers |
4 | 5 | from langflow.api.v1 import mcp_utils |
5 | 6 | from lfx.interface.components import component_cache |
6 | 7 |
|
@@ -292,3 +293,78 @@ async def test_handle_list_tools_requires_current_user_on_global_server(monkeypa |
292 | 293 | # No user context set — must return empty. |
293 | 294 | tools = await mcp_utils.handle_list_tools() |
294 | 295 | assert tools == [] |
| 296 | +class TestExtractGlobalVariablesFromHeaders: |
| 297 | + """Unit tests for ``extract_global_variables_from_headers``. |
| 298 | +
|
| 299 | + Covers the MCP auth-header propagation fix (issue #12529): ``x-api-key`` |
| 300 | + and ``authorization`` should be captured under their lowercase names when |
| 301 | + (and only when) ``include_auth_headers=True`` is passed. The default |
| 302 | + behavior must remain backwards-compatible for non-MCP routes, where |
| 303 | + ``x-api-key`` is Langflow's own auth key and must not leak into the graph |
| 304 | + context. |
| 305 | + """ |
| 306 | + |
| 307 | + def test_langflow_global_var_prefix_still_extracted(self): |
| 308 | + """Regression guard: ``X-LANGFLOW-GLOBAL-VAR-*`` extraction is preserved.""" |
| 309 | + headers = { |
| 310 | + "X-LANGFLOW-GLOBAL-VAR-API-KEY": "secret-value", |
| 311 | + "X-LANGFLOW-GLOBAL-VAR-DB-URL": "postgres://host/db", |
| 312 | + "Content-Type": "application/json", |
| 313 | + } |
| 314 | + |
| 315 | + result = extract_global_variables_from_headers(headers) |
| 316 | + |
| 317 | + assert result == {"API-KEY": "secret-value", "DB-URL": "postgres://host/db"} |
| 318 | + |
| 319 | + def test_auth_headers_not_extracted_by_default(self): |
| 320 | + """Non-MCP call sites: ``x-api-key`` / ``authorization`` must not leak through.""" |
| 321 | + headers = { |
| 322 | + "x-api-key": "langflow-auth-key", |
| 323 | + "authorization": "Bearer token", |
| 324 | + "X-LANGFLOW-GLOBAL-VAR-MY-VAR": "value", |
| 325 | + } |
| 326 | + |
| 327 | + result = extract_global_variables_from_headers(headers) |
| 328 | + |
| 329 | + assert "x-api-key" not in result |
| 330 | + assert "authorization" not in result |
| 331 | + assert result == {"MY-VAR": "value"} |
| 332 | + |
| 333 | + def test_auth_headers_extracted_under_lowercase_when_opted_in(self): |
| 334 | + """MCP call sites: lowercase auth headers are captured when opted in.""" |
| 335 | + headers = { |
| 336 | + "x-api-key": "api-key-value", |
| 337 | + "authorization": "Bearer jwt-token", |
| 338 | + } |
| 339 | + |
| 340 | + result = extract_global_variables_from_headers(headers, include_auth_headers=True) |
| 341 | + |
| 342 | + assert result == {"x-api-key": "api-key-value", "authorization": "Bearer jwt-token"} |
| 343 | + |
| 344 | + def test_auth_header_matching_is_case_insensitive(self): |
| 345 | + """Headers with mixed or uppercase casing still match (e.g. ``X-Api-Key``, ``AUTHORIZATION``).""" |
| 346 | + headers = { |
| 347 | + "X-Api-Key": "mixed-case-value", |
| 348 | + "AUTHORIZATION": "Bearer UPPER", |
| 349 | + } |
| 350 | + |
| 351 | + result = extract_global_variables_from_headers(headers, include_auth_headers=True) |
| 352 | + |
| 353 | + assert result == {"x-api-key": "mixed-case-value", "authorization": "Bearer UPPER"} |
| 354 | + |
| 355 | + def test_both_categories_extracted_together(self): |
| 356 | + """``X-LANGFLOW-GLOBAL-VAR-*`` and auth headers coexist when opted in.""" |
| 357 | + headers = { |
| 358 | + "X-LANGFLOW-GLOBAL-VAR-API-KEY": "global-secret", |
| 359 | + "x-api-key": "incoming-mcp-key", |
| 360 | + "Authorization": "Bearer mcp-token", |
| 361 | + "Content-Type": "application/json", |
| 362 | + } |
| 363 | + |
| 364 | + result = extract_global_variables_from_headers(headers, include_auth_headers=True) |
| 365 | + |
| 366 | + assert result == { |
| 367 | + "API-KEY": "global-secret", |
| 368 | + "x-api-key": "incoming-mcp-key", |
| 369 | + "authorization": "Bearer mcp-token", |
| 370 | + } |
0 commit comments