Skip to content

Commit 737fdb7

Browse files
committed
test: add tests for mcp_base_url config and URL fallback priority
1 parent 4025ebe commit 737fdb7

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

src/backend/tests/unit/api/v1/test_endpoints.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,41 @@ async def test_get_config_authenticated_returns_full_config(client: AsyncClient,
291291
assert "auto_saving_interval" in result, "Authenticated response must contain 'auto_saving_interval'"
292292
assert "health_check_max_retries" in result, "Authenticated response must contain 'health_check_max_retries'"
293293
assert "feature_flags" in result, "Authenticated response must contain 'feature_flags'"
294+
295+
296+
async def test_get_config_returns_mcp_base_url(client: AsyncClient, logged_in_headers: dict):
297+
"""Test that /config includes mcp_base_url for both authenticated and unauthenticated responses."""
298+
# Authenticated
299+
response = await client.get("api/v1/config", headers=logged_in_headers)
300+
result = response.json()
301+
assert response.status_code == status.HTTP_200_OK
302+
assert "mcp_base_url" in result, "Authenticated response must contain 'mcp_base_url'"
303+
assert isinstance(result["mcp_base_url"], str), "mcp_base_url must be a string"
304+
305+
# Unauthenticated
306+
response = await client.get("api/v1/config")
307+
result = response.json()
308+
assert response.status_code == status.HTTP_200_OK
309+
assert "mcp_base_url" in result, "Public response must contain 'mcp_base_url'"
310+
assert isinstance(result["mcp_base_url"], str), "mcp_base_url must be a string"
311+
312+
313+
async def test_get_config_mcp_base_url_defaults_to_empty(client: AsyncClient, logged_in_headers: dict):
314+
"""Test that mcp_base_url defaults to empty string when LANGFLOW_MCP_BASE_URL is not set."""
315+
response = await client.get("api/v1/config", headers=logged_in_headers)
316+
result = response.json()
317+
assert response.status_code == status.HTTP_200_OK
318+
assert result["mcp_base_url"] == ""
319+
320+
321+
async def test_get_config_mcp_base_url_from_settings(client: AsyncClient, logged_in_headers: dict, monkeypatch):
322+
"""Test that mcp_base_url reflects the value from settings."""
323+
from langflow.services.deps import get_settings_service
324+
325+
settings_service = get_settings_service()
326+
monkeypatch.setattr(settings_service.settings, "mcp_base_url", "https://langflow.example.com")
327+
328+
response = await client.get("api/v1/config", headers=logged_in_headers)
329+
result = response.json()
330+
assert response.status_code == status.HTTP_200_OK
331+
assert result["mcp_base_url"] == "https://langflow.example.com"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { api } from "@/controllers/API/api";
2+
import { useUtilityStore } from "@/stores/utilityStore";
3+
import { customGetMCPUrl } from "../custom-mcp-url";
4+
5+
describe("customGetMCPUrl", () => {
6+
const originalBaseURL = api.defaults.baseURL;
7+
8+
afterEach(() => {
9+
api.defaults.baseURL = originalBaseURL;
10+
useUtilityStore.setState({ mcpBaseUrl: "" });
11+
});
12+
13+
it("uses mcpBaseUrl from store when set", () => {
14+
api.defaults.baseURL = "";
15+
useUtilityStore.setState({ mcpBaseUrl: "https://custom.example.com" });
16+
17+
const url = customGetMCPUrl("proj-1");
18+
19+
expect(url).toBe(
20+
"https://custom.example.com/api/v1/mcp/project/proj-1/streamable",
21+
);
22+
});
23+
24+
it("mcpBaseUrl takes priority over api.defaults.baseURL", () => {
25+
api.defaults.baseURL = "https://api-default.example.com";
26+
useUtilityStore.setState({ mcpBaseUrl: "https://override.example.com" });
27+
28+
const url = customGetMCPUrl("proj-1");
29+
30+
expect(url).toBe(
31+
"https://override.example.com/api/v1/mcp/project/proj-1/streamable",
32+
);
33+
});
34+
35+
it("falls back to api.defaults.baseURL when mcpBaseUrl is empty", () => {
36+
api.defaults.baseURL = "https://api-default.example.com";
37+
useUtilityStore.setState({ mcpBaseUrl: "" });
38+
39+
const url = customGetMCPUrl("proj-1");
40+
41+
expect(url).toBe(
42+
"https://api-default.example.com/api/v1/mcp/project/proj-1/streamable",
43+
);
44+
});
45+
46+
it("falls back to window.location.origin when both are empty", () => {
47+
api.defaults.baseURL = "";
48+
useUtilityStore.setState({ mcpBaseUrl: "" });
49+
50+
const url = customGetMCPUrl("proj-1");
51+
52+
expect(url).toBe(
53+
`${window.location.origin}/api/v1/mcp/project/proj-1/streamable`,
54+
);
55+
});
56+
57+
it("strips trailing slashes from mcpBaseUrl", () => {
58+
useUtilityStore.setState({ mcpBaseUrl: "https://example.com/" });
59+
60+
const url = customGetMCPUrl("proj-1");
61+
62+
expect(url).toBe(
63+
"https://example.com/api/v1/mcp/project/proj-1/streamable",
64+
);
65+
});
66+
67+
it("strips multiple trailing slashes", () => {
68+
useUtilityStore.setState({ mcpBaseUrl: "https://example.com///" });
69+
70+
const url = customGetMCPUrl("proj-1");
71+
72+
expect(url).toBe(
73+
"https://example.com/api/v1/mcp/project/proj-1/streamable",
74+
);
75+
});
76+
77+
it("returns SSE URL when transport is sse", () => {
78+
useUtilityStore.setState({ mcpBaseUrl: "https://example.com" });
79+
80+
const url = customGetMCPUrl("proj-1", {}, "sse");
81+
82+
expect(url).toBe("https://example.com/api/v1/mcp/project/proj-1/sse");
83+
});
84+
85+
it("returns composer URL when useComposer is true and streamableHttpUrl is set", () => {
86+
useUtilityStore.setState({ mcpBaseUrl: "https://should-not-use.com" });
87+
88+
const url = customGetMCPUrl(
89+
"proj-1",
90+
{
91+
useComposer: true,
92+
streamableHttpUrl: "https://composer.example.com/streamable",
93+
},
94+
"streamablehttp",
95+
);
96+
97+
expect(url).toBe("https://composer.example.com/streamable");
98+
});
99+
});

0 commit comments

Comments
 (0)