Point flow's tests and scripts at /v1

Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
This commit is contained in:
2026-09-29 18:34:34 -07:00
parent 8c119882bc
commit db35659c45
70 changed files with 1253 additions and 1253 deletions
@@ -1,7 +1,7 @@
"""Direct Watsonx adapter scenario runner.
Runs scenario matrices against `WatsonxOrchestrateDeploymentService` directly
(no `/api/v1/deployments` calls).
(no `/v1/deployments` calls).
Warning:
--------
@@ -1,6 +1,6 @@
"""Deployments API create/update matrix runner.
This script exercises `/api/v1/deployments` over HTTP and focuses on
This script exercises `/v1/deployments` over HTTP and focuses on
create/update payload-heavy scenarios for the Watsonx Orchestrate provider.
Warning:
@@ -1009,16 +1009,16 @@ class DeploymentsApiParallelE2E:
return results
async def _call_create(self, payload: dict[str, Any]) -> HttpResponseEnvelope:
response = await self._client.post("/api/v1/deployments", json=payload)
response = await self._client.post("/v1/deployments", json=payload)
return self._normalize_response(response)
async def _call_update(self, payload: dict[str, Any]) -> HttpResponseEnvelope:
response = await self._client.patch(f"/api/v1/deployments/{payload['deployment_id']}", json=payload["body"])
response = await self._client.patch(f"/v1/deployments/{payload['deployment_id']}", json=payload["body"])
return self._normalize_response(response)
async def _call_update_chain_add_remove(self, payload: dict[str, Any]) -> HttpResponseEnvelope:
add_response = await self._client.patch(
f"/api/v1/deployments/{payload['deployment_id']}",
f"/v1/deployments/{payload['deployment_id']}",
json={
"provider_data": self._provider_data_update(
upsert_flows=[
@@ -1036,7 +1036,7 @@ class DeploymentsApiParallelE2E:
if normalized_add.status_code >= HTTP_STATUS_MULTIPLE_CHOICES:
return normalized_add
remove_response = await self._client.patch(
f"/api/v1/deployments/{payload['deployment_id']}",
f"/v1/deployments/{payload['deployment_id']}",
json={
"provider_data": self._provider_data_update(
remove_flows=[payload["add_flow_version_id"]],
@@ -1593,7 +1593,7 @@ class DeploymentsApiParallelE2E:
msg = f"refusing to delete unmanaged deployment id: {deployment_id}"
raise RuntimeError(msg)
response = await self._client.delete(
f"/api/v1/deployments/{deployment_id}",
f"/v1/deployments/{deployment_id}",
params={"include_provider": str(include_provider).lower()},
)
if response.status_code not in {HTTP_STATUS_NO_CONTENT, HTTP_STATUS_NOT_FOUND}:
@@ -1673,7 +1673,7 @@ class DeploymentsApiParallelE2E:
created_flow_ids = list(self.created_flow_ids)
for index, flow_id in enumerate(created_flow_ids, start=1):
print(f"cleanup: deleting flow {index}/{len(created_flow_ids)} {flow_id} ...")
response = await self._client.delete(f"/api/v1/flows/{flow_id}")
response = await self._client.delete(f"/v1/flows/{flow_id}")
if response.status_code in {HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT, HTTP_STATUS_NOT_FOUND}:
self.created_flow_ids.discard(flow_id)
if response.status_code == HTTP_STATUS_NOT_FOUND:
@@ -1812,7 +1812,7 @@ class DeploymentsApiParallelE2E:
provider_id = self.created_provider_account_id
if provider_id is None:
return
response = await self._client.delete(f"/api/v1/deployments/providers/{provider_id}")
response = await self._client.delete(f"/v1/deployments/providers/{provider_id}")
if response.status_code in {HTTP_STATUS_NO_CONTENT, HTTP_STATUS_NOT_FOUND}:
self.created_provider_account_id = None
return
@@ -1840,7 +1840,7 @@ class DeploymentsApiParallelE2E:
size = 50
while True:
response = await self._client.get(
"/api/v1/deployments/providers",
"/v1/deployments/providers",
params={"page": page, "size": size},
)
normalized = self._normalize_response(response)
@@ -1884,7 +1884,7 @@ class DeploymentsApiParallelE2E:
}
if self.provider_tenant_id:
create_payload["provider_data"]["tenant_id"] = self.provider_tenant_id
response = await self._client.post("/api/v1/deployments/providers", json=create_payload)
response = await self._client.post("/v1/deployments/providers", json=create_payload)
normalized = self._normalize_response(response)
if normalized.status_code != HTTP_STATUS_CREATED:
msg = f"creating provider account failed: status={normalized.status_code} detail={normalized.detail}"
@@ -1914,7 +1914,7 @@ class DeploymentsApiParallelE2E:
for starter_path in starter_paths:
starter_payload = self._load_starter_project_payload(starter_path)
flow_payload = self._build_flow_create_payload(starter_payload=starter_payload, starter_path=starter_path)
flow_response = await self._client.post("/api/v1/flows/", json=flow_payload)
flow_response = await self._client.post("/v1/flows/", json=flow_payload)
flow_envelope = self._normalize_response(flow_response)
if flow_envelope.status_code != HTTP_STATUS_CREATED:
msg = (
@@ -1930,7 +1930,7 @@ class DeploymentsApiParallelE2E:
self.created_flow_ids.add(flow_id)
snapshot_response = await self._client.post(
f"/api/v1/flows/{flow_id}/versions/",
f"/v1/flows/{flow_id}/versions/",
json={"description": f"e2e version from {starter_path.stem}"},
)
snapshot_envelope = self._normalize_response(snapshot_response)
@@ -2056,7 +2056,7 @@ def _parse_csv(raw: str) -> list[str]:
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run deployments API create/update matrix against /api/v1/deployments."
description="Run deployments API create/update matrix against /v1/deployments."
)
parser.add_argument("--base-url", default=os.getenv("FLOW_BASE_URL", "http://localhost:7860"))
parser.add_argument("--api-key", default=os.getenv("FLOW_API_KEY", ""))
+10 -10
View File
@@ -10,7 +10,7 @@ SUITES="${SUITES:-curl,python,javascript}"
EXECUTE_MODE="${EXECUTE_MODE:-true}"
export FLOW_AUTO_LOGIN="${FLOW_AUTO_LOGIN:-true}"
# /api/v2/workflows (docs Python workflow examples) requires this. Always enable for this
# /v1/workflows (docs Python workflow examples) requires this. Always enable for this
# harness so a user-wide FLOW_DEVELOPER_API_ENABLED=false does not break the suite.
export FLOW_DEVELOPER_API_ENABLED=true
@@ -33,7 +33,7 @@ cleanup() {
trap cleanup EXIT
# If PORT is already taken, Hanzo Flow may bind to PORT+1 while this script still uses PORT for
# curl and examples — you then hit the wrong server (e.g. 403 on /api/v2/workflows without dev API).
# curl and examples — you then hit the wrong server (e.g. 403 on /v1/workflows without dev API).
port_is_in_use() {
local host="$1" port="$2"
uv run python - "$host" "$port" <<'PY'
@@ -74,7 +74,7 @@ if port_is_in_use "$HOST" "$PORT"; then
echo "Port $REQUESTED_PORT was in use; using $PORT for this run (set FLOW_PORT to pin a port)."
fi
echo "Starting Hanzo Flow on http://$HOST:$PORT (developer API enabled for /api/v2/workflows)"
echo "Starting Hanzo Flow on http://$HOST:$PORT (developer API enabled for /v1/workflows)"
# Set on the command line so the server process always sees it (macOS launcher/exec paths).
FLOW_DEVELOPER_API_ENABLED=true uv run flow run --backend-only --host "$HOST" --port "$PORT" >/tmp/flow-server.log 2>&1 &
echo $! >/tmp/flow-server.pid
@@ -116,14 +116,14 @@ last_status = None
# Fail fast: short timeouts and small retry budget.
for attempt in range(1, 9):
try:
resp = session.get(f"{base}/api/v1/auto_login", timeout=8)
resp = session.get(f"{base}/v1/auto_login", timeout=8)
last_status = ("auto_login", resp.status_code)
if resp.status_code == 200:
token = resp.json()["access_token"]
break
resp2 = session.post(
f"{base}/api/v1/login",
f"{base}/v1/login",
data={"username": user, "password": password},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=8,
@@ -142,7 +142,7 @@ if not token:
raise RuntimeError(f"Could not log in after retries (last {last_status})")
key_resp = session.post(
f"{base}/api/v1/api_key/",
f"{base}/v1/api_key/",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"name": "local-docs-examples"},
timeout=8,
@@ -180,7 +180,7 @@ headers = {"accept": "application/json", "Content-Type": "application/json", "x-
try:
project_name = f"api-example-project-{uuid.uuid4().hex[:8]}"
project_resp = requests.post(
f"{base_url}/api/v1/projects/",
f"{base_url}/v1/projects/",
headers=headers,
json={"name": project_name, "description": "Local docs examples bootstrap", "components_list": [], "flows_list": []},
timeout=20,
@@ -193,7 +193,7 @@ try:
flow_name = f"api-example-flow-{uuid.uuid4().hex[:8]}"
flow_resp = requests.post(
f"{base_url}/api/v1/flows/",
f"{base_url}/v1/flows/",
headers=headers,
json={
"name": flow_name,
@@ -209,7 +209,7 @@ try:
raise RuntimeError(f"Flow creation returned no id: {flow_data}")
build_resp = requests.post(
f"{base_url}/api/v1/build/{flow_id}/flow",
f"{base_url}/v1/build/{flow_id}/flow",
headers=headers,
json={},
timeout=20,
@@ -223,7 +223,7 @@ try:
project_zip_path = Path("/tmp/flow-project-import.zip")
try:
export_resp = requests.get(
f"{base_url}/api/v1/projects/download/{project_id}",
f"{base_url}/v1/projects/download/{project_id}",
headers={"accept": "application/json", "x-api-key": api_key},
timeout=30,
)
+17 -17
View File
@@ -479,7 +479,7 @@ async def test_user(client):
username="testuser",
password="testpassword", # noqa: S106
)
response = await client.post("api/v1/users/", json=user_data.model_dump())
response = await client.post("v1/users/", json=user_data.model_dump())
assert response.status_code == 201
user = response.json()
yield user
@@ -525,7 +525,7 @@ async def active_user(client): # noqa: ARG001
@pytest.fixture
async def logged_in_headers(client, active_user):
login_data = {"username": active_user.username, "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
tokens = response.json()
a_token = tokens["access_token"]
@@ -561,7 +561,7 @@ async def active_super_user(client): # noqa: ARG001
@pytest.fixture
async def logged_in_headers_super_user(client, active_super_user):
login_data = {"username": active_super_user.username, "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
tokens = response.json()
a_token = tokens["access_token"]
@@ -607,12 +607,12 @@ async def added_flow_webhook_test(client, json_webhook_test, logged_in_headers):
flow = orjson.loads(json_webhook_test)
data = flow["data"]
flow = FlowCreate(name="Basic Chat", description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
yield response.json()
await client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers)
await client.delete(f"v1/flows/{response.json()['id']}", headers=logged_in_headers)
@pytest.fixture
@@ -620,12 +620,12 @@ async def added_flow_chat_input(client, json_chat_input, logged_in_headers):
flow = orjson.loads(json_chat_input)
data = flow["data"]
flow = FlowCreate(name="Chat Input", description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
yield response.json()
await client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers)
await client.delete(f"v1/flows/{response.json()['id']}", headers=logged_in_headers)
@pytest.fixture
@@ -633,12 +633,12 @@ async def added_flow_two_outputs(client, json_two_outputs, logged_in_headers):
flow = orjson.loads(json_two_outputs)
data = flow["data"]
flow = FlowCreate(name="Two Outputs", description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
yield response.json()
await client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers)
await client.delete(f"v1/flows/{response.json()['id']}", headers=logged_in_headers)
@pytest.fixture
@@ -646,12 +646,12 @@ async def added_vector_store(client, json_vector_store, logged_in_headers):
vector_store = orjson.loads(json_vector_store)
data = vector_store["data"]
vector_store = FlowCreate(name="Vector Store", description="description", data=data)
response = await client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == vector_store.name
assert response.json()["data"] == vector_store.data
yield response.json()
await client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers)
await client.delete(f"v1/flows/{response.json()['id']}", headers=logged_in_headers)
@pytest.fixture
@@ -661,12 +661,12 @@ async def added_webhook_test(client, json_webhook_test, logged_in_headers):
webhook_test = FlowCreate(
name="Webhook Test", description="description", data=data, endpoint_name=webhook_test["endpoint_name"]
)
response = await client.post("api/v1/flows/", json=webhook_test.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=webhook_test.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == webhook_test.name
assert response.json()["data"] == webhook_test.data
yield response.json()
await client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers)
await client.delete(f"v1/flows/{response.json()['id']}", headers=logged_in_headers)
@pytest.fixture
@@ -675,10 +675,10 @@ async def flow_component(client: AsyncClient, logged_in_headers):
graph = Graph(start=chat_input, end=chat_input)
graph_dict = graph.dump(name="Chat Input Component")
flow = FlowCreate(**graph_dict)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
yield response.json()
await client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers)
await client.delete(f"v1/flows/{response.json()['id']}", headers=logged_in_headers)
@pytest.fixture
@@ -786,10 +786,10 @@ async def get_simple_api_test(client, logged_in_headers, json_simple_api_test):
flow = orjson.loads(json_simple_api_test)
data = flow["data"]
flow = FlowCreate(name="Simple API Test", data=data, description="Simple API Test")
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
yield response.json()
await client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers)
await client.delete(f"v1/flows/{response.json()['id']}", headers=logged_in_headers)
@pytest.fixture(name="starter_project")
@@ -23,7 +23,7 @@ class TestRunFlowEndToEnd:
# First, create a folder for our flows
folder_response = await client.post(
"api/v1/folders/",
"v1/folders/",
json={"name": "Test Folder", "description": "Folder for integration tests"},
headers=logged_in_headers,
)
@@ -39,7 +39,7 @@ class TestRunFlowEndToEnd:
# Create target flow via API (uses real database)
response = await client.post(
"api/v1/flows/",
"v1/flows/",
json=target_flow.model_dump(mode="json"),
headers=logged_in_headers,
)
@@ -55,7 +55,7 @@ class TestRunFlowEndToEnd:
wrapper_flow = FlowCreate(**wrapper_dict, folder_id=folder_id)
wrapper_response = await client.post(
"api/v1/flows/",
"v1/flows/",
json=wrapper_flow.model_dump(mode="json"),
headers=logged_in_headers,
)
@@ -100,9 +100,9 @@ class TestRunFlowEndToEnd:
assert target_flow_id in flow_ids
finally:
# Cleanup
await client.delete(f"api/v1/flows/{target_flow_id}", headers=logged_in_headers)
await client.delete(f"api/v1/flows/{wrapper_flow_id}", headers=logged_in_headers)
await client.delete(f"api/v1/folders/{folder_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{target_flow_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{wrapper_flow_id}", headers=logged_in_headers)
await client.delete(f"v1/folders/{folder_id}", headers=logged_in_headers)
@pytest.mark.asyncio
async def test_run_flow_with_inputs_and_outputs(self, active_user):
@@ -151,7 +151,7 @@ class TestRunFlowComponentWithTools:
# Create a folder for our flows
folder_response = await client.post(
"api/v1/folders/",
"v1/folders/",
json={"name": "Tool Test Folder", "description": "Folder for tool generation tests"},
headers=logged_in_headers,
)
@@ -167,7 +167,7 @@ class TestRunFlowComponentWithTools:
tool_flow = FlowCreate(**graph_dict, folder_id=folder_id, user_id=str(active_user.id))
# Create tool flow via API (will be associated with active_user via logged_in_headers)
response = await client.post("api/v1/flows/", json=tool_flow.model_dump(mode="json"), headers=logged_in_headers)
response = await client.post("v1/flows/", json=tool_flow.model_dump(mode="json"), headers=logged_in_headers)
assert response.status_code == 201
flow_data = response.json()
flow_id = flow_data["id"]
@@ -182,7 +182,7 @@ class TestRunFlowComponentWithTools:
wrapper_flow = FlowCreate(**wrapper_dict, folder_id=folder_id, user_id=str(active_user.id))
wrapper_response = await client.post(
"api/v1/flows/",
"v1/flows/",
json=wrapper_flow.model_dump(mode="json"),
headers=logged_in_headers,
)
@@ -223,9 +223,9 @@ class TestRunFlowComponentWithTools:
assert isinstance(tools, list), "Expected tools to be a list"
finally:
# Cleanup
await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
await client.delete(f"api/v1/flows/{wrapper_flow_id}", headers=logged_in_headers)
await client.delete(f"api/v1/folders/{folder_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{flow_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{wrapper_flow_id}", headers=logged_in_headers)
await client.delete(f"v1/folders/{folder_id}", headers=logged_in_headers)
class TestRunFlowOutputResolution:
@@ -245,7 +245,7 @@ class TestRunFlowOutputResolution:
flow = FlowCreate(**graph_dict)
# Create flow via API
response = await client.post("api/v1/flows/", json=flow.model_dump(mode="json"), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(mode="json"), headers=logged_in_headers)
assert response.status_code == 201
flow_data = response.json()
flow_id = flow_data["id"]
@@ -281,7 +281,7 @@ class TestRunFlowOutputResolution:
assert real_graph.flow_name == flow_name
finally:
# Cleanup
await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{flow_id}", headers=logged_in_headers)
class TestRunFlowCaching:
@@ -302,7 +302,7 @@ class TestRunFlowCaching:
flow = FlowCreate(**graph_dict)
# Create flow via API
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_data = response.json()
flow_id = flow_data["id"]
@@ -332,7 +332,7 @@ class TestRunFlowCaching:
assert graph1 == graph2, "Expected same graph instance from cache"
finally:
# Cleanup
await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{flow_id}", headers=logged_in_headers)
@pytest.fixture
@@ -461,7 +461,7 @@ class TestRunFlowInternalLogic:
flow = FlowCreate(**graph_dict)
# Create flow via API
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_data = response.json()
flow_id = flow_data["id"]
@@ -511,4 +511,4 @@ class TestRunFlowInternalLogic:
finally:
# Cleanup
await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{flow_id}", headers=logged_in_headers)
@@ -23,7 +23,7 @@ async def test_openai_pre_streaming_error_format(client: AsyncClient, created_ap
}
response = await client.post(
"api/v1/responses",
"v1/responses",
json=payload,
headers=headers,
)
@@ -61,7 +61,7 @@ async def test_openai_streaming_runtime_error_format(client: AsyncClient, create
}
response = await client.post(
"api/v1/responses",
"v1/responses",
json=payload,
headers=headers,
)
@@ -110,7 +110,7 @@ async def test_openai_streaming_success_finish_reason(client: AsyncClient, creat
}
response = await client.post(
"api/v1/responses",
"v1/responses",
json=payload,
headers=headers,
)
@@ -80,7 +80,7 @@ async def test_project_authentication_settings(client: AsyncClient, logged_in_he
mock_encrypt.return_value = {"encrypted": "apikey_auth"}
response = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "Auth Test 1", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -101,7 +101,7 @@ async def test_project_authentication_settings(client: AsyncClient, logged_in_he
mock_get_settings.return_value = mock_service
response = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "Auth Test 2", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -115,7 +115,7 @@ async def test_project_authentication_settings(client: AsyncClient, logged_in_he
@pytest.mark.asyncio
async def test_create_project_blocks_moving_deployed_flow(client: AsyncClient, logged_in_headers, active_user):
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "integration-deployed-flow", "data": {"nodes": [], "edges": []}},
headers=logged_in_headers,
)
@@ -131,7 +131,7 @@ async def test_create_project_blocks_moving_deployed_flow(client: AsyncClient, l
)
create_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "integration-target-project", "flows_list": [str(flow_id)], "components_list": []},
headers=logged_in_headers,
)
@@ -149,7 +149,7 @@ class TestValidateMcpServerForProject:
# Create MCP server via API
response = await client.post(
"/v2/mcp/servers/lf-test_project", json=server_config, headers={"x-api-key": created_api_key.api_key}
"/v1/mcp/servers/lf-test_project", json=server_config, headers={"x-api-key": created_api_key.api_key}
)
assert response.status_code == 200
@@ -170,7 +170,7 @@ class TestValidateMcpServerForProject:
assert result.conflict_message == ""
# Cleanup - delete the server
await client.delete("/v2/mcp/servers/lf-test_project", headers={"x-api-key": created_api_key.api_key})
await client.delete("/v1/mcp/servers/lf-test_project", headers={"x-api-key": created_api_key.api_key})
@pytest.mark.asyncio
@pytest.mark.parametrize("transport", ["streamable", "sse"])
@@ -184,7 +184,7 @@ class TestValidateMcpServerForProject:
# Create MCP server with different project ID via API
response = await client.post(
f"/v2/mcp/servers/{server_name}", json=server_config, headers={"x-api-key": created_api_key.api_key}
f"/v1/mcp/servers/{server_name}", json=server_config, headers={"x-api-key": created_api_key.api_key}
)
assert response.status_code == 200
@@ -206,7 +206,7 @@ class TestValidateMcpServerForProject:
assert str(test_project.id) in result.conflict_message
# Cleanup - delete the server
await client.delete(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": created_api_key.api_key})
await client.delete(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": created_api_key.api_key})
@pytest.mark.asyncio
@pytest.mark.parametrize("transport", ["streamable", "sse"])
@@ -220,7 +220,7 @@ class TestValidateMcpServerForProject:
# Create MCP server with different project ID via API
response = await client.post(
f"/v2/mcp/servers/{server_name}", json=server_config, headers={"x-api-key": created_api_key.api_key}
f"/v1/mcp/servers/{server_name}", json=server_config, headers={"x-api-key": created_api_key.api_key}
)
assert response.status_code == 200
@@ -249,7 +249,7 @@ class TestValidateMcpServerForProject:
assert "Cannot delete MCP server" in result.conflict_message
# Cleanup - delete the server
await client.delete(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": created_api_key.api_key})
await client.delete(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": created_api_key.api_key})
@pytest.mark.asyncio
async def test_validate_server_exception_handling(self, active_user, test_project, client: AsyncClient): # noqa: ARG002
@@ -483,7 +483,7 @@ class TestMultiUserMCPServerAccess:
# User One creates a server
response = await client.post(
f"/v2/mcp/servers/{server_name}",
f"/v1/mcp/servers/{server_name}",
json=config_one,
headers={"x-api-key": user_one_api_key},
)
@@ -492,7 +492,7 @@ class TestMultiUserMCPServerAccess:
# User Two creates a server with the same name
response = await client.post(
f"/v2/mcp/servers/{server_name}",
f"/v1/mcp/servers/{server_name}",
json=config_two,
headers={"x-api-key": user_two_api_key},
)
@@ -500,17 +500,17 @@ class TestMultiUserMCPServerAccess:
assert response.json() == config_two
# Verify each user gets their own server config
response_one = await client.get(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
response_one = await client.get(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
assert response_one.status_code == 200
assert response_one.json() == config_one
response_two = await client.get(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
response_two = await client.get(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
assert response_two.status_code == 200
assert response_two.json() == config_two
# User One updates their server
response = await client.patch(
f"/v2/mcp/servers/{server_name}",
f"/v1/mcp/servers/{server_name}",
json=updated_config_one,
headers={"x-api-key": user_one_api_key},
)
@@ -518,32 +518,32 @@ class TestMultiUserMCPServerAccess:
assert response.json() == updated_config_one
# Verify User One's server is updated and User Two's is not
response_one = await client.get(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
response_one = await client.get(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
assert response_one.status_code == 200
assert response_one.json() == updated_config_one
response_two = await client.get(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
response_two = await client.get(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
assert response_two.status_code == 200
assert response_two.json() == config_two, "User Two's server should not be affected by User One's update"
# User One tries to delete User Two's server (should only delete their own)
response = await client.delete(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
response = await client.delete(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
assert response.status_code == 200
# Verify User One's server is deleted
response_one = await client.get(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
response_one = await client.get(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_one_api_key})
assert response_one.json() is None
# Verify User Two's server still exists
response_two = await client.get(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
response_two = await client.get(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
assert response_two.status_code == 200
assert response_two.json() == config_two, "User Two's server should not be affected by User One's delete"
# Cleanup: User Two deletes their server
response = await client.delete(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
response = await client.delete(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
assert response.status_code == 200
response_two = await client.get(f"/v2/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
response_two = await client.get(f"/v1/mcp/servers/{server_name}", headers={"x-api-key": user_two_api_key})
assert response_two.json() is None
@@ -3,7 +3,7 @@ from httpx import AsyncClient
async def test_create_folder(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/api_key/", headers=logged_in_headers)
response = await client.get("v1/api_key/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -21,7 +21,7 @@ async def test_create_api_key_route(client: AsyncClient, logged_in_headers, acti
"api_key": "string",
"user_id": str(active_user.id),
}
response = await client.post("api/v1/api_key/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/api_key/", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -43,10 +43,10 @@ async def test_delete_api_key_route(client: AsyncClient, logged_in_headers, acti
"api_key": "string",
"user_id": str(active_user.id),
}
response_ = await client.post("api/v1/api_key/", json=basic_case, headers=logged_in_headers)
response_ = await client.post("v1/api_key/", json=basic_case, headers=logged_in_headers)
id_ = response_.json()["id"]
response = await client.delete(f"api/v1/api_key/{id_}", headers=logged_in_headers)
response = await client.delete(f"v1/api_key/{id_}", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -56,7 +56,7 @@ async def test_delete_api_key_route(client: AsyncClient, logged_in_headers, acti
async def test_save_store_api_key(client: AsyncClient, logged_in_headers):
basic_case = {"api_key": "string"}
response = await client.post("api/v1/api_key/store", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/api_key/store", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -80,7 +80,7 @@ async def test_delete_api_key_route_unauthorized(client: AsyncClient, logged_in_
"api_key": "string",
"user_id": str(active_user.id),
}
response = await client.post("api/v1/api_key/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/api_key/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
user1_api_key_id = response.json()["id"]
@@ -103,20 +103,20 @@ async def test_delete_api_key_route_unauthorized(client: AsyncClient, logged_in_
# Login as second user
login_data = {"username": "testuser2", "password": "testpassword2"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == status.HTTP_200_OK
user2_token = response.json()["access_token"]
user2_headers = {"Authorization": f"Bearer {user2_token}"}
# Try to delete first user's API key using second user's credentials
response = await client.delete(f"api/v1/api_key/{user1_api_key_id}", headers=user2_headers)
response = await client.delete(f"v1/api_key/{user1_api_key_id}", headers=user2_headers)
# Should fail with 400 error (API Key not found - we don't reveal it exists)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "API Key not found" in response.json()["detail"]
# Verify the first user's API key still exists by trying to delete it with correct credentials
response = await client.delete(f"api/v1/api_key/{user1_api_key_id}", headers=logged_in_headers)
response = await client.delete(f"v1/api_key/{user1_api_key_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
assert response.json()["detail"] == "API Key deleted"
@@ -17,7 +17,7 @@ async def test_optional_auth_returns_user_with_valid_bearer(client: AsyncClient,
# the endpoint proceeds (may fail on flow validation, but NOT on auth)
fake_flow_id = "00000000-0000-0000-0000-000000000099"
response = await client.post(
f"api/v1/build_public_tmp/{fake_flow_id}/flow",
f"v1/build_public_tmp/{fake_flow_id}/flow",
headers=logged_in_headers,
json={"inputs": None},
)
@@ -34,7 +34,7 @@ async def test_optional_auth_returns_none_without_credentials(
"""No credentials should still allow the endpoint to proceed (anonymous mode)."""
fake_flow_id = "00000000-0000-0000-0000-000000000099"
response = await client.post(
f"api/v1/build_public_tmp/{fake_flow_id}/flow",
f"v1/build_public_tmp/{fake_flow_id}/flow",
json={"inputs": None},
)
# Without auth AND without client_id cookie → 400 (no client_id)
@@ -48,7 +48,7 @@ async def test_optional_auth_returns_none_with_invalid_token(
"""Invalid Bearer token should not block the request — falls back to anonymous."""
fake_flow_id = "00000000-0000-0000-0000-000000000099"
response = await client.post(
f"api/v1/build_public_tmp/{fake_flow_id}/flow",
f"v1/build_public_tmp/{fake_flow_id}/flow",
headers={"Authorization": "Bearer invalid-expired-token"},
json={"inputs": None},
)
@@ -64,7 +64,7 @@ async def test_optional_auth_returns_none_with_invalid_api_key(
"""Invalid API key should not block the request — falls back to anonymous."""
fake_flow_id = "00000000-0000-0000-0000-000000000099"
response = await client.post(
f"api/v1/build_public_tmp/{fake_flow_id}/flow",
f"v1/build_public_tmp/{fake_flow_id}/flow",
headers={"x-api-key": "invalid-api-key"},
json={"inputs": None},
)
@@ -163,7 +163,7 @@ async def test_create_provider_account_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.post(
"api/v1/deployments/providers",
"v1/deployments/providers",
json={"provider_key": "watsonx-orchestrate", "name": "Test", "provider_data": {"foo": "bar"}},
headers=logged_in_headers,
)
@@ -181,7 +181,7 @@ async def test_update_provider_account_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.patch(
f"api/v1/deployments/providers/{uuid4()}",
f"v1/deployments/providers/{uuid4()}",
json={"name": "Test", "provider_data": {"foo": "bar"}},
headers=logged_in_headers,
)
@@ -198,7 +198,7 @@ async def test_update_provider_account_telemetry(
async def test_delete_provider_account_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.delete(f"api/v1/deployments/providers/{uuid4()}", headers=logged_in_headers)
response = await client.delete(f"v1/deployments/providers/{uuid4()}", headers=logged_in_headers)
assert response.status_code == status.HTTP_204_NO_CONTENT
mock_telemetry_service.log_package_deployment_provider.assert_awaited_once()
payload = mock_telemetry_service.log_package_deployment_provider.call_args[0][0]
@@ -213,7 +213,7 @@ async def test_create_deployment_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.post(
"api/v1/deployments",
"v1/deployments",
json={"provider_id": str(uuid4()), "name": "Test", "type": "agent", "provider_data": {}},
headers=logged_in_headers,
)
@@ -230,7 +230,7 @@ async def test_create_deployment_telemetry(
async def test_update_deployment_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.patch(f"api/v1/deployments/{uuid4()}", json={"name": "Test"}, headers=logged_in_headers)
response = await client.patch(f"v1/deployments/{uuid4()}", json={"name": "Test"}, headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
mock_telemetry_service.log_package_deployment.assert_awaited_once()
payload = mock_telemetry_service.log_package_deployment.call_args[0][0]
@@ -244,7 +244,7 @@ async def test_update_deployment_telemetry(
async def test_delete_deployment_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.delete(f"api/v1/deployments/{uuid4()}", headers=logged_in_headers)
response = await client.delete(f"v1/deployments/{uuid4()}", headers=logged_in_headers)
assert response.status_code == status.HTTP_204_NO_CONTENT
mock_telemetry_service.log_package_deployment.assert_awaited_once()
payload = mock_telemetry_service.log_package_deployment.call_args[0][0]
@@ -259,7 +259,7 @@ async def test_create_deployment_run_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.post(
f"api/v1/deployments/{uuid4()}/runs", json={"provider_data": {}}, headers=logged_in_headers
f"v1/deployments/{uuid4()}/runs", json={"provider_data": {}}, headers=logged_in_headers
)
assert response.status_code == status.HTTP_201_CREATED, response.json()
mock_telemetry_service.log_package_deployment_run.assert_awaited_once()
@@ -275,7 +275,7 @@ async def test_update_snapshot_telemetry(
client: AsyncClient, mock_telemetry_service, mock_adapter, mock_mapper, mock_db_crud, logged_in_headers
):
response = await client.patch(
"api/v1/deployments/snapshots/snap-1", json={"flow_version_id": str(uuid4())}, headers=logged_in_headers
"v1/deployments/snapshots/snap-1", json={"flow_version_id": str(uuid4())}, headers=logged_in_headers
)
assert response.status_code == status.HTTP_200_OK
mock_telemetry_service.log_package_deployment.assert_awaited_once()
@@ -292,7 +292,7 @@ async def test_create_provider_account_telemetry_error(
):
mock_adapter.verify_credentials.side_effect = ValueError("Invalid credentials")
response = await client.post(
"api/v1/deployments/providers",
"v1/deployments/providers",
json={"provider_key": "watsonx-orchestrate", "name": "Test", "provider_data": {"foo": "bar"}},
headers=logged_in_headers,
)
@@ -314,7 +314,7 @@ async def test_cross_route_smoke_exception_after_provider_set(
# Simulate an error during adapter.create after the provider has been set in the route
mock_adapter.create.side_effect = RuntimeError("Something went wrong")
response = await client.post(
"api/v1/deployments",
"v1/deployments",
json={"provider_id": str(uuid4()), "name": "Test", "type": "agent", "provider_data": {}},
headers=logged_in_headers,
)
+22 -22
View File
@@ -11,7 +11,7 @@ from lfx.custom.utils import build_custom_component_template
async def test_get_version(client: AsyncClient):
response = await client.get("api/v1/version")
response = await client.get("v1/version")
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -23,7 +23,7 @@ async def test_get_version(client: AsyncClient):
async def test_get_config_basic(client: AsyncClient, logged_in_headers: dict):
"""Test basic authenticated /config endpoint returns expected structure."""
response = await client.get("api/v1/config", headers=logged_in_headers)
response = await client.get("v1/config", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -46,7 +46,7 @@ async def test_update_component_outputs(client: AsyncClient, logged_in_headers:
field_value=True,
template={},
)
response = await client.post("api/v1/custom_component/update", json=request.model_dump(), headers=logged_in_headers)
response = await client.post("v1/custom_component/update", json=request.model_dump(), headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -82,7 +82,7 @@ async def test_update_component_model_name_options(client: AsyncClient, logged_i
)
# Make the request to update the component
response = await client.post("api/v1/custom_component/update", json=request.model_dump(), headers=logged_in_headers)
response = await client.post("v1/custom_component/update", json=request.model_dump(), headers=logged_in_headers)
result = response.json()
# Verify the response
@@ -123,7 +123,7 @@ class TestMetadataComponent(Component):
"""
request = CustomComponentRequest(code=component_code)
response = await client.post("api/v1/custom_component", json=request.model_dump(), headers=logged_in_headers)
response = await client.post("v1/custom_component", json=request.model_dump(), headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -169,10 +169,10 @@ class ConsistencyTestComponent(Component):
# Make two identical requests
request = CustomComponentRequest(code=component_code)
response1 = await client.post("api/v1/custom_component", json=request.model_dump(), headers=logged_in_headers)
response1 = await client.post("v1/custom_component", json=request.model_dump(), headers=logged_in_headers)
# result1 = response1.json()
response2 = await client.post("api/v1/custom_component", json=request.model_dump(), headers=logged_in_headers)
response2 = await client.post("v1/custom_component", json=request.model_dump(), headers=logged_in_headers)
# result2 = response2.json()
# Both requests should succeed
@@ -190,13 +190,13 @@ class ConsistencyTestComponent(Component):
async def test_get_config_without_authentication_returns_public_config(client: AsyncClient):
"""Test that /config returns public config when accessed without authentication."""
response = await client.get("api/v1/config")
response = await client.get("v1/config")
assert response.status_code == status.HTTP_200_OK
async def test_get_config_unauthenticated_returns_expected_fields(client: AsyncClient):
"""Test that unauthenticated /config response contains only public-safe fields."""
response = await client.get("api/v1/config")
response = await client.get("v1/config")
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -216,7 +216,7 @@ async def test_get_config_unauthenticated_returns_expected_fields(client: AsyncC
async def test_get_config_unauthenticated_does_not_expose_sensitive_fields(client: AsyncClient):
"""Test that unauthenticated /config response does not contain sensitive configuration fields."""
response = await client.get("api/v1/config")
response = await client.get("v1/config")
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -241,7 +241,7 @@ async def test_get_config_unauthenticated_does_not_expose_sensitive_fields(clien
async def test_get_config_unauthenticated_returns_correct_field_types(client: AsyncClient):
"""Test that unauthenticated /config response fields have correct types."""
response = await client.get("api/v1/config")
response = await client.get("v1/config")
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -267,7 +267,7 @@ async def test_get_config_returns_500_on_settings_error(client: AsyncClient, mon
# Patch get_settings_service at the module level
monkeypatch.setattr("flow.api.v1.endpoints.get_settings_service", raise_settings_error)
response = await client.get("api/v1/config")
response = await client.get("v1/config")
result = response.json()
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -276,7 +276,7 @@ async def test_get_config_returns_500_on_settings_error(client: AsyncClient, mon
async def test_get_config_authenticated_returns_full_config(client: AsyncClient, logged_in_headers: dict):
"""Test that authenticated /config returns full ConfigResponse with all settings."""
response = await client.get("api/v1/config", headers=logged_in_headers)
response = await client.get("v1/config", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -296,14 +296,14 @@ async def test_get_config_authenticated_returns_full_config(client: AsyncClient,
async def test_get_config_returns_mcp_base_url(client: AsyncClient, logged_in_headers: dict):
"""Test that /config includes mcp_base_url for both authenticated and unauthenticated responses."""
# Authenticated
response = await client.get("api/v1/config", headers=logged_in_headers)
response = await client.get("v1/config", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
assert "mcp_base_url" in result, "Authenticated response must contain 'mcp_base_url'"
assert isinstance(result["mcp_base_url"], str), "mcp_base_url must be a string"
# Unauthenticated
response = await client.get("api/v1/config")
response = await client.get("v1/config")
result = response.json()
assert response.status_code == status.HTTP_200_OK
assert "mcp_base_url" in result, "Public response must contain 'mcp_base_url'"
@@ -312,7 +312,7 @@ async def test_get_config_returns_mcp_base_url(client: AsyncClient, logged_in_he
async def test_get_config_mcp_base_url_defaults_to_empty(client: AsyncClient, logged_in_headers: dict):
"""Test that mcp_base_url defaults to empty string when FLOW_MCP_BASE_URL is not set."""
response = await client.get("api/v1/config", headers=logged_in_headers)
response = await client.get("v1/config", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
assert result["mcp_base_url"] == ""
@@ -325,20 +325,20 @@ async def test_get_config_mcp_base_url_from_settings(client: AsyncClient, logged
settings_service = get_settings_service()
monkeypatch.setattr(settings_service.settings, "mcp_base_url", "https://flow.example.com")
response = await client.get("api/v1/config", headers=logged_in_headers)
response = await client.get("v1/config", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
assert result["mcp_base_url"] == "https://flow.example.com"
async def test_deprecated_upload_rejects_unauthenticated(client: AsyncClient, flow):
"""Regression: the deprecated /api/v1/upload/{flow_id} must require auth.
"""Regression: the deprecated /v1/upload/{flow_id} must require auth.
Previously this endpoint accepted uploads without any credentials, letting
anonymous callers write arbitrary files into a flow's cache folder.
"""
response = await client.post(
f"api/v1/upload/{flow.id}",
f"v1/upload/{flow.id}",
files={"file": ("test.txt", b"test content")},
)
assert response.status_code != status.HTTP_201_CREATED, (
@@ -353,7 +353,7 @@ async def test_deprecated_upload_rejects_unauthenticated(client: AsyncClient, fl
async def test_deprecated_upload_authenticated_succeeds(client: AsyncClient, logged_in_headers: dict, flow):
"""The deprecated endpoint still works for the flow's owner."""
response = await client.post(
f"api/v1/upload/{flow.id}",
f"v1/upload/{flow.id}",
files={"file": ("test.txt", b"test content")},
headers=logged_in_headers,
)
@@ -371,7 +371,7 @@ async def test_deprecated_upload_enforces_max_file_size(
Without this guard, an authenticated user could still fill disk through
this route by uploading arbitrarily large files, bypassing the limit the
non-deprecated twin at /api/v1/files/upload/{flow_id} already enforces.
non-deprecated twin at /v1/files/upload/{flow_id} already enforces.
"""
from flow.services.deps import get_settings_service
@@ -380,7 +380,7 @@ async def test_deprecated_upload_enforces_max_file_size(
oversized = b"x" * (2 * 1024 * 1024) # 2 MB, exceeds the limit
response = await client.post(
f"api/v1/upload/{flow.id}",
f"v1/upload/{flow.id}",
files={"file": ("big.bin", oversized)},
headers=logged_in_headers,
)
+52 -52
View File
@@ -205,7 +205,7 @@ async def test_upload_file(files_client, files_created_api_key, files_flow):
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
@@ -224,7 +224,7 @@ async def test_download_file(files_client, files_created_api_key, files_flow):
# First upload a file
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
@@ -235,7 +235,7 @@ async def test_download_file(files_client, files_created_api_key, files_flow):
file_name = file_path.split("/")[-1]
# Then try to download it
response = await files_client.get(f"api/v1/files/download/{files_flow.id}/{file_name}", headers=headers)
response = await files_client.get(f"v1/files/download/{files_flow.id}/{file_name}", headers=headers)
assert response.status_code == 200
assert response.content == b"test content"
@@ -245,14 +245,14 @@ async def test_list_files(files_client, files_created_api_key, files_flow):
# First upload a file
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
assert response.status_code == 201
# Then list the files
response = await files_client.get(f"api/v1/files/list/{files_flow.id}", headers=headers)
response = await files_client.get(f"v1/files/list/{files_flow.id}", headers=headers)
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.json()}"
files = response.json()["files"]
assert len(files) == 1
@@ -262,7 +262,7 @@ async def test_list_files(files_client, files_created_api_key, files_flow):
async def test_delete_file(files_client, files_created_api_key, files_flow):
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.delete(f"api/v1/files/delete/{files_flow.id}/test.txt", headers=headers)
response = await files_client.delete(f"v1/files/delete/{files_flow.id}/test.txt", headers=headers)
assert response.status_code == 200
assert response.json() == {"message": "File test.txt deleted successfully"}
@@ -275,7 +275,7 @@ async def test_file_operations(files_client, files_created_api_key, files_flow):
# Step 1: Upload the file
response = await files_client.post(
f"api/v1/files/upload/{flow_id}",
f"v1/files/upload/{flow_id}",
files={"file": (file_name, file_content)},
headers=headers,
)
@@ -292,23 +292,23 @@ async def test_file_operations(files_client, files_created_api_key, files_flow):
full_file_name = response_json["file_path"].split("/")[-1]
# Step 2: List files in the folder
response = await files_client.get(f"api/v1/files/list/{files_flow.id}", headers=headers)
response = await files_client.get(f"v1/files/list/{files_flow.id}", headers=headers)
assert response.status_code == 200
assert full_file_name in response.json()["files"]
# Step 3: Download the file and verify its content
response = await files_client.get(f"api/v1/files/download/{files_flow.id}/{full_file_name}", headers=headers)
response = await files_client.get(f"v1/files/download/{files_flow.id}/{full_file_name}", headers=headers)
assert response.status_code == 200
assert response.content == file_content
assert response.headers["content-type"] == "application/octet-stream"
# Step 4: Delete the file
response = await files_client.delete(f"api/v1/files/delete/{files_flow.id}/{full_file_name}", headers=headers)
response = await files_client.delete(f"v1/files/delete/{files_flow.id}/{full_file_name}", headers=headers)
assert response.status_code == 200
assert response.json() == {"message": f"File {full_file_name} deleted successfully"}
# Verify that the file is indeed deleted
response = await files_client.get(f"api/v1/files/list/{files_flow.id}", headers=headers)
response = await files_client.get(f"v1/files/list/{files_flow.id}", headers=headers)
assert full_file_name not in response.json()["files"]
@@ -321,7 +321,7 @@ async def test_upload_file_size_limit(files_client, files_created_api_key, files
small_file = ("small_file.txt", small_content, "application/octet-stream")
headers["Content-Length"] = str(len(small_content))
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": small_file},
headers=headers,
)
@@ -333,7 +333,7 @@ async def test_upload_file_size_limit(files_client, files_created_api_key, files
bio = BytesIO(large_content)
headers["Content-Length"] = str(len(large_content))
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("large_file.txt", bio, "application/octet-stream")},
headers=headers,
)
@@ -396,7 +396,7 @@ async def test_list_profile_pictures(setup_profile_pictures, files_client): # n
files_client: HTTP client for making API requests
setup_profile_pictures: Fixture that sets up profile pictures directory
"""
response = await files_client.get("api/v1/files/profile_pictures/list")
response = await files_client.get("v1/files/profile_pictures/list")
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.json()}"
data = response.json()
@@ -422,7 +422,7 @@ async def test_download_profile_picture_space_rocket(setup_profile_pictures, fil
files_client: HTTP client for making API requests
setup_profile_pictures: Fixture that sets up profile pictures directory
"""
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
response = await files_client.get("v1/files/profile_pictures/Space/046-rocket.svg")
assert response.status_code == 200
# Verify content type
@@ -447,7 +447,7 @@ async def test_download_profile_picture_people(setup_profile_pictures, files_cli
setup_profile_pictures: Fixture that sets up profile pictures directory
"""
# List available people profile pictures first
list_response = await files_client.get("api/v1/files/profile_pictures/list")
list_response = await files_client.get("v1/files/profile_pictures/list")
assert list_response.status_code == 200
people_files = [f for f in list_response.json()["files"] if f.startswith("People/")]
@@ -459,7 +459,7 @@ async def test_download_profile_picture_people(setup_profile_pictures, files_cli
# Test downloading the first available people profile picture
first_people_file = people_files[0].replace("People/", "")
response = await files_client.get(f"api/v1/files/profile_pictures/People/{first_people_file}")
response = await files_client.get(f"v1/files/profile_pictures/People/{first_people_file}")
assert response.status_code == 200
# Verify content type
@@ -479,7 +479,7 @@ async def test_download_profile_picture_not_found(setup_profile_pictures, files_
files_client: HTTP client for making API requests
setup_profile_pictures: Fixture that sets up profile pictures directory
"""
response = await files_client.get("api/v1/files/profile_pictures/Space/nonexistent.svg")
response = await files_client.get("v1/files/profile_pictures/Space/nonexistent.svg")
assert response.status_code == 404
data = response.json()
@@ -501,7 +501,7 @@ async def test_profile_pictures_with_s3_storage(setup_profile_pictures, files_cl
monkeypatch.setenv("FLOW_STORAGE_TYPE", "s3")
# List should still work (from local filesystem)
response = await files_client.get("api/v1/files/profile_pictures/list")
response = await files_client.get("v1/files/profile_pictures/list")
assert response.status_code == 200
data = response.json()
# Should have profile pictures (app copies them during init)
@@ -509,7 +509,7 @@ async def test_profile_pictures_with_s3_storage(setup_profile_pictures, files_cl
assert "Space/046-rocket.svg" in data["files"], "Should have rocket profile picture"
# Download should still work (from local filesystem)
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
response = await files_client.get("v1/files/profile_pictures/Space/046-rocket.svg")
assert response.status_code == 200
assert b"<svg" in response.content
@@ -525,17 +525,17 @@ async def test_profile_pictures_different_file_types(setup_profile_pictures, fil
setup_profile_pictures: Fixture that sets up profile pictures directory
"""
# Test SVG content type (all real profile pictures are SVGs)
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
response = await files_client.get("v1/files/profile_pictures/Space/046-rocket.svg")
assert response.status_code == 200
assert "image/svg+xml" in response.headers["content-type"]
# Test with a people profile picture
list_response = await files_client.get("api/v1/files/profile_pictures/list")
list_response = await files_client.get("v1/files/profile_pictures/list")
people_files = [f for f in list_response.json()["files"] if f.startswith("People/")]
if people_files:
first_people_file = people_files[0].replace("People/", "")
response = await files_client.get(f"api/v1/files/profile_pictures/People/{first_people_file}")
response = await files_client.get(f"v1/files/profile_pictures/People/{first_people_file}")
assert response.status_code == 200
# All profile pictures should be SVGs
assert "image/svg+xml" in response.headers["content-type"]
@@ -577,7 +577,7 @@ async def test_download_profile_picture_fallback_to_package(empty_config_dir, fi
in config_dir, they should be served from the package's bundled directory.
"""
# The 046-rocket.svg should be found in the package's bundled directory
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
response = await files_client.get("v1/files/profile_pictures/Space/046-rocket.svg")
assert response.status_code == 200, (
f"Expected 200, got {response.status_code}. Fallback to package profile pictures should work."
)
@@ -597,7 +597,7 @@ async def test_list_profile_pictures_fallback_to_package(empty_config_dir, files
This tests the list fallback from PR #10758 - when config_dir has no
profile pictures, it should list from the package's bundled directory.
"""
response = await files_client.get("api/v1/files/profile_pictures/list")
response = await files_client.get("v1/files/profile_pictures/list")
assert response.status_code == 200
data = response.json()
@@ -619,7 +619,7 @@ async def test_download_profile_picture_not_found_in_both_locations(empty_config
This ensures the fallback logic correctly returns 404 when the file
is not found in either location.
"""
response = await files_client.get("api/v1/files/profile_pictures/Space/nonexistent-file-xyz.svg")
response = await files_client.get("v1/files/profile_pictures/Space/nonexistent-file-xyz.svg")
assert response.status_code == 404
data = response.json()
@@ -633,7 +633,7 @@ async def test_download_profile_picture_invalid_folder(empty_config_dir, files_c
Only 'People' and 'Space' folders are whitelisted for security.
Invalid folder names should be rejected with 400 Bad Request.
"""
response = await files_client.get("api/v1/files/profile_pictures/InvalidFolder/file.svg")
response = await files_client.get("v1/files/profile_pictures/InvalidFolder/file.svg")
assert response.status_code == 400
data = response.json()
@@ -655,7 +655,7 @@ async def test_download_profile_picture_config_dir_takes_precedence(setup_profil
space_dir.mkdir(parents=True, exist_ok=True)
(space_dir / "046-rocket.svg").write_bytes(custom_svg)
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
response = await files_client.get("v1/files/profile_pictures/Space/046-rocket.svg")
assert response.status_code == 200
# Should get the config_dir version, not the package version
@@ -676,7 +676,7 @@ async def test_list_profile_pictures_config_dir_takes_precedence(setup_profile_p
space_dir.mkdir(parents=True, exist_ok=True)
(space_dir / "custom-test-file.svg").write_bytes(b"<svg></svg>")
response = await files_client.get("api/v1/files/profile_pictures/list")
response = await files_client.get("v1/files/profile_pictures/list")
assert response.status_code == 200
data = response.json()
@@ -693,7 +693,7 @@ async def test_download_profile_picture_path_traversal_attempt(empty_config_dir,
using '../' should not work.
"""
# Try path traversal to access parent directories
response = await files_client.get("api/v1/files/profile_pictures/../../../etc/passwd")
response = await files_client.get("v1/files/profile_pictures/../../../etc/passwd")
# Should be 400 (invalid input), 404 (not found), or 422 (validation error)
assert response.status_code in [400, 404, 422, 500]
@@ -704,7 +704,7 @@ async def test_download_profile_picture_special_characters_in_filename(empty_con
Filenames with spaces or special characters should be handled properly.
"""
# Test with URL-encoded space (the real file has a space: "042-space shuttle.svg")
response = await files_client.get("api/v1/files/profile_pictures/Space/042-space%20shuttle.svg")
response = await files_client.get("v1/files/profile_pictures/Space/042-space%20shuttle.svg")
# Should work if the file exists with that name, or 404 if not
assert response.status_code in [200, 404]
@@ -714,7 +714,7 @@ async def test_list_profile_pictures_empty_response_format(empty_config_dir, fil
The response should always have the correct format: {"files": [...]}
"""
response = await files_client.get("api/v1/files/profile_pictures/list")
response = await files_client.get("v1/files/profile_pictures/list")
assert response.status_code == 200
data = response.json()
@@ -738,7 +738,7 @@ async def test_download_profile_picture_content_is_valid_svg(empty_config_dir, f
This ensures the fallback serves actual SVG content, not corrupted data.
"""
# Download the rocket from package fallback
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
response = await files_client.get("v1/files/profile_pictures/Space/046-rocket.svg")
assert response.status_code == 200
content = response.content
@@ -783,7 +783,7 @@ async def test_profile_pictures_fallback_with_partial_config_dir(partial_config_
"""
# For list: since we have at least one file in People, it should NOT fallback completely
# The current implementation only falls back if BOTH people AND space are empty
response = await files_client.get("api/v1/files/profile_pictures/list")
response = await files_client.get("v1/files/profile_pictures/list")
assert response.status_code == 200
data = response.json()
@@ -793,7 +793,7 @@ async def test_profile_pictures_fallback_with_partial_config_dir(partial_config_
assert "People/test-person.svg" in files
# For download: Space files should still work via fallback to package
response = await files_client.get("api/v1/files/profile_pictures/Space/046-rocket.svg")
response = await files_client.get("v1/files/profile_pictures/Space/046-rocket.svg")
assert response.status_code == 200, "Space files should fallback to package"
@@ -820,7 +820,7 @@ async def test_download_image_for_browser(files_client, files_created_api_key, f
)
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("test_image.png", png_content, "image/png")},
headers=headers,
)
@@ -832,7 +832,7 @@ async def test_download_image_for_browser(files_client, files_created_api_key, f
# Download the image - simulates browser <img> tag behavior
response = await files_client.get(
f"api/v1/files/images/{files_flow.id}/{file_name}",
f"v1/files/images/{files_flow.id}/{file_name}",
headers=headers,
)
@@ -856,7 +856,7 @@ async def test_download_image_returns_correct_content_type(files_client, files_c
)
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("test.png", png_content, "image/png")},
headers=headers,
)
@@ -866,7 +866,7 @@ async def test_download_image_returns_correct_content_type(files_client, files_c
file_name = file_path.split("/")[-1]
# Download image
response = await files_client.get(f"api/v1/files/images/{files_flow.id}/{file_name}", headers=headers)
response = await files_client.get(f"v1/files/images/{files_flow.id}/{file_name}", headers=headers)
assert response.status_code == 200
assert "image/png" in response.headers.get("content-type", "")
@@ -878,7 +878,7 @@ async def test_download_image_rejects_non_image_files(files_client, files_create
# Upload a text file
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("test.txt", b"not an image", "text/plain")},
headers=headers,
)
@@ -888,7 +888,7 @@ async def test_download_image_rejects_non_image_files(files_client, files_create
file_name = file_path.split("/")[-1]
# Try to download via /images endpoint (should fail)
response = await files_client.get(f"api/v1/files/images/{files_flow.id}/{file_name}", headers=headers)
response = await files_client.get(f"v1/files/images/{files_flow.id}/{file_name}", headers=headers)
# Should reject non-image content types
assert response.status_code == 500
@@ -906,7 +906,7 @@ async def test_download_image_requires_authentication(files_client, files_create
)
response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("auth_required.png", png_content, "image/png")},
headers=headers,
)
@@ -916,7 +916,7 @@ async def test_download_image_requires_authentication(files_client, files_create
file_name = file_path.split("/")[-1]
# No auth header: browser-like unauthenticated request must be rejected.
response = await files_client.get(f"api/v1/files/images/{files_flow.id}/{file_name}")
response = await files_client.get(f"v1/files/images/{files_flow.id}/{file_name}")
assert response.status_code == status.HTTP_403_FORBIDDEN
@@ -928,7 +928,7 @@ async def test_download_image_with_invalid_flow_id(files_client, files_created_a
fake_flow_id = uuid.uuid4()
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.get(f"api/v1/files/images/{fake_flow_id}/nonexistent.png", headers=headers)
response = await files_client.get(f"v1/files/images/{fake_flow_id}/nonexistent.png", headers=headers)
assert response.status_code == status.HTTP_404_NOT_FOUND
@@ -947,7 +947,7 @@ async def test_download_image_returns_404_for_other_users_flow(
)
upload_response = await files_client.post(
f"api/v1/files/upload/{files_flow.id}",
f"v1/files/upload/{files_flow.id}",
files={"file": ("browser_test.png", png_content, "image/png")},
headers=owner_headers,
)
@@ -958,7 +958,7 @@ async def test_download_image_returns_404_for_other_users_flow(
# Hide resource existence across tenants by returning 404 for non-owners.
download_response = await files_client.get(
f"api/v1/files/images/{files_flow.id}/{file_name}",
f"v1/files/images/{files_flow.id}/{file_name}",
headers=other_headers,
)
@@ -988,7 +988,7 @@ async def test_download_file_path_traversal_rejected(
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.get(
f"api/v1/files/download/{files_flow.id}/{malicious_filename}",
f"v1/files/download/{files_flow.id}/{malicious_filename}",
headers=headers,
)
@@ -1019,7 +1019,7 @@ async def test_download_file_forward_slash_traversal_blocked(
providing defense in depth before our validation layer is reached.
"""
headers = {"x-api-key": files_created_api_key.api_key}
url = f"api/v1/files/download/{files_flow.id}/{malicious_filename}"
url = f"v1/files/download/{files_flow.id}/{malicious_filename}"
response = await files_client.get(url, headers=headers)
# FastAPI returns 404 because the path with slashes doesn't match any route
@@ -1041,7 +1041,7 @@ async def test_download_file_forward_slash_traversal_blocked(
async def test_download_image_path_traversal_rejected(files_client, files_flow, malicious_filename):
"""Test that path traversal attempts are rejected on /images endpoint."""
response = await files_client.get(
f"api/v1/files/images/{files_flow.id}/{malicious_filename}",
f"v1/files/images/{files_flow.id}/{malicious_filename}",
)
assert response.status_code == 400, f"Path traversal should be rejected: {malicious_filename}"
@@ -1064,7 +1064,7 @@ async def test_download_image_path_traversal_rejected(files_client, files_flow,
)
async def test_download_image_forward_slash_traversal_blocked(files_client, files_flow, malicious_filename):
"""Test that forward slash path traversal is blocked by FastAPI routing on /images endpoint."""
url = f"api/v1/files/images/{files_flow.id}/{malicious_filename}"
url = f"v1/files/images/{files_flow.id}/{malicious_filename}"
response = await files_client.get(url)
# FastAPI returns 404 because the path with slashes doesn't match any route
@@ -1088,7 +1088,7 @@ async def test_delete_file_path_traversal_rejected(files_client, files_created_a
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.delete(
f"api/v1/files/delete/{files_flow.id}/{malicious_filename}",
f"v1/files/delete/{files_flow.id}/{malicious_filename}",
headers=headers,
)
@@ -1115,7 +1115,7 @@ async def test_delete_file_forward_slash_traversal_blocked(
):
"""Test that forward slash path traversal is blocked by FastAPI routing on /delete endpoint."""
headers = {"x-api-key": files_created_api_key.api_key}
url = f"api/v1/files/delete/{files_flow.id}/{malicious_filename}"
url = f"v1/files/delete/{files_flow.id}/{malicious_filename}"
response = await files_client.delete(url, headers=headers)
# FastAPI returns 404 because the path with slashes doesn't match any route
@@ -5,7 +5,7 @@ from starlette import status
async def _create_flow(client: AsyncClient, headers: dict) -> str:
"""Create a minimal flow and return its id."""
response = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "event-test-flow", "data": {}},
headers=headers,
)
@@ -17,7 +17,7 @@ async def test_create_and_get_flow_events(client: AsyncClient, logged_in_headers
flow_id = await _create_flow(client, logged_in_headers)
response = await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "component_added", "summary": "Added OpenAI"},
headers=logged_in_headers,
)
@@ -28,7 +28,7 @@ async def test_create_and_get_flow_events(client: AsyncClient, logged_in_headers
assert "timestamp" in event
response = await client.get(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
params={"since": 0.0},
headers=logged_in_headers,
)
@@ -43,20 +43,20 @@ async def test_get_events_cursor_based(client: AsyncClient, logged_in_headers):
flow_id = await _create_flow(client, logged_in_headers)
r1 = await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "component_added", "summary": "First"},
headers=logged_in_headers,
)
first_ts = r1.json()["timestamp"]
await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "connection_added", "summary": "Second"},
headers=logged_in_headers,
)
response = await client.get(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
params={"since": first_ts},
headers=logged_in_headers,
)
@@ -69,18 +69,18 @@ async def test_settled_on_flow_settled_event(client: AsyncClient, logged_in_head
flow_id = await _create_flow(client, logged_in_headers)
await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "component_added", "summary": "Added"},
headers=logged_in_headers,
)
await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "flow_settled", "summary": "Done"},
headers=logged_in_headers,
)
response = await client.get(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
params={"since": 0.0},
headers=logged_in_headers,
)
@@ -93,18 +93,18 @@ async def test_full_event_lifecycle(client: AsyncClient, logged_in_headers):
flow_id = await _create_flow(client, logged_in_headers)
await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "component_added", "summary": "Added OpenAI"},
headers=logged_in_headers,
)
await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "connection_added", "summary": "Connected to Chat Output"},
headers=logged_in_headers,
)
response = await client.get(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
params={"since": 0.0},
headers=logged_in_headers,
)
@@ -113,13 +113,13 @@ async def test_full_event_lifecycle(client: AsyncClient, logged_in_headers):
assert data["settled"] is False
await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "flow_settled", "summary": "Built a RAG pipeline"},
headers=logged_in_headers,
)
response = await client.get(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
params={"since": 0.0},
headers=logged_in_headers,
)
@@ -133,14 +133,14 @@ async def test_nonexistent_flow_returns_404(client: AsyncClient, logged_in_heade
fake_id = "00000000-0000-0000-0000-000000000000"
response = await client.get(
f"api/v1/flows/{fake_id}/events",
f"v1/flows/{fake_id}/events",
params={"since": 0.0},
headers=logged_in_headers,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.post(
f"api/v1/flows/{fake_id}/events",
f"v1/flows/{fake_id}/events",
json={"type": "component_added", "summary": "Should fail"},
headers=logged_in_headers,
)
@@ -151,7 +151,7 @@ async def test_invalid_event_type_returns_422(client: AsyncClient, logged_in_hea
flow_id = await _create_flow(client, logged_in_headers)
response = await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "invalid_type", "summary": "bad event"},
headers=logged_in_headers,
)
@@ -162,7 +162,7 @@ async def test_missing_event_type_returns_422(client: AsyncClient, logged_in_hea
flow_id = await _create_flow(client, logged_in_headers)
response = await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"summary": "no type field"},
headers=logged_in_headers,
)
@@ -173,7 +173,7 @@ async def test_summary_max_length_returns_422(client: AsyncClient, logged_in_hea
flow_id = await _create_flow(client, logged_in_headers)
response = await client.post(
f"api/v1/flows/{flow_id}/events",
f"v1/flows/{flow_id}/events",
json={"type": "component_added", "summary": "x" * 501},
headers=logged_in_headers,
)
@@ -35,7 +35,7 @@ async def test_create_flow_with_nonexistent_folder_id_assigns_default_folder(
"folder_id": non_existent_folder_id,
}
response = await client.post("api/v1/flows/", json=flow_data, headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
result = response.json()
@@ -60,7 +60,7 @@ async def test_create_flow_without_folder_id_assigns_default_folder(
"data": {},
}
response = await client.post("api/v1/flows/", json=flow_data, headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
result = response.json()
@@ -103,7 +103,7 @@ async def test_create_flow_after_all_folders_deleted_creates_default_folder(
"data": {},
}
response = await client.post("api/v1/flows/", json=flow_data, headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
result = response.json()
@@ -134,7 +134,7 @@ async def test_update_flow_with_nonexistent_folder_id_assigns_default_folder(
"name": "Flow to Update",
"data": {},
}
flow_response = await client.post("api/v1/flows/", json=flow_data, headers=logged_in_headers)
flow_response = await client.post("v1/flows/", json=flow_data, headers=logged_in_headers)
assert flow_response.status_code == status.HTTP_201_CREATED
flow_id = flow_response.json()["id"]
@@ -145,7 +145,7 @@ async def test_update_flow_with_nonexistent_folder_id_assigns_default_folder(
"folder_id": non_existent_folder_id, # This folder doesn't exist
}
update_response = await client.patch(f"api/v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
update_response = await client.patch(f"v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
assert update_response.status_code == status.HTTP_200_OK
result = update_response.json()
@@ -171,7 +171,7 @@ async def test_update_flow_without_folder_id_keeps_existing_folder(client: Async
"name": "Flow to Update",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=flow_data, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=flow_data, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
original_folder_id = create_response.json()["folder_id"]
@@ -181,7 +181,7 @@ async def test_update_flow_without_folder_id_keeps_existing_folder(client: Async
"name": "Updated Flow Name",
}
update_response = await client.patch(f"api/v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
update_response = await client.patch(f"v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
assert update_response.status_code == status.HTTP_200_OK
result = update_response.json()
@@ -211,7 +211,7 @@ async def test_upload_flow_with_nonexistent_folder_id_assigns_default(
file_content = json.dumps(flow_data)
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("flow.json", file_content, "application/json")},
headers=logged_in_headers,
)
@@ -247,13 +247,13 @@ async def test_flow_created_is_retrievable_in_folder(client: AsyncClient, logged
"name": "Retrievable Flow",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=flow_data, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=flow_data, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
folder_id = create_response.json()["folder_id"]
# List flows in the folder
response = await client.get(f"api/v1/folders/{folder_id}", headers=logged_in_headers)
response = await client.get(f"v1/folders/{folder_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
# Check if the flow is in the folder's flows list
@@ -285,7 +285,7 @@ async def test_upsert_flow_with_nonexistent_folder_id_on_create(client: AsyncCli
"folder_id": non_existent_folder_id,
}
response = await client.put(f"api/v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
# The request should be rejected with 400 Bad Request since folder doesn't exist
# This is the expected behavior based on the existing test_upsert_flow_returns_400_for_invalid_folder_id
@@ -30,7 +30,7 @@ async def _create_flow(client: AsyncClient, headers: dict, name: str = "version-
"data": {"nodes": [], "edges": []},
"is_component": False,
}
resp = await client.post("api/v1/flows/", json=payload, headers=headers)
resp = await client.post("v1/flows/", json=payload, headers=headers)
assert resp.status_code == status.HTTP_201_CREATED
return resp.json()
@@ -38,13 +38,13 @@ async def _create_flow(client: AsyncClient, headers: dict, name: str = "version-
async def _create_snapshot(client: AsyncClient, headers: dict, flow_id: str, description: str | None = None) -> dict:
"""POST a snapshot and return the JSON response."""
body = {"description": description} if description else {}
resp = await client.post(f"api/v1/flows/{flow_id}/versions/", json=body, headers=headers)
resp = await client.post(f"v1/flows/{flow_id}/versions/", json=body, headers=headers)
assert resp.status_code == status.HTTP_201_CREATED
return resp.json()
async def _list_versions(client: AsyncClient, headers: dict, flow_id: str, *, params: dict | None = None) -> list[dict]:
resp = await client.get(f"api/v1/flows/{flow_id}/versions/", headers=headers, params=params)
resp = await client.get(f"v1/flows/{flow_id}/versions/", headers=headers, params=params)
assert resp.status_code == status.HTTP_200_OK
body = resp.json()
assert "entries" in body
@@ -54,7 +54,7 @@ async def _list_versions(client: AsyncClient, headers: dict, flow_id: str, *, pa
async def _patch_flow_data(client: AsyncClient, headers: dict, flow_id: str, data: dict) -> dict:
"""PATCH the flow to change its data (simulates canvas auto-save)."""
resp = await client.patch(f"api/v1/flows/{flow_id}", json={"data": data}, headers=headers)
resp = await client.patch(f"v1/flows/{flow_id}", json={"data": data}, headers=headers)
assert resp.status_code == status.HTTP_200_OK
return resp.json()
@@ -107,7 +107,7 @@ async def test_list_versions_response_includes_max_entries(client: AsyncClient,
flow = await _create_flow(client, logged_in_headers)
await _create_snapshot(client, logged_in_headers, flow["id"])
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
body = resp.json()
@@ -147,7 +147,7 @@ async def test_get_single_version_entry_includes_data(client: AsyncClient, logge
flow = await _create_flow(client, logged_in_headers)
snap = await _create_snapshot(client, logged_in_headers, flow["id"])
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
full = resp.json()
@@ -171,8 +171,8 @@ async def test_snapshot_captures_current_flow_data(client: AsyncClient, logged_i
s2 = await _create_snapshot(client, logged_in_headers, flow["id"], description="with node")
# Fetch full snapshots and compare
r1 = await client.get(f"api/v1/flows/{flow['id']}/versions/{s1['id']}", headers=logged_in_headers)
r2 = await client.get(f"api/v1/flows/{flow['id']}/versions/{s2['id']}", headers=logged_in_headers)
r1 = await client.get(f"v1/flows/{flow['id']}/versions/{s1['id']}", headers=logged_in_headers)
r2 = await client.get(f"v1/flows/{flow['id']}/versions/{s2['id']}", headers=logged_in_headers)
assert r1.json()["data"] == {"nodes": [], "edges": []}
assert r2.json()["data"] == new_data
@@ -182,7 +182,7 @@ async def test_delete_version_entry(client: AsyncClient, logged_in_headers):
flow = await _create_flow(client, logged_in_headers)
snap = await _create_snapshot(client, logged_in_headers, flow["id"])
resp = await client.delete(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
resp = await client.delete(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_204_NO_CONTENT
entries = await _list_versions(client, logged_in_headers, flow["id"])
@@ -207,7 +207,7 @@ async def test_activate_version_overwrites_flow_data(client: AsyncClient, logged
await _patch_flow_data(client, logged_in_headers, flow["id"], modified_data)
# Activate the old snapshot
resp = await client.post(f"api/v1/flows/{flow['id']}/versions/{snap['id']}/activate", headers=logged_in_headers)
resp = await client.post(f"v1/flows/{flow['id']}/versions/{snap['id']}/activate", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
updated_flow = resp.json()
@@ -223,7 +223,7 @@ async def test_activate_creates_auto_snapshot(client: AsyncClient, logged_in_hea
await _patch_flow_data(client, logged_in_headers, flow["id"], {"nodes": [{"id": "x"}], "edges": []})
# Activate — this should create an auto-snapshot first
await client.post(f"api/v1/flows/{flow['id']}/versions/{snap['id']}/activate", headers=logged_in_headers)
await client.post(f"v1/flows/{flow['id']}/versions/{snap['id']}/activate", headers=logged_in_headers)
entries = await _list_versions(client, logged_in_headers, flow["id"])
# 1 manual snapshot + 1 auto-snapshot = 2
@@ -243,7 +243,7 @@ async def test_activate_skips_auto_snapshot_when_save_draft_false(client: AsyncC
# Activate with save_draft=false
resp = await client.post(
f"api/v1/flows/{flow['id']}/versions/{snap['id']}/activate",
f"v1/flows/{flow['id']}/versions/{snap['id']}/activate",
params={"save_draft": False},
headers=logged_in_headers,
)
@@ -265,11 +265,11 @@ async def test_deleting_flow_cascades_to_versions(client: AsyncClient, logged_in
await _create_snapshot(client, logged_in_headers, flow["id"])
# Delete the flow
resp = await client.delete(f"api/v1/flows/{flow['id']}", headers=logged_in_headers)
resp = await client.delete(f"v1/flows/{flow['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
# Versions endpoint for the deleted flow should 404
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
@@ -280,28 +280,28 @@ async def test_deleting_flow_cascades_to_versions(client: AsyncClient, logged_in
async def test_get_versions_for_nonexistent_flow(client: AsyncClient, logged_in_headers):
fake_id = "00000000-0000-0000-0000-000000000000"
resp = await client.get(f"api/v1/flows/{fake_id}/versions/", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{fake_id}/versions/", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
async def test_get_nonexistent_version_entry(client: AsyncClient, logged_in_headers):
flow = await _create_flow(client, logged_in_headers)
fake_id = "00000000-0000-0000-0000-000000000000"
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/{fake_id}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/{fake_id}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
async def test_activate_nonexistent_version_entry(client: AsyncClient, logged_in_headers):
flow = await _create_flow(client, logged_in_headers)
fake_id = "00000000-0000-0000-0000-000000000000"
resp = await client.post(f"api/v1/flows/{flow['id']}/versions/{fake_id}/activate", headers=logged_in_headers)
resp = await client.post(f"v1/flows/{flow['id']}/versions/{fake_id}/activate", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
async def test_delete_nonexistent_version_entry(client: AsyncClient, logged_in_headers):
flow = await _create_flow(client, logged_in_headers)
fake_id = "00000000-0000-0000-0000-000000000000"
resp = await client.delete(f"api/v1/flows/{flow['id']}/versions/{fake_id}", headers=logged_in_headers)
resp = await client.delete(f"v1/flows/{flow['id']}/versions/{fake_id}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
@@ -312,7 +312,7 @@ async def test_activate_entry_belonging_to_different_flow(client: AsyncClient, l
snap_a = await _create_snapshot(client, logged_in_headers, flow_a["id"])
# Try to activate snap_a on flow_b
resp = await client.post(f"api/v1/flows/{flow_b['id']}/versions/{snap_a['id']}/activate", headers=logged_in_headers)
resp = await client.post(f"v1/flows/{flow_b['id']}/versions/{snap_a['id']}/activate", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
@@ -322,13 +322,13 @@ async def test_delete_entry_belonging_to_different_flow(client: AsyncClient, log
flow_b = await _create_flow(client, logged_in_headers, name="flow-b-del")
snap_a = await _create_snapshot(client, logged_in_headers, flow_a["id"])
resp = await client.delete(f"api/v1/flows/{flow_b['id']}/versions/{snap_a['id']}", headers=logged_in_headers)
resp = await client.delete(f"v1/flows/{flow_b['id']}/versions/{snap_a['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
async def test_unauthenticated_request_is_rejected(client: AsyncClient):
fake_id = "00000000-0000-0000-0000-000000000000"
resp = await client.get(f"api/v1/flows/{fake_id}/versions/")
resp = await client.get(f"v1/flows/{fake_id}/versions/")
assert resp.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
@@ -346,7 +346,7 @@ async def test_list_versions_pagination(client: AsyncClient, logged_in_headers):
# Fetch with limit=2
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"limit": 2, "offset": 0},
headers=logged_in_headers,
)
@@ -358,7 +358,7 @@ async def test_list_versions_pagination(client: AsyncClient, logged_in_headers):
# Second page
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"limit": 2, "offset": 2},
headers=logged_in_headers,
)
@@ -396,7 +396,7 @@ async def test_full_lifecycle(client: AsyncClient, logged_in_headers):
await _patch_flow_data(client, logged_in_headers, flow_id, data_v3)
# 6. Activate v1 — should auto-snapshot current state, then revert to v1's data
resp = await client.post(f"api/v1/flows/{flow_id}/versions/{v1['id']}/activate", headers=logged_in_headers)
resp = await client.post(f"v1/flows/{flow_id}/versions/{v1['id']}/activate", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
activated_flow = resp.json()
assert activated_flow["data"] == initial_data
@@ -407,11 +407,11 @@ async def test_full_lifecycle(client: AsyncClient, logged_in_headers):
# 8. The auto-snapshot should contain data_v3
auto = next(e for e in entries if "Auto-saved" in (e.get("description") or ""))
auto_full = await client.get(f"api/v1/flows/{flow_id}/versions/{auto['id']}", headers=logged_in_headers)
auto_full = await client.get(f"v1/flows/{flow_id}/versions/{auto['id']}", headers=logged_in_headers)
assert auto_full.json()["data"] == data_v3
# 9. Activate v2
resp2 = await client.post(f"api/v1/flows/{flow_id}/versions/{v2['id']}/activate", headers=logged_in_headers)
resp2 = await client.post(f"v1/flows/{flow_id}/versions/{v2['id']}/activate", headers=logged_in_headers)
assert resp2.status_code == status.HTTP_200_OK
assert resp2.json()["data"] == data_v2
@@ -433,7 +433,7 @@ async def test_snapshot_and_activate_with_complex_flow_data(client: AsyncClient,
"data": complex_data,
"is_component": False,
}
resp = await client.post("api/v1/flows/", json=payload, headers=logged_in_headers)
resp = await client.post("v1/flows/", json=payload, headers=logged_in_headers)
assert resp.status_code == status.HTTP_201_CREATED
flow = resp.json()
flow_id = flow["id"]
@@ -450,12 +450,12 @@ async def test_snapshot_and_activate_with_complex_flow_data(client: AsyncClient,
await _patch_flow_data(client, logged_in_headers, flow_id, minimal_data)
# Verify flow was actually changed
get_resp = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
get_resp = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
assert len(get_resp.json()["data"]["nodes"]) == 1
# Activate the complex snapshot — should restore the full graph
activate_resp = await client.post(
f"api/v1/flows/{flow_id}/versions/{snap['id']}/activate", headers=logged_in_headers
f"v1/flows/{flow_id}/versions/{snap['id']}/activate", headers=logged_in_headers
)
assert activate_resp.status_code == status.HTTP_200_OK
restored = activate_resp.json()
@@ -479,12 +479,12 @@ async def test_snapshot_preserves_full_node_metadata(client: AsyncClient, logged
"data": complex_data,
"is_component": False,
}
resp = await client.post("api/v1/flows/", json=payload, headers=logged_in_headers)
resp = await client.post("v1/flows/", json=payload, headers=logged_in_headers)
flow = resp.json()
snap = await _create_snapshot(client, logged_in_headers, flow["id"])
# Fetch the full snapshot and compare node-by-node
full = await client.get(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
full = await client.get(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
snapshot_data = full.json()["data"]
assert len(snapshot_data["nodes"]) == len(complex_data["nodes"])
@@ -564,7 +564,7 @@ async def test_pruning_deletes_oldest_by_data_content(client: AsyncClient, logge
# Fetch full data for each survivor and confirm it matches the expected snapshot data.
for entry, expected_idx in zip(entries, [2, 1], strict=False):
resp = await client.get(
f"api/v1/flows/{flow_id}/versions/{entry['id']}",
f"v1/flows/{flow_id}/versions/{entry['id']}",
headers=logged_in_headers,
)
assert resp.status_code == 200
@@ -631,7 +631,7 @@ async def test_activate_version_with_null_data(client: AsyncClient, logged_in_he
entry.data = None
session.add(entry)
resp = await client.post(f"api/v1/flows/{flow_id}/versions/{snap['id']}/activate", headers=logged_in_headers)
resp = await client.post(f"v1/flows/{flow_id}/versions/{snap['id']}/activate", headers=logged_in_headers)
assert resp.status_code == status.HTTP_400_BAD_REQUEST
assert "no data" in resp.json()["detail"].lower()
@@ -648,7 +648,7 @@ async def test_version_entry_scoped_to_flow(client: AsyncClient, logged_in_heade
snap_a = await _create_snapshot(client, logged_in_headers, flow_a["id"])
# Try to access snap_a through flow_b's endpoint
resp = await client.get(f"api/v1/flows/{flow_b['id']}/versions/{snap_a['id']}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow_b['id']}/versions/{snap_a['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_404_NOT_FOUND
@@ -664,7 +664,7 @@ async def test_list_versions_limit_of_one(client: AsyncClient, logged_in_headers
await _create_snapshot(client, logged_in_headers, flow["id"], description=f"s-{i}")
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"limit": 1},
headers=logged_in_headers,
)
@@ -677,7 +677,7 @@ async def test_list_versions_invalid_limit_zero(client: AsyncClient, logged_in_h
"""Limit=0 should be rejected by validation (ge=1)."""
flow = await _create_flow(client, logged_in_headers)
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"limit": 0},
headers=logged_in_headers,
)
@@ -688,7 +688,7 @@ async def test_list_versions_limit_exceeds_max(client: AsyncClient, logged_in_he
"""Limit > 100 should be rejected by validation (le=100)."""
flow = await _create_flow(client, logged_in_headers)
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"limit": 101},
headers=logged_in_headers,
)
@@ -727,7 +727,7 @@ async def test_get_single_entry_strips_api_keys(client: AsyncClient, logged_in_h
"data": api_key_data,
"is_component": False,
}
resp = await client.post("api/v1/flows/", json=payload, headers=logged_in_headers)
resp = await client.post("v1/flows/", json=payload, headers=logged_in_headers)
assert resp.status_code == status.HTTP_201_CREATED
flow = resp.json()
@@ -735,7 +735,7 @@ async def test_get_single_entry_strips_api_keys(client: AsyncClient, logged_in_h
# Fetch the single entry — API key should be stripped (value set to None)
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/{snap['id']}",
f"v1/flows/{flow['id']}/versions/{snap['id']}",
headers=logged_in_headers,
)
assert resp.status_code == status.HTTP_200_OK
@@ -750,7 +750,7 @@ async def test_create_snapshot_rejects_long_description(client: AsyncClient, log
long_description = "x" * 501
resp = await client.post(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
json={"description": long_description},
headers=logged_in_headers,
)
@@ -782,7 +782,7 @@ async def test_activate_with_deeply_nested_data(client: AsyncClient, logged_in_h
"data": deep_data,
"is_component": False,
}
resp = await client.post("api/v1/flows/", json=payload, headers=logged_in_headers)
resp = await client.post("v1/flows/", json=payload, headers=logged_in_headers)
assert resp.status_code == status.HTTP_201_CREATED
flow = resp.json()
flow_id = flow["id"]
@@ -795,7 +795,7 @@ async def test_activate_with_deeply_nested_data(client: AsyncClient, logged_in_h
# Activate the old version — triggers deepcopy of both current and target data
resp = await client.post(
f"api/v1/flows/{flow_id}/versions/{snap['id']}/activate",
f"v1/flows/{flow_id}/versions/{snap['id']}/activate",
headers=logged_in_headers,
)
assert resp.status_code == status.HTTP_200_OK
@@ -837,7 +837,7 @@ async def test_rapid_snapshots_with_low_limit(client: AsyncClient, logged_in_hea
# Create 5 snapshots in quick succession
for i in range(5):
resp = await client.post(
f"api/v1/flows/{flow_id}/versions/",
f"v1/flows/{flow_id}/versions/",
json={"description": f"rapid-{i}"},
headers=logged_in_headers,
)
@@ -1064,7 +1064,7 @@ async def test_list_versions_hides_deployment_state_when_feature_disabled(
deployment_id=deployment.id,
)
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
assert "is_deployed" not in resp.json()["entries"][0]
@@ -1080,7 +1080,7 @@ async def test_list_versions_rejects_provider_id_when_feature_disabled(
await _create_snapshot(client, logged_in_headers, flow["id"])
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"deployment_provider_id": str(uuid4())},
headers=logged_in_headers,
)
@@ -1097,7 +1097,7 @@ async def test_list_versions_rejects_unknown_provider_id(client: AsyncClient, lo
await _create_snapshot(client, logged_in_headers, flow["id"])
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"deployment_provider_id": str(uuid4())},
headers=logged_in_headers,
)
@@ -1128,7 +1128,7 @@ async def test_list_versions_rejects_foreign_provider_id(client: AsyncClient, lo
foreign_provider_id = str(foreign_provider.id)
resp = await client.get(
f"api/v1/flows/{flow['id']}/versions/",
f"v1/flows/{flow['id']}/versions/",
params={"deployment_provider_id": foreign_provider_id},
headers=logged_in_headers,
)
@@ -1252,7 +1252,7 @@ async def test_get_single_version_reports_unknown_deployment_status_when_unattac
flow = await _create_flow(client, logged_in_headers)
snap = await _create_snapshot(client, logged_in_headers, flow["id"])
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["is_deployed"] is None
@@ -1290,7 +1290,7 @@ async def test_get_single_version_reports_unknown_deployment_status_when_attache
deployment_id=deployment.id,
)
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["is_deployed"] is None
@@ -1328,7 +1328,7 @@ async def test_get_single_version_reports_unknown_deployment_status_when_feature
deployment_id=deployment.id,
)
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["is_deployed"] is None
@@ -1344,7 +1344,7 @@ async def test_get_single_version_sync_failure_does_not_block_response(
snap = await _create_snapshot(client, logged_in_headers, flow["id"])
with patch(SYNC_MODULE, new_callable=AsyncMock, side_effect=RuntimeError("provider down")) as mock_sync:
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["id"] == snap["id"]
assert resp.json()["is_deployed"] is None
@@ -1389,7 +1389,7 @@ async def test_get_single_version_does_not_trigger_sync_prune(client: AsyncClien
)
with patch(SYNC_MODULE, new_callable=AsyncMock) as mock_sync:
resp = await client.get(f"api/v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
resp = await client.get(f"v1/flows/{flow['id']}/versions/{snap['id']}", headers=logged_in_headers)
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["is_deployed"] is None
mock_sync.assert_not_awaited()
+75 -75
View File
@@ -78,7 +78,7 @@ async def test_create_flow(client: AsyncClient, logged_in_headers):
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fs_path": flow_filename,
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -108,7 +108,7 @@ async def test_read_flows(client: AsyncClient, logged_in_headers):
"page": 1,
"size": 50,
}
response = await client.get("api/v1/flows/", params=params, headers=logged_in_headers)
response = await client.get("v1/flows/", params=params, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -118,7 +118,7 @@ async def test_read_flows(client: AsyncClient, logged_in_headers):
async def test_get_flows_with_malformed_bearer_token_returns_401(client: AsyncClient):
"""CT-010: GET /v1/flows with malformed Bearer token must return 401 Unauthorized."""
headers = {"Authorization": "Bearer invalid.token.here"}
response = await client.get("api/v1/flows/", headers=headers)
response = await client.get("v1/flows/", headers=headers)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
data = response.json()
assert "detail" in data
@@ -139,9 +139,9 @@ async def test_read_flow(client: AsyncClient, logged_in_headers):
"tags": ["string"],
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}
response_ = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response_ = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
id_ = response_.json()["id"]
response = await client.get(f"api/v1/flows/{id_}", headers=logged_in_headers)
response = await client.get(f"v1/flows/{id_}", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -178,7 +178,7 @@ async def test_update_flow(client: AsyncClient, logged_in_headers):
"folder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
}
basic_case["name"] = name
response_ = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response_ = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
id_ = response_.json()["id"]
# Use relative path - absolute paths outside allowed directory are rejected
@@ -186,7 +186,7 @@ async def test_update_flow(client: AsyncClient, logged_in_headers):
basic_case["name"] = updated_name
basic_case["fs_path"] = flow_filename
response = await client.patch(f"api/v1/flows/{id_}", json=basic_case, headers=logged_in_headers)
response = await client.patch(f"v1/flows/{id_}", json=basic_case, headers=logged_in_headers)
result = response.json()
assert isinstance(result, dict), "The result must be a dictionary"
@@ -214,12 +214,12 @@ async def test_patch_flow_keeps_existing_endpoint_when_not_provided(client: Asyn
"endpoint_name": "keep_patch_endpoint",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=initial_flow, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=initial_flow, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"name": "patch_endpoint_flow_updated"},
headers=logged_in_headers,
)
@@ -235,12 +235,12 @@ async def test_patch_flow_allows_clearing_endpoint_with_null(client: AsyncClient
"endpoint_name": "clear_patch_endpoint",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=initial_flow, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=initial_flow, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"endpoint_name": None},
headers=logged_in_headers,
)
@@ -252,7 +252,7 @@ async def test_patch_flow_allows_clearing_endpoint_with_null(client: AsyncClient
async def test_patch_flow_updates_access_and_action_fields(client: AsyncClient, logged_in_headers):
"""PATCH should persist public-sharing and MCP action metadata fields."""
create_response = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "patch_access_type_flow", "data": {}},
headers=logged_in_headers,
)
@@ -260,7 +260,7 @@ async def test_patch_flow_updates_access_and_action_fields(client: AsyncClient,
flow_id = create_response.json()["id"]
response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={
"access_type": "PUBLIC",
"action_name": "shared_action",
@@ -295,7 +295,7 @@ async def test_create_flows(client: AsyncClient, logged_in_headers):
case["endpoint_name"] = f"string_{i}"
cases.append(case)
response = await client.post("api/v1/flows/batch/", json={"flows": cases}, headers=logged_in_headers)
response = await client.post("v1/flows/batch/", json={"flows": cases}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -305,7 +305,7 @@ async def test_create_flows(client: AsyncClient, logged_in_headers):
async def test_create_flows_with_explicit_folder(client: AsyncClient, logged_in_headers):
project_response = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "batch-folder-target", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -331,7 +331,7 @@ async def test_create_flows_with_explicit_folder(client: AsyncClient, logged_in_
case["endpoint_name"] = f"string_folder_{i}"
cases.append(case)
response = await client.post("api/v1/flows/batch/", json={"flows": cases}, headers=logged_in_headers)
response = await client.post("v1/flows/batch/", json={"flows": cases}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -341,7 +341,7 @@ async def test_create_flows_with_explicit_folder(client: AsyncClient, logged_in_
async def test_read_basic_examples(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/flows/basic_examples/", headers=logged_in_headers)
response = await client.get("v1/flows/basic_examples/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -373,7 +373,7 @@ async def test_read_flows_user_isolation(client: AsyncClient, logged_in_headers,
# Login as the other user to get headers
login_data = {"username": "other_test_user", "password": "testpassword"} # pragma: allowlist secret
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
tokens = response.json()
other_user_headers = {"Authorization": f"Bearer {tokens['access_token']}"}
@@ -423,13 +423,13 @@ async def test_read_flows_user_isolation(client: AsyncClient, logged_in_headers,
}
# Create flows using the appropriate user headers
response1 = await client.post("api/v1/flows/", json=flow_user1_1, headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=flow_user1_1, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED
response2 = await client.post("api/v1/flows/", json=flow_user1_2, headers=logged_in_headers)
response2 = await client.post("v1/flows/", json=flow_user1_2, headers=logged_in_headers)
assert response2.status_code == status.HTTP_201_CREATED
response3 = await client.post("api/v1/flows/", json=flow_user2_1, headers=other_user_headers)
response3 = await client.post("v1/flows/", json=flow_user2_1, headers=other_user_headers)
assert response3.status_code == status.HTTP_201_CREATED
# Test read_flows for user 1 - should only return user 1's flows
@@ -442,7 +442,7 @@ async def test_read_flows_user_isolation(client: AsyncClient, logged_in_headers,
"size": 50,
}
response_user1 = await client.get("api/v1/flows/", params=params, headers=logged_in_headers)
response_user1 = await client.get("v1/flows/", params=params, headers=logged_in_headers)
result_user1 = response_user1.json()
assert response_user1.status_code == status.HTTP_200_OK
@@ -459,7 +459,7 @@ async def test_read_flows_user_isolation(client: AsyncClient, logged_in_headers,
assert str(flow["user_id"]) == str(active_user.id), f"Flow {flow['name']} should belong to user 1"
# Test read_flows for user 2 - should only return user 2's flows
response_user2 = await client.get("api/v1/flows/", params=params, headers=other_user_headers)
response_user2 = await client.get("v1/flows/", params=params, headers=other_user_headers)
result_user2 = response_user2.json()
assert response_user2.status_code == status.HTTP_200_OK
@@ -490,7 +490,7 @@ async def test_create_flow_rejects_absolute_path_outside_allowed_directory(clien
"data": {},
"fs_path": "/etc/passwd", # Absolute path outside allowed directory should be rejected
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "within" in response.json()["detail"].lower() or "outside" in response.json()["detail"].lower()
@@ -502,7 +502,7 @@ async def test_create_flow_rejects_directory_traversal(client: AsyncClient, logg
"data": {},
"fs_path": "../../etc/passwd", # Directory traversal should be rejected
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert (
"directory traversal" in response.json()["detail"].lower()
@@ -517,7 +517,7 @@ async def test_create_flow_rejects_null_bytes(client: AsyncClient, logged_in_hea
"data": {},
"fs_path": "file\x00name.json", # Null byte should be rejected
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "absolute paths" in response.json()["detail"].lower() or "null" in response.json()["detail"].lower()
@@ -532,7 +532,7 @@ async def test_create_flow_rejects_windows_absolute_path_outside_allowed_directo
"fs_path": "C:\\Windows\\System32\\config\\sam", # Windows absolute path outside
# allowed directory should be rejected
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "within" in response.json()["detail"].lower() or "outside" in response.json()["detail"].lower()
@@ -544,7 +544,7 @@ async def test_create_flow_accepts_relative_path(client: AsyncClient, logged_in_
"data": {},
"fs_path": "my_flow.json", # Valid relative path
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
@@ -555,7 +555,7 @@ async def test_create_flow_accepts_nested_relative_path(client: AsyncClient, log
"data": {},
"fs_path": "subfolder/my_flow.json", # Valid nested relative path
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
@@ -566,7 +566,7 @@ async def test_update_flow_rejects_absolute_path_outside_allowed_directory(clien
"name": "test_flow",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
@@ -575,7 +575,7 @@ async def test_update_flow_rejects_absolute_path_outside_allowed_directory(clien
update_case = {
"fs_path": temp_file.name,
}
update_response = await client.patch(f"api/v1/flows/{flow_id}", json=update_case, headers=logged_in_headers)
update_response = await client.patch(f"v1/flows/{flow_id}", json=update_case, headers=logged_in_headers)
assert update_response.status_code == status.HTTP_400_BAD_REQUEST
assert "within" in update_response.json()["detail"].lower() or "outside" in update_response.json()["detail"].lower()
@@ -587,7 +587,7 @@ async def test_update_flow_accepts_relative_path(client: AsyncClient, logged_in_
"name": "test_flow",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
@@ -595,7 +595,7 @@ async def test_update_flow_accepts_relative_path(client: AsyncClient, logged_in_
update_case = {
"fs_path": "updated_flow.json",
}
update_response = await client.patch(f"api/v1/flows/{flow_id}", json=update_case, headers=logged_in_headers)
update_response = await client.patch(f"v1/flows/{flow_id}", json=update_case, headers=logged_in_headers)
assert update_response.status_code == status.HTTP_200_OK
@@ -610,7 +610,7 @@ async def test_create_flow_rejects_empty_path(client: AsyncClient, logged_in_hea
"data": {},
"fs_path": "", # Empty string
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
# Empty string should be rejected by validation
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -622,7 +622,7 @@ async def test_create_flow_allows_none_path(client: AsyncClient, logged_in_heade
"data": {},
# fs_path not provided (None)
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
@@ -633,7 +633,7 @@ async def test_create_flow_rejects_multiple_traversal(client: AsyncClient, logge
"data": {},
"fs_path": "../../../etc/passwd", # Multiple traversals
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -644,7 +644,7 @@ async def test_create_flow_rejects_traversal_in_subpath(client: AsyncClient, log
"data": {},
"fs_path": "subfolder/../../etc/passwd", # Traversal in subpath
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -662,7 +662,7 @@ async def test_upload_flow_rejects_absolute_path(client: AsyncClient, logged_in_
file_content = json.dumps({"flows": [flow_data]})
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("flows.json", file_content, "application/json")},
headers=logged_in_headers,
)
@@ -681,7 +681,7 @@ async def test_upsert_flow_creates_new_flow_with_specified_id(client: AsyncClien
"data": {},
}
response = await client.put(f"api/v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
result = response.json()
@@ -697,7 +697,7 @@ async def test_upsert_flow_updates_existing_flow(client: AsyncClient, logged_in_
"description": "initial description",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=initial_flow, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=initial_flow, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
@@ -707,7 +707,7 @@ async def test_upsert_flow_updates_existing_flow(client: AsyncClient, logged_in_
"description": "updated description",
"data": {"nodes": [], "edges": []},
}
response = await client.put(f"api/v1/flows/{flow_id}", json=updated_flow, headers=logged_in_headers)
response = await client.put(f"v1/flows/{flow_id}", json=updated_flow, headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -737,17 +737,17 @@ async def test_upsert_flow_returns_404_for_other_users_flow(client: AsyncClient,
# Login as other user and create a flow
login_data = {"username": "other_user_for_upsert_test", "password": "testpassword"} # pragma: allowlist secret
login_response = await client.post("api/v1/login", data=login_data)
login_response = await client.post("v1/login", data=login_data)
assert login_response.status_code == status.HTTP_200_OK, f"Login failed: {login_response.text}"
other_user_headers = {"Authorization": f"Bearer {login_response.json()['access_token']}"}
flow_data = {"name": "other_user_flow", "data": {}}
create_response = await client.post("api/v1/flows/", json=flow_data, headers=other_user_headers)
create_response = await client.post("v1/flows/", json=flow_data, headers=other_user_headers)
other_user_flow_id = create_response.json()["id"]
# Try to upsert other user's flow with original user's credentials
update_data = {"name": "trying_to_steal", "data": {}}
response = await client.put(f"api/v1/flows/{other_user_flow_id}", json=update_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{other_user_flow_id}", json=update_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert "not found" in response.json()["detail"].lower()
@@ -770,7 +770,7 @@ async def test_upsert_flow_returns_400_for_invalid_folder_id(client: AsyncClient
"folder_id": non_existent_folder_id,
}
response = await client.put(f"api/v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "folder not found" in response.json()["detail"].lower()
@@ -784,7 +784,7 @@ async def test_upsert_flow_returns_409_for_endpoint_name_conflict_on_create(clie
"endpoint_name": "unique_endpoint",
"data": {},
}
await client.post("api/v1/flows/", json=first_flow, headers=logged_in_headers)
await client.post("v1/flows/", json=first_flow, headers=logged_in_headers)
# Try to create new flow via PUT with same endpoint_name
specified_id = str(uuid.uuid4())
@@ -794,7 +794,7 @@ async def test_upsert_flow_returns_409_for_endpoint_name_conflict_on_create(clie
"data": {},
}
response = await client.put(f"api/v1/flows/{specified_id}", json=second_flow, headers=logged_in_headers)
response = await client.put(f"v1/flows/{specified_id}", json=second_flow, headers=logged_in_headers)
assert response.status_code == status.HTTP_409_CONFLICT
assert "endpoint" in response.json()["detail"].lower()
@@ -807,7 +807,7 @@ async def test_upsert_flow_auto_renames_name_on_create_conflict(client: AsyncCli
"name": "duplicate_name",
"data": {},
}
await client.post("api/v1/flows/", json=first_flow, headers=logged_in_headers)
await client.post("v1/flows/", json=first_flow, headers=logged_in_headers)
# Create new flow via PUT with same name - should auto-rename
specified_id = str(uuid.uuid4())
@@ -816,7 +816,7 @@ async def test_upsert_flow_auto_renames_name_on_create_conflict(client: AsyncCli
"data": {},
}
response = await client.put(f"api/v1/flows/{specified_id}", json=second_flow, headers=logged_in_headers)
response = await client.put(f"v1/flows/{specified_id}", json=second_flow, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
result = response.json()
@@ -829,14 +829,14 @@ async def test_upsert_flow_returns_409_for_name_conflict_on_update(client: Async
first_flow = {"name": "flow_one", "data": {}}
second_flow = {"name": "flow_two", "data": {}}
await client.post("api/v1/flows/", json=first_flow, headers=logged_in_headers)
second_response = await client.post("api/v1/flows/", json=second_flow, headers=logged_in_headers)
await client.post("v1/flows/", json=first_flow, headers=logged_in_headers)
second_response = await client.post("v1/flows/", json=second_flow, headers=logged_in_headers)
second_flow_id = second_response.json()["id"]
# Try to update second flow to have first flow's name
update_data = {"name": "flow_one", "data": {}} # Conflict with first flow
response = await client.put(f"api/v1/flows/{second_flow_id}", json=update_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{second_flow_id}", json=update_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_409_CONFLICT
assert "name" in response.json()["detail"].lower()
@@ -848,14 +848,14 @@ async def test_upsert_flow_returns_409_for_endpoint_conflict_on_update(client: A
first_flow = {"name": "endpoint_flow_one", "endpoint_name": "endpoint_one", "data": {}}
second_flow = {"name": "endpoint_flow_two", "endpoint_name": "endpoint_two", "data": {}}
await client.post("api/v1/flows/", json=first_flow, headers=logged_in_headers)
second_response = await client.post("api/v1/flows/", json=second_flow, headers=logged_in_headers)
await client.post("v1/flows/", json=first_flow, headers=logged_in_headers)
second_response = await client.post("v1/flows/", json=second_flow, headers=logged_in_headers)
second_flow_id = second_response.json()["id"]
# Try to update second flow to have first flow's endpoint_name
update_data = {"name": "endpoint_flow_two", "endpoint_name": "endpoint_one", "data": {}}
response = await client.put(f"api/v1/flows/{second_flow_id}", json=update_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{second_flow_id}", json=update_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_409_CONFLICT
assert "endpoint" in response.json()["detail"].lower()
@@ -865,14 +865,14 @@ async def test_upsert_flow_keeps_existing_folder_on_update_when_not_provided(cli
"""Test that PUT keeps existing folder_id when not provided during UPDATE."""
# Create a flow (will be assigned to default folder)
initial_flow = {"name": "folder_test_flow", "data": {}}
create_response = await client.post("api/v1/flows/", json=initial_flow, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=initial_flow, headers=logged_in_headers)
flow_id = create_response.json()["id"]
original_folder_id = create_response.json()["folder_id"]
# Update via PUT without providing folder_id
update_data = {"name": "folder_test_flow_updated", "data": {}}
response = await client.put(f"api/v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -886,12 +886,12 @@ async def test_upsert_flow_keeps_existing_endpoint_when_not_provided(client: Asy
"endpoint_name": "keep_upsert_endpoint",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=initial_flow, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=initial_flow, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
response = await client.put(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"name": "upsert_endpoint_flow_updated", "data": {}},
headers=logged_in_headers,
)
@@ -907,12 +907,12 @@ async def test_upsert_flow_allows_clearing_endpoint_with_null(client: AsyncClien
"endpoint_name": "clear_upsert_endpoint",
"data": {},
}
create_response = await client.post("api/v1/flows/", json=initial_flow, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=initial_flow, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
flow_id = create_response.json()["id"]
response = await client.put(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"name": "upsert_clear_endpoint_flow", "endpoint_name": None, "data": {}},
headers=logged_in_headers,
)
@@ -931,7 +931,7 @@ async def test_upsert_flow_ignores_user_id_from_body(client: AsyncClient, logged
"user_id": fake_user_id, # Should be ignored
}
response = await client.put(f"api/v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{specified_id}", json=flow_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
result = response.json()
@@ -943,13 +943,13 @@ async def test_upsert_flow_allows_updating_own_flow_name(client: AsyncClient, lo
"""Test that PUT allows updating a flow to keep the same name (no conflict with itself)."""
# Create a flow
initial_flow = {"name": "self_update_flow", "description": "initial", "data": {}}
create_response = await client.post("api/v1/flows/", json=initial_flow, headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=initial_flow, headers=logged_in_headers)
flow_id = create_response.json()["id"]
# Update the flow keeping the same name but changing description
update_data = {"name": "self_update_flow", "description": "updated", "data": {}}
response = await client.put(f"api/v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
response = await client.put(f"v1/flows/{flow_id}", json=update_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -959,7 +959,7 @@ async def test_upsert_flow_allows_updating_own_flow_name(client: AsyncClient, lo
async def test_delete_flow_with_deployed_versions_returns_409(client: AsyncClient, logged_in_headers, active_user):
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "deployed-delete-flow", "data": {"nodes": [], "edges": []}},
headers=logged_in_headers,
)
@@ -974,14 +974,14 @@ async def test_delete_flow_with_deployed_versions_returns_409(client: AsyncClien
project_id=source_project_id,
)
delete_resp = await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
delete_resp = await client.delete(f"v1/flows/{flow_id}", headers=logged_in_headers)
assert delete_resp.status_code == status.HTTP_409_CONFLICT
assert "cannot be deleted because it has deployed versions" in delete_resp.json()["detail"].lower()
async def test_bulk_delete_with_deployed_flow_returns_409(client: AsyncClient, logged_in_headers, active_user):
deployed_flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "deployed-bulk-flow", "data": {"nodes": [], "edges": []}},
headers=logged_in_headers,
)
@@ -991,7 +991,7 @@ async def test_bulk_delete_with_deployed_flow_returns_409(client: AsyncClient, l
source_project_id = UUID(deployed_payload["folder_id"])
undeployed_flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "undeployed-bulk-flow", "data": {"nodes": [], "edges": []}},
headers=logged_in_headers,
)
@@ -1006,7 +1006,7 @@ async def test_bulk_delete_with_deployed_flow_returns_409(client: AsyncClient, l
delete_resp = await client.request(
"DELETE",
"api/v1/flows/",
"v1/flows/",
json=[str(deployed_flow_id), undeployed_flow_id],
headers=logged_in_headers,
)
@@ -1018,7 +1018,7 @@ async def test_patch_flow_folder_move_with_deployed_versions_returns_409(
client: AsyncClient, logged_in_headers, active_user
):
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "deployed-patch-flow", "data": {"nodes": [], "edges": []}},
headers=logged_in_headers,
)
@@ -1028,7 +1028,7 @@ async def test_patch_flow_folder_move_with_deployed_versions_returns_409(
source_project_id = UUID(flow_payload["folder_id"])
project_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "patch-target-project", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -1042,7 +1042,7 @@ async def test_patch_flow_folder_move_with_deployed_versions_returns_409(
)
patch_resp = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"folder_id": target_project_id},
headers=logged_in_headers,
)
@@ -1054,7 +1054,7 @@ async def test_upsert_flow_folder_move_with_deployed_versions_returns_409(
client: AsyncClient, logged_in_headers, active_user
):
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "deployed-put-flow", "data": {"nodes": [], "edges": []}},
headers=logged_in_headers,
)
@@ -1064,7 +1064,7 @@ async def test_upsert_flow_folder_move_with_deployed_versions_returns_409(
source_project_id = UUID(flow_payload["folder_id"])
project_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "put-target-project", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -1078,7 +1078,7 @@ async def test_upsert_flow_folder_move_with_deployed_versions_returns_409(
)
put_resp = await client.put(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={
"name": "deployed-put-flow-updated",
"data": {"nodes": [], "edges": []},
@@ -17,7 +17,7 @@ async def test_create_folder(client: AsyncClient, logged_in_headers, basic_case)
# Configure client to follow redirects
client.follow_redirects = True
response = await client.post("api/v1/folders/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/folders/", json=basic_case, headers=logged_in_headers)
result = response.json()
# Check that we're getting a valid response from the projects endpoint
@@ -33,7 +33,7 @@ async def test_read_folders(client: AsyncClient, logged_in_headers):
# Configure client to follow redirects
client.follow_redirects = True
response = await client.get("api/v1/folders/", headers=logged_in_headers)
response = await client.get("v1/folders/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -46,11 +46,11 @@ async def test_read_folder(client: AsyncClient, logged_in_headers, basic_case):
client.follow_redirects = True
# Create a folder first
response_ = await client.post("api/v1/folders/", json=basic_case, headers=logged_in_headers)
response_ = await client.post("v1/folders/", json=basic_case, headers=logged_in_headers)
id_ = response_.json()["id"]
# Get the folder
response = await client.get(f"api/v1/folders/{id_}", headers=logged_in_headers)
response = await client.get(f"v1/folders/{id_}", headers=logged_in_headers)
result = response.json()
# The response structure may be different depending on whether pagination is enabled
@@ -87,11 +87,11 @@ async def test_update_folder(client: AsyncClient, logged_in_headers, basic_case)
update_case["name"] = "Updated Folder"
# Create a folder first
response_ = await client.post("api/v1/folders/", json=basic_case, headers=logged_in_headers)
response_ = await client.post("v1/folders/", json=basic_case, headers=logged_in_headers)
id_ = response_.json()["id"]
# Update the folder
response = await client.patch(f"api/v1/folders/{id_}", json=update_case, headers=logged_in_headers)
response = await client.patch(f"v1/folders/{id_}", json=update_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
+19 -19
View File
@@ -114,27 +114,27 @@ def mock_current_user_ctx(mock_user):
# Test the HEAD /sse endpoint (checks server availability)
async def test_mcp_sse_head_endpoint(client: AsyncClient):
"""Test HEAD /sse endpoint returns 200 OK."""
response = await client.head("api/v1/mcp/sse")
response = await client.head("v1/mcp/sse")
assert response.status_code == status.HTTP_200_OK
# Test the HEAD /sse endpoint without authentication
async def test_mcp_sse_head_endpoint_no_auth(client: AsyncClient):
"""Test HEAD /sse endpoint without authentication returns 200 OK (HEAD requests don't require auth)."""
response = await client.head("api/v1/mcp/sse")
response = await client.head("v1/mcp/sse")
assert response.status_code == status.HTTP_200_OK
async def test_mcp_sse_get_endpoint_invalid_auth(client: AsyncClient):
"""Test GET /sse endpoint with invalid authentication returns 401."""
headers = {"Authorization": "Bearer invalid_token"}
response = await client.get("api/v1/mcp/sse", headers=headers)
response = await client.get("v1/mcp/sse", headers=headers)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_mcp_sse_post_endpoint(client: AsyncClient, mock_sse_transport):
"""Test POST / endpoint for SSE transport succeeds without auth."""
response = await client.post("api/v1/mcp/", json={"type": "test"})
response = await client.post("v1/mcp/", json={"type": "test"})
assert response.status_code == status.HTTP_200_OK
mock_sse_transport.handle_post_message.assert_called_once()
@@ -143,7 +143,7 @@ async def test_mcp_sse_post_endpoint(client: AsyncClient, mock_sse_transport):
async def test_mcp_post_endpoint_success(client: AsyncClient, logged_in_headers, mock_sse_transport):
"""Test POST / endpoint successfully handles MCP messages with auth."""
test_message = {"type": "test", "content": "message"}
response = await client.post("api/v1/mcp/", headers=logged_in_headers, json=test_message)
response = await client.post("v1/mcp/", headers=logged_in_headers, json=test_message)
assert response.status_code == status.HTTP_200_OK
mock_sse_transport.handle_post_message.assert_called_once()
@@ -151,13 +151,13 @@ async def test_mcp_post_endpoint_success(client: AsyncClient, logged_in_headers,
async def test_mcp_post_endpoint_no_auth(client: AsyncClient):
"""Test POST / endpoint without authentication returns 400 (current behavior)."""
response = await client.post("api/v1/mcp/", json={})
response = await client.post("v1/mcp/", json={})
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_mcp_post_endpoint_invalid_json(client: AsyncClient, logged_in_headers):
"""Test POST / endpoint with invalid JSON returns 400."""
response = await client.post("api/v1/mcp/", headers=logged_in_headers, content="invalid json")
response = await client.post("v1/mcp/", headers=logged_in_headers, content="invalid json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -165,7 +165,7 @@ async def test_mcp_sse_post_endpoint_disconnect_error(client: AsyncClient, mock_
"""Test POST / endpoint handles disconnection errors correctly for SSE."""
mock_sse_transport.handle_post_message.side_effect = BrokenPipeError("Simulated disconnect")
response = await client.post("api/v1/mcp/", json={"type": "test"})
response = await client.post("v1/mcp/", json={"type": "test"})
assert response.status_code == status.HTTP_404_NOT_FOUND
mock_sse_transport.handle_post_message.assert_called_once()
@@ -175,7 +175,7 @@ async def test_mcp_post_endpoint_disconnect_error(client: AsyncClient, logged_in
"""Test POST / endpoint handles disconnection errors correctly with auth."""
mock_sse_transport.handle_post_message.side_effect = BrokenPipeError("Simulated disconnect")
response = await client.post("api/v1/mcp/", headers=logged_in_headers, json={"type": "test"})
response = await client.post("v1/mcp/", headers=logged_in_headers, json={"type": "test"})
assert response.status_code == status.HTTP_404_NOT_FOUND
assert "MCP Server disconnected" in response.json()["detail"]
@@ -186,7 +186,7 @@ async def test_mcp_sse_post_endpoint_server_error(client: AsyncClient, mock_sse_
"""Test POST / endpoint handles server errors correctly for SSE."""
mock_sse_transport.handle_post_message.side_effect = Exception("Internal server error")
response = await client.post("api/v1/mcp/", json={"type": "test"})
response = await client.post("v1/mcp/", json={"type": "test"})
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -195,7 +195,7 @@ async def test_mcp_post_endpoint_server_error(client: AsyncClient, logged_in_hea
"""Test POST / endpoint handles server errors correctly with auth."""
mock_sse_transport.handle_post_message.side_effect = Exception("Internal server error")
response = await client.post("api/v1/mcp/", headers=logged_in_headers, json={"type": "test"})
response = await client.post("v1/mcp/", headers=logged_in_headers, json={"type": "test"})
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
assert "Internal server error" in response.json()["detail"]
@@ -209,7 +209,7 @@ async def test_mcp_streamable_post_endpoint(
):
"""Test POST /streamable endpoint successfully handles MCP messages."""
test_message = {"type": "test", "content": "message"}
response = await client.post("api/v1/mcp/streamable", headers=logged_in_headers, json=test_message)
response = await client.post("v1/mcp/streamable", headers=logged_in_headers, json=test_message)
assert response.status_code == status.HTTP_200_OK
mock_streamable_http_manager.handle_request.assert_called_once()
@@ -217,7 +217,7 @@ async def test_mcp_streamable_post_endpoint(
async def test_mcp_streamable_post_endpoint_no_auth(client: AsyncClient):
"""Test POST /streamable endpoint without authentication returns 403 Forbidden."""
response = await client.post("api/v1/mcp/streamable", json={})
response = await client.post("v1/mcp/streamable", json={})
assert response.status_code == status.HTTP_403_FORBIDDEN
@@ -227,7 +227,7 @@ async def test_mcp_streamable_post_endpoint_disconnect_error(
"""Test POST /streamable endpoint handles disconnection errors correctly."""
mock_streamable_http_manager.handle_request.side_effect = BrokenPipeError("Simulated disconnect")
response = await client.post("api/v1/mcp/streamable", headers=logged_in_headers, json={"type": "test"})
response = await client.post("v1/mcp/streamable", headers=logged_in_headers, json={"type": "test"})
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
mock_streamable_http_manager.handle_request.assert_called_once()
@@ -239,7 +239,7 @@ async def test_mcp_streamable_post_endpoint_server_error(
"""Test POST /streamable endpoint handles server errors correctly."""
mock_streamable_http_manager.handle_request.side_effect = Exception("Internal server error")
response = await client.post("api/v1/mcp/streamable", headers=logged_in_headers, json={"type": "test"})
response = await client.post("v1/mcp/streamable", headers=logged_in_headers, json={"type": "test"})
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -251,7 +251,7 @@ async def test_mcp_streamable_get_endpoint(
mock_streamable_http_manager,
):
"""Test GET /streamable endpoint successfully handles MCP messages."""
response = await client.get("api/v1/mcp/streamable", headers=logged_in_headers)
response = await client.get("v1/mcp/streamable", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
mock_streamable_http_manager.handle_request.assert_called_once()
@@ -262,20 +262,20 @@ async def test_mcp_streamable_delete_endpoint(
mock_streamable_http_manager,
):
"""Test DELETE /streamable endpoint successfully handles MCP messages."""
response = await client.delete("api/v1/mcp/streamable", headers=logged_in_headers)
response = await client.delete("v1/mcp/streamable", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
mock_streamable_http_manager.handle_request.assert_called_once()
async def test_mcp_streamable_get_endpoint_no_auth(client: AsyncClient):
"""Test GET /streamable endpoint without authentication returns 403 Forbidden."""
response = await client.get("api/v1/mcp/streamable")
response = await client.get("v1/mcp/streamable")
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_mcp_streamable_delete_endpoint_no_auth(client: AsyncClient):
"""Test DELETE /streamable endpoint without authentication returns 403 Forbidden."""
response = await client.delete("api/v1/mcp/streamable")
response = await client.delete("v1/mcp/streamable")
assert response.status_code == status.HTTP_403_FORBIDDEN
@@ -232,7 +232,7 @@ async def test_handle_project_streamable_messages_success(
):
"""Test successful handling of project messages over Streamable HTTP."""
response = await client.post(
f"api/v1/mcp/project/{user_test_project.id}/streamable",
f"v1/mcp/project/{user_test_project.id}/streamable",
headers=logged_in_headers,
json={"type": "test", "content": "message"},
)
@@ -246,7 +246,7 @@ async def test_handle_project_messages_success(
):
"""Test successful handling of project messages over SSE."""
response = await client.post(
f"api/v1/mcp/project/{user_test_project.id}",
f"v1/mcp/project/{user_test_project.id}",
headers=logged_in_headers,
json={"type": "test", "content": "message"},
)
@@ -257,7 +257,7 @@ async def test_handle_project_messages_success(
async def test_update_project_mcp_settings_invalid_json(client: AsyncClient, user_test_project, logged_in_headers):
"""Test updating MCP settings with invalid JSON."""
response = await client.patch(
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json="invalid"
f"v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json="invalid"
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@@ -322,7 +322,7 @@ async def test_update_project_mcp_settings_success(
# Make the real PATCH request
response = await client.patch(
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
f"v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Assert response
@@ -345,7 +345,7 @@ async def test_update_project_mcp_settings_invalid_project(client: AsyncClient,
nonexistent_project_id = uuid4()
# Try to access the project
response = await client.get(f"api/v1/mcp/project/{nonexistent_project_id}/sse", headers=logged_in_headers)
response = await client.get(f"v1/mcp/project/{nonexistent_project_id}/sse", headers=logged_in_headers)
# Verify the response
assert response.status_code == 404
@@ -360,7 +360,7 @@ async def test_update_project_mcp_settings_other_user_project(
# This test disables MCP Composer to test JWT token-based access control
# Try to access the other user's project using active_user's credentials
response = await client.get(f"api/v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
response = await client.get(f"v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
# Verify the response
assert response.status_code == 404
@@ -375,7 +375,7 @@ async def test_update_project_mcp_settings_other_user_project_with_composer(
assert enable_mcp_composer # Fixture ensures MCP Composer is enabled
# Try to access the other user's project using active_user's JWT credentials
response = await client.get(f"api/v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
response = await client.get(f"v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
# Verify the response - should get 401 because JWT tokens aren't accepted
assert response.status_code == 401
@@ -401,7 +401,7 @@ async def test_update_project_mcp_settings_empty_settings(client: AsyncClient, u
# Make the request to the actual endpoint
response = await client.patch(
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
f"v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Verify response - the real endpoint should handle empty settings correctly
@@ -412,7 +412,7 @@ async def test_update_project_mcp_settings_empty_settings(client: AsyncClient, u
async def test_user_can_only_access_own_projects(client: AsyncClient, other_test_project, logged_in_headers):
"""Test that a user can only access their own projects."""
# Try to access the other user's project using first user's credentials
response = await client.get(f"api/v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
response = await client.get(f"v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
# Should fail with 404 as first user cannot see second user's project
assert response.status_code == 404
assert response.json()["detail"] == "Project not found"
@@ -444,7 +444,7 @@ async def test_user_data_isolation_with_real_db(
try:
# Test that first user can't see the project
response = await client.get(f"api/v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
response = await client.get(f"v1/mcp/project/{other_test_project.id}/sse", headers=logged_in_headers)
# Should fail with 404
assert response.status_code == 404
@@ -533,7 +533,7 @@ async def test_user_can_update_own_flow_mcp_settings(
# Make the PATCH request to update settings
response = await client.patch(
f"api/v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
f"v1/mcp/project/{user_test_project.id}", headers=logged_in_headers, json=json_payload
)
# Should succeed as the user owns this project and flow
@@ -22,10 +22,10 @@ class TestSseTransportRootPath:
@pytest.fixture
def transport(self):
return SseServerTransport("/api/v1/mcp/")
return SseServerTransport("/v1/mcp/")
def test_endpoint_stored(self, transport):
assert transport._endpoint == "/api/v1/mcp/"
assert transport._endpoint == "/v1/mcp/"
def test_project_transport_endpoint(self):
"""get_project_sse stores the correct endpoint path."""
@@ -37,7 +37,7 @@ class TestSseTransportRootPath:
# Clean up after the test
try:
sse = get_project_sse(project_id)
assert sse._endpoint == f"/api/v1/mcp/project/{project_id_str}/"
assert sse._endpoint == f"/v1/mcp/project/{project_id_str}/"
finally:
project_sse_transports.pop(project_id_str, None)
@@ -75,7 +75,7 @@ class TestForwardedPrefixMiddleware:
scope = {
"type": "http",
"method": "GET",
"path": "/api/v1/mcp/sse",
"path": "/v1/mcp/sse",
"root_path": "",
"query_string": b"",
"headers": raw_headers,
@@ -127,7 +127,7 @@ class TestForwardedPrefixMiddleware:
scope = {
"type": "http",
"method": "GET",
"path": "/api/v1/mcp/sse",
"path": "/v1/mcp/sse",
"root_path": "",
"query_string": b"",
"headers": [(b"x-forwarded-prefix", b"/attacker-prefix")],
@@ -62,7 +62,7 @@ def google_credential():
@pytest.mark.usefixtures("active_user")
async def test_enabled_providers_empty_initially(client: AsyncClient, logged_in_headers):
"""Test that enabled_providers returns empty status when no credentials exist."""
response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
response = await client.get("v1/models/enabled_providers", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -76,14 +76,14 @@ async def test_enabled_providers_empty_initially(client: AsyncClient, logged_in_
async def test_enabled_providers_after_credential_creation(client: AsyncClient, openai_credential, logged_in_headers):
"""Test that provider status changes after credential creation."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
openai_var_name = _provider_variable_mapping.get("OpenAI")
for var in all_vars.json():
if var.get("name") == openai_var_name:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
# Check initial status
initial_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
initial_response = await client.get("v1/models/enabled_providers", headers=logged_in_headers)
initial_result = initial_response.json()
assert initial_response.status_code == status.HTTP_200_OK
@@ -94,14 +94,14 @@ async def test_enabled_providers_after_credential_creation(client: AsyncClient,
# Mock API validation - mock where it's used (in the variable endpoint)
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None # validate_model_provider_key returns None on success
create_response = await client.post("api/v1/variables/", json=variable_payload, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=variable_payload, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
# Check status after credential creation
# Mock validation for enabled_providers endpoint as well
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
after_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
after_response = await client.get("v1/models/enabled_providers", headers=logged_in_headers)
after_result = after_response.json()
assert after_response.status_code == status.HTTP_200_OK
@@ -118,7 +118,7 @@ async def test_enabled_providers_multiple_credentials(
):
"""Test provider status with multiple credentials."""
# Clean up any existing variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
var_names = {
_provider_variable_mapping.get("OpenAI"),
_provider_variable_mapping.get("Anthropic"),
@@ -126,7 +126,7 @@ async def test_enabled_providers_multiple_credentials(
}
for var in all_vars.json():
if var.get("name") in var_names:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
# Create multiple credentials using variables endpoint
openai_var = _create_variable_payload(openai_credential["provider"], openai_credential["value"])
@@ -136,14 +136,14 @@ async def test_enabled_providers_multiple_credentials(
# Mock API validations
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
await client.post("api/v1/variables/", json=openai_var, headers=logged_in_headers)
await client.post("api/v1/variables/", json=anthropic_var, headers=logged_in_headers)
await client.post("api/v1/variables/", json=google_var, headers=logged_in_headers)
await client.post("v1/variables/", json=openai_var, headers=logged_in_headers)
await client.post("v1/variables/", json=anthropic_var, headers=logged_in_headers)
await client.post("v1/variables/", json=google_var, headers=logged_in_headers)
# Check enabled providers - mock validation for enabled_providers endpoint
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
response = await client.get("v1/models/enabled_providers", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -160,35 +160,35 @@ async def test_enabled_providers_multiple_credentials(
async def test_enabled_providers_after_credential_deletion(client: AsyncClient, openai_credential, logged_in_headers):
"""Test that provider status updates after credential deletion."""
# Get initial OpenAI credentials to clean up (using variables endpoint)
all_variables = await client.get("api/v1/variables/", headers=logged_in_headers)
all_variables = await client.get("v1/variables/", headers=logged_in_headers)
openai_var_name = _provider_variable_mapping.get("OpenAI")
for var in all_variables.json():
if var.get("name") == openai_var_name:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
# Create credential using variables endpoint
variable_payload = _create_variable_payload(openai_credential["provider"], openai_credential["value"])
# Mock API validation
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
create_response = await client.post("api/v1/variables/", json=variable_payload, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=variable_payload, headers=logged_in_headers)
created_credential = create_response.json()
credential_id = created_credential["id"]
# Verify enabled - mock validation for enabled_providers endpoint as well
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
enabled_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
enabled_response = await client.get("v1/models/enabled_providers", headers=logged_in_headers)
enabled_result = enabled_response.json()
assert "OpenAI" in enabled_result["enabled_providers"]
assert enabled_result["provider_status"]["OpenAI"] is True
# Delete credential
delete_response = await client.delete(f"api/v1/variables/{credential_id}", headers=logged_in_headers)
delete_response = await client.delete(f"v1/variables/{credential_id}", headers=logged_in_headers)
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
# Verify disabled
disabled_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
disabled_response = await client.get("v1/models/enabled_providers", headers=logged_in_headers)
disabled_result = disabled_response.json()
assert "OpenAI" not in disabled_result["enabled_providers"]
# When no credentials exist, provider_status may be empty or OpenAI should be False
@@ -201,14 +201,14 @@ async def test_enabled_providers_filter_by_specific_providers(
):
"""Test filtering enabled_providers by specific providers."""
# Clean up any existing variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
var_names = {
_provider_variable_mapping.get("OpenAI"),
_provider_variable_mapping.get("Anthropic"),
}
for var in all_vars.json():
if var.get("name") in var_names:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
# Create credentials using variables endpoint
openai_var = _create_variable_payload(openai_credential["provider"], openai_credential["value"])
@@ -217,14 +217,14 @@ async def test_enabled_providers_filter_by_specific_providers(
# Mock API validations
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
await client.post("api/v1/variables/", json=openai_var, headers=logged_in_headers)
await client.post("api/v1/variables/", json=anthropic_var, headers=logged_in_headers)
await client.post("v1/variables/", json=openai_var, headers=logged_in_headers)
await client.post("v1/variables/", json=anthropic_var, headers=logged_in_headers)
# Request specific providers (only providers that are in the mapping) - mock validation
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
response = await client.get(
"api/v1/models/enabled_providers?providers=OpenAI&providers=Anthropic", headers=logged_in_headers
"v1/models/enabled_providers?providers=OpenAI&providers=Anthropic", headers=logged_in_headers
)
result = response.json()
@@ -238,7 +238,7 @@ async def test_enabled_providers_filter_by_specific_providers(
# Test filtering with non-existent provider (should not error, just return empty)
response2 = await client.get(
"api/v1/models/enabled_providers?providers=NonExistentProvider", headers=logged_in_headers
"v1/models/enabled_providers?providers=NonExistentProvider", headers=logged_in_headers
)
result2 = response2.json()
assert response2.status_code == status.HTTP_200_OK
@@ -251,23 +251,23 @@ async def test_enabled_providers_filter_by_specific_providers(
async def test_variables_credential_redaction(client: AsyncClient, openai_credential, logged_in_headers):
"""Test that credential variables have credentials properly redacted."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
openai_var_name = _provider_variable_mapping.get("OpenAI")
for var in all_vars.json():
if var.get("name") == openai_var_name:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
# Create a credential using variables endpoint
variable_payload = _create_variable_payload(openai_credential["provider"], openai_credential["value"])
# Mock API validation
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
create_response = await client.post("api/v1/variables/", json=variable_payload, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=variable_payload, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
created_credential = create_response.json()
# Get all variables
response = await client.get("api/v1/variables/", headers=logged_in_headers)
response = await client.get("v1/variables/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -290,14 +290,14 @@ async def test_variables_multiple_credentials_all_redacted(
):
"""Test that all credentials are redacted when fetching all variables."""
# Clean up any existing variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
var_names = {
_provider_variable_mapping.get("OpenAI"),
_provider_variable_mapping.get("Anthropic"),
}
for var in all_vars.json():
if var.get("name") in var_names:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
# Create multiple credentials using variables endpoint
openai_var = _create_variable_payload(openai_credential["provider"], openai_credential["value"])
@@ -306,14 +306,14 @@ async def test_variables_multiple_credentials_all_redacted(
# Mock API validations
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
create_response1 = await client.post("api/v1/variables/", json=openai_var, headers=logged_in_headers)
create_response2 = await client.post("api/v1/variables/", json=anthropic_var, headers=logged_in_headers)
create_response1 = await client.post("v1/variables/", json=openai_var, headers=logged_in_headers)
create_response2 = await client.post("v1/variables/", json=anthropic_var, headers=logged_in_headers)
assert create_response1.status_code == status.HTTP_201_CREATED
assert create_response2.status_code == status.HTTP_201_CREATED
# Get all variables
response = await client.get("api/v1/variables/", headers=logged_in_headers)
response = await client.get("v1/variables/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -329,28 +329,28 @@ async def test_variables_multiple_credentials_all_redacted(
async def test_enabled_providers_reflects_models_endpoint(client: AsyncClient, openai_credential, logged_in_headers):
"""Test that /models endpoint reflects same is_enabled status as /enabled_providers."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
openai_var_name = _provider_variable_mapping.get("OpenAI")
for var in all_vars.json():
if var.get("name") == openai_var_name:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
# Create credential using variables endpoint
variable_payload = _create_variable_payload(openai_credential["provider"], openai_credential["value"])
# Mock API validation
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
await client.post("api/v1/variables/", json=variable_payload, headers=logged_in_headers)
await client.post("v1/variables/", json=variable_payload, headers=logged_in_headers)
# Get enabled providers and models - mock validation in unified_models so providers are marked enabled
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
enabled_response = await client.get("api/v1/models/enabled_providers", headers=logged_in_headers)
enabled_response = await client.get("v1/models/enabled_providers", headers=logged_in_headers)
enabled_result = enabled_response.json()
# Get models (which should include provider information)
models_response = await client.get("api/v1/models", headers=logged_in_headers)
models_response = await client.get("v1/models", headers=logged_in_headers)
models_result = models_response.json()
assert models_response.status_code == status.HTTP_200_OK
@@ -371,11 +371,11 @@ async def test_security_credential_value_never_exposed_in_variables_endpoint(
):
"""Critical security test: ensure credential values are NEVER exposed in plain text."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
openai_var_name = _provider_variable_mapping.get("OpenAI")
for var in all_vars.json():
if var.get("name") == openai_var_name:
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
original_value = openai_credential["value"]
@@ -384,11 +384,11 @@ async def test_security_credential_value_never_exposed_in_variables_endpoint(
# Mock API validation
with mock.patch("flow.api.v1.variable.validate_model_provider_key") as mock_validate:
mock_validate.return_value = None
create_response = await client.post("api/v1/variables/", json=variable_payload, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=variable_payload, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
# Get all variables - this is the security-critical path
response = await client.get("api/v1/variables/", headers=logged_in_headers)
response = await client.get("v1/variables/", headers=logged_in_headers)
result = response.json()
# CRITICAL: Original value must NEVER appear in response
@@ -405,7 +405,7 @@ async def test_security_credential_value_never_exposed_in_variables_endpoint(
@pytest.mark.usefixtures("active_user")
async def test_provider_variable_mapping_returns_full_variable_info(client: AsyncClient, logged_in_headers):
"""Test that provider-variable-mapping endpoint returns full variable info for each provider."""
response = await client.get("api/v1/models/provider-variable-mapping", headers=logged_in_headers)
response = await client.get("v1/models/provider-variable-mapping", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -442,7 +442,7 @@ async def test_provider_variable_mapping_returns_full_variable_info(client: Asyn
@pytest.mark.usefixtures("active_user")
async def test_provider_variable_mapping_multi_variable_provider(client: AsyncClient, logged_in_headers):
"""Test that IBM WatsonX returns multiple required variables."""
response = await client.get("api/v1/models/provider-variable-mapping", headers=logged_in_headers)
response = await client.get("v1/models/provider-variable-mapping", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -524,7 +524,7 @@ async def test_list_models_returns_live_ollama_models_when_configured(client: As
side_effect=mock_get_live_models,
),
):
response = await client.get("api/v1/models", headers=logged_in_headers)
response = await client.get("v1/models", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
data = response.json()
@@ -556,7 +556,7 @@ async def test_list_models_ollama_empty_when_live_fetch_returns_empty(client: As
return_value=[],
),
):
response = await client.get("api/v1/models", headers=logged_in_headers)
response = await client.get("v1/models", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
data = response.json()
@@ -7,7 +7,7 @@ from httpx import AsyncClient
async def test_get_messages_requires_auth(client: AsyncClient):
"""Test that GET /monitor/messages requires authentication."""
response = await client.get("api/v1/monitor/messages")
response = await client.get("v1/monitor/messages")
# Flow returns 403 for missing/invalid authentication
assert response.status_code == status.HTTP_403_FORBIDDEN
@@ -15,21 +15,21 @@ async def test_get_messages_requires_auth(client: AsyncClient):
async def test_get_transactions_requires_auth(client: AsyncClient):
"""Test that GET /monitor/transactions requires authentication."""
# Include required query parameter
response = await client.get("api/v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000")
response = await client.get("v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000")
# Flow returns 403 for missing/invalid authentication
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_delete_messages_session_requires_auth(client: AsyncClient):
"""Test that DELETE /monitor/messages/session/{session_id} requires authentication."""
response = await client.delete("api/v1/monitor/messages/session/test-session")
response = await client.delete("v1/monitor/messages/session/test-session")
# Flow returns 403 for missing/invalid authentication
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_get_messages_with_fake_token(client: AsyncClient):
"""Test that GET /monitor/messages rejects fake tokens."""
response = await client.get("api/v1/monitor/messages", headers={"Authorization": "Bearer fake-token"})
response = await client.get("v1/monitor/messages", headers={"Authorization": "Bearer fake-token"})
# Flow returns 401 for invalid Bearer tokens (JWT validation fails)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@@ -37,7 +37,7 @@ async def test_get_messages_with_fake_token(client: AsyncClient):
async def test_get_transactions_with_fake_token(client: AsyncClient):
"""Test that GET /monitor/transactions rejects fake tokens."""
response = await client.get(
"api/v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000",
"v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000",
headers={"Authorization": "Bearer fake-token"},
)
# Flow returns 401 for invalid Bearer tokens (JWT validation fails)
@@ -47,7 +47,7 @@ async def test_get_transactions_with_fake_token(client: AsyncClient):
async def test_delete_messages_session_with_fake_token(client: AsyncClient):
"""Test that DELETE /monitor/messages/session/{session_id} rejects fake tokens."""
response = await client.delete(
"api/v1/monitor/messages/session/test-session", headers={"Authorization": "Bearer fake-token"}
"v1/monitor/messages/session/test-session", headers={"Authorization": "Bearer fake-token"}
)
# Flow returns 401 for invalid Bearer tokens (JWT validation fails)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@@ -56,7 +56,7 @@ async def test_delete_messages_session_with_fake_token(client: AsyncClient):
@pytest.mark.usefixtures("active_user")
async def test_get_messages_with_valid_auth(client: AsyncClient, logged_in_headers):
"""Test that GET /monitor/messages works with valid authentication."""
response = await client.get("api/v1/monitor/messages", headers=logged_in_headers)
response = await client.get("v1/monitor/messages", headers=logged_in_headers)
# Should return 200 OK (even if empty list)
assert response.status_code == status.HTTP_200_OK
assert isinstance(response.json(), list)
@@ -66,7 +66,7 @@ async def test_get_messages_with_valid_auth(client: AsyncClient, logged_in_heade
async def test_get_transactions_with_valid_auth(client: AsyncClient, logged_in_headers):
"""Test that GET /monitor/transactions works with valid authentication."""
response = await client.get(
"api/v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000", headers=logged_in_headers
"v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000", headers=logged_in_headers
)
# Should return 200 OK with pagination structure
assert response.status_code == status.HTTP_200_OK
@@ -78,6 +78,6 @@ async def test_get_transactions_with_valid_auth(client: AsyncClient, logged_in_h
@pytest.mark.usefixtures("active_user")
async def test_delete_messages_session_with_valid_auth(client: AsyncClient, logged_in_headers):
"""Test that DELETE /monitor/messages/session/{session_id} works with valid authentication."""
response = await client.delete("api/v1/monitor/messages/session/test-session", headers=logged_in_headers)
response = await client.delete("v1/monitor/messages/session/test-session", headers=logged_in_headers)
# Should return 204 No Content
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -37,7 +37,7 @@ async def other_active_user(client): # noqa: ARG001
@pytest.fixture
async def other_logged_in_headers(client: AsyncClient, other_active_user):
login_data = {"username": other_active_user.username, "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
tokens = response.json()
return {"Authorization": f"Bearer {tokens['access_token']}"}
@@ -112,7 +112,7 @@ async def test_get_monitor_builds_does_not_return_other_users_data(
cross_user_monitor_data,
):
own_response = await client.get(
"api/v1/monitor/builds",
"v1/monitor/builds",
params={"flow_id": cross_user_monitor_data["owned_flow_id"]},
headers=logged_in_headers,
)
@@ -120,7 +120,7 @@ async def test_get_monitor_builds_does_not_return_other_users_data(
assert own_response.json()["vertex_builds"]
foreign_response = await client.get(
"api/v1/monitor/builds",
"v1/monitor/builds",
params={"flow_id": cross_user_monitor_data["foreign_flow_id"]},
headers=logged_in_headers,
)
@@ -128,7 +128,7 @@ async def test_get_monitor_builds_does_not_return_other_users_data(
assert foreign_response.json() == {"vertex_builds": {}}
owner_response = await client.get(
"api/v1/monitor/builds",
"v1/monitor/builds",
params={"flow_id": cross_user_monitor_data["foreign_flow_id"]},
headers=other_logged_in_headers,
)
@@ -144,14 +144,14 @@ async def test_delete_monitor_builds_cannot_delete_other_users_data(
cross_user_monitor_data,
):
delete_response = await client.delete(
"api/v1/monitor/builds",
"v1/monitor/builds",
params={"flow_id": cross_user_monitor_data["foreign_flow_id"]},
headers=logged_in_headers,
)
assert delete_response.status_code == 204, delete_response.text
owner_response = await client.get(
"api/v1/monitor/builds",
"v1/monitor/builds",
params={"flow_id": cross_user_monitor_data["foreign_flow_id"]},
headers=other_logged_in_headers,
)
@@ -167,7 +167,7 @@ async def test_get_monitor_transactions_does_not_return_other_users_data(
cross_user_monitor_data,
):
own_response = await client.get(
"api/v1/monitor/transactions",
"v1/monitor/transactions",
params={"flow_id": cross_user_monitor_data["owned_flow_id"]},
headers=logged_in_headers,
)
@@ -175,7 +175,7 @@ async def test_get_monitor_transactions_does_not_return_other_users_data(
assert len(own_response.json()["items"]) == 1
foreign_response = await client.get(
"api/v1/monitor/transactions",
"v1/monitor/transactions",
params={"flow_id": cross_user_monitor_data["foreign_flow_id"]},
headers=logged_in_headers,
)
@@ -184,7 +184,7 @@ async def test_get_monitor_transactions_does_not_return_other_users_data(
assert foreign_response.json()["total"] == 0
owner_response = await client.get(
"api/v1/monitor/transactions",
"v1/monitor/transactions",
params={"flow_id": cross_user_monitor_data["foreign_flow_id"]},
headers=other_logged_in_headers,
)
@@ -75,23 +75,23 @@ FAKE_FLOW_ID = "00000000-0000-0000-0000-000000000001"
async def test_get_shared_sessions_requires_auth(client: AsyncClient):
response = await client.get(f"api/v1/monitor/messages/shared/sessions?source_flow_id={FAKE_FLOW_ID}")
response = await client.get(f"v1/monitor/messages/shared/sessions?source_flow_id={FAKE_FLOW_ID}")
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_get_shared_messages_requires_auth(client: AsyncClient):
response = await client.get(f"api/v1/monitor/messages/shared?source_flow_id={FAKE_FLOW_ID}")
response = await client.get(f"v1/monitor/messages/shared?source_flow_id={FAKE_FLOW_ID}")
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_delete_shared_session_requires_auth(client: AsyncClient):
response = await client.delete(f"api/v1/monitor/messages/shared/session/test-session?source_flow_id={FAKE_FLOW_ID}")
response = await client.delete(f"v1/monitor/messages/shared/session/test-session?source_flow_id={FAKE_FLOW_ID}")
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_rename_shared_session_requires_auth(client: AsyncClient):
response = await client.patch(
f"api/v1/monitor/messages/shared/session/old-session?new_session_id=new-session&source_flow_id={FAKE_FLOW_ID}"
f"v1/monitor/messages/shared/session/old-session?new_session_id=new-session&source_flow_id={FAKE_FLOW_ID}"
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@@ -99,7 +99,7 @@ async def test_rename_shared_session_requires_auth(client: AsyncClient):
async def test_update_shared_message_requires_auth(client: AsyncClient):
fake_msg_id = "00000000-0000-0000-0000-000000000002"
response = await client.put(
f"api/v1/monitor/messages/shared/{fake_msg_id}?source_flow_id={FAKE_FLOW_ID}",
f"v1/monitor/messages/shared/{fake_msg_id}?source_flow_id={FAKE_FLOW_ID}",
json={"text": "updated"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@@ -150,7 +150,7 @@ async def shared_messages_setup(active_user):
async def test_get_shared_sessions_returns_user_sessions(client: AsyncClient, logged_in_headers, shared_messages_setup):
source_flow_id = shared_messages_setup["source_flow_id"]
response = await client.get(
f"api/v1/monitor/messages/shared/sessions?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared/sessions?source_flow_id={source_flow_id}",
headers=logged_in_headers,
)
assert response.status_code == status.HTTP_200_OK
@@ -164,7 +164,7 @@ async def test_get_shared_sessions_returns_user_sessions(client: AsyncClient, lo
async def test_get_shared_messages_returns_messages(client: AsyncClient, logged_in_headers, shared_messages_setup):
source_flow_id = shared_messages_setup["source_flow_id"]
response = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-1",
f"v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-1",
headers=logged_in_headers,
)
assert response.status_code == status.HTTP_200_OK
@@ -178,7 +178,7 @@ async def test_get_shared_messages_empty_for_wrong_flow(client: AsyncClient, log
"""Requesting messages for a flow the user hasn't interacted with returns empty."""
random_flow_id = uuid.uuid4()
response = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={random_flow_id}",
f"v1/monitor/messages/shared?source_flow_id={random_flow_id}",
headers=logged_in_headers,
)
assert response.status_code == status.HTTP_200_OK
@@ -191,21 +191,21 @@ async def test_delete_shared_session_removes_messages(client: AsyncClient, logge
# Delete session-1
response = await client.delete(
f"api/v1/monitor/messages/shared/session/test-session-1?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared/session/test-session-1?source_flow_id={source_flow_id}",
headers=logged_in_headers,
)
assert response.status_code == status.HTTP_204_NO_CONTENT
# Verify session-1 messages are gone
response = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-1",
f"v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-1",
headers=logged_in_headers,
)
assert response.json() == []
# Verify session-2 messages still exist
response = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-2",
f"v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-2",
headers=logged_in_headers,
)
assert len(response.json()) == 1
@@ -216,7 +216,7 @@ async def test_rename_shared_session(client: AsyncClient, logged_in_headers, sha
source_flow_id = shared_messages_setup["source_flow_id"]
response = await client.patch(
f"api/v1/monitor/messages/shared/session/test-session-1"
f"v1/monitor/messages/shared/session/test-session-1"
f"?new_session_id=renamed-session&source_flow_id={source_flow_id}",
headers=logged_in_headers,
)
@@ -238,7 +238,7 @@ async def test_shared_messages_isolated_between_users(
# User A can see their messages
response_a = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared?source_flow_id={source_flow_id}",
headers=logged_in_headers,
)
assert response_a.status_code == status.HTTP_200_OK
@@ -246,13 +246,13 @@ async def test_shared_messages_isolated_between_users(
# Log in as User B
login_data = {"username": user_two.username, "password": "hashed_password"}
login_response = await client.post("api/v1/login", data=login_data)
login_response = await client.post("v1/login", data=login_data)
assert login_response.status_code == 200
user_b_headers = {"Authorization": f"Bearer {login_response.json()['access_token']}"}
# User B sees EMPTY messages for the same source_flow_id
response_b = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared?source_flow_id={source_flow_id}",
headers=user_b_headers,
)
assert response_b.status_code == status.HTTP_200_OK
@@ -260,7 +260,7 @@ async def test_shared_messages_isolated_between_users(
# User B also sees no sessions
response_b_sessions = await client.get(
f"api/v1/monitor/messages/shared/sessions?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared/sessions?source_flow_id={source_flow_id}",
headers=user_b_headers,
)
assert response_b_sessions.status_code == status.HTTP_200_OK
@@ -274,7 +274,7 @@ async def test_update_shared_message_updates_properties(client: AsyncClient, log
# Get a message to update
response = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-1",
f"v1/monitor/messages/shared?source_flow_id={source_flow_id}&session_id=test-session-1",
headers=logged_in_headers,
)
messages = response.json()
@@ -283,7 +283,7 @@ async def test_update_shared_message_updates_properties(client: AsyncClient, log
# Update the message properties with build_duration
response = await client.put(
f"api/v1/monitor/messages/shared/{message_id}?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared/{message_id}?source_flow_id={source_flow_id}",
headers=logged_in_headers,
json={"properties": {"build_duration": 1500}},
)
@@ -301,7 +301,7 @@ async def test_update_shared_message_returns_404_for_wrong_flow(
# Get a real message ID
response = await client.get(
f"api/v1/monitor/messages/shared?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared?source_flow_id={source_flow_id}",
headers=logged_in_headers,
)
message_id = response.json()[0]["id"]
@@ -309,7 +309,7 @@ async def test_update_shared_message_returns_404_for_wrong_flow(
# Try to update with a different source_flow_id
wrong_flow_id = uuid.uuid4()
response = await client.put(
f"api/v1/monitor/messages/shared/{message_id}?source_flow_id={wrong_flow_id}",
f"v1/monitor/messages/shared/{message_id}?source_flow_id={wrong_flow_id}",
headers=logged_in_headers,
json={"properties": {"build_duration": 9999}},
)
@@ -325,7 +325,7 @@ async def test_update_shared_message_returns_404_for_nonexistent_id(
fake_msg_id = uuid.uuid4()
response = await client.put(
f"api/v1/monitor/messages/shared/{fake_msg_id}?source_flow_id={source_flow_id}",
f"v1/monitor/messages/shared/{fake_msg_id}?source_flow_id={source_flow_id}",
headers=logged_in_headers,
json={"text": "should not work"},
)
+108 -108
View File
@@ -26,7 +26,7 @@ def basic_case():
async def test_create_project(client: AsyncClient, logged_in_headers, basic_case):
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -51,20 +51,20 @@ async def test_create_project_duplicate_name_escapes_like_wildcards(client: Asyn
"components_list": [],
}
response = await client.post("api/v1/projects/", json=unrelated, headers=logged_in_headers)
response = await client.post("v1/projects/", json=unrelated, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
response = await client.post("api/v1/projects/", json=wildcard, headers=logged_in_headers)
response = await client.post("v1/projects/", json=wildcard, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == "proj_%"
response = await client.post("api/v1/projects/", json=wildcard, headers=logged_in_headers)
response = await client.post("v1/projects/", json=wildcard, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == "proj_% (1)"
async def test_read_projects(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/projects/", headers=logged_in_headers)
response = await client.get("v1/projects/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -74,11 +74,11 @@ async def test_read_projects(client: AsyncClient, logged_in_headers):
async def test_read_project(client: AsyncClient, logged_in_headers, basic_case):
# Create a project first
response_ = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response_ = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
id_ = response_.json()["id"]
# Get the project
response = await client.get(f"api/v1/projects/{id_}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{id_}", headers=logged_in_headers)
result = response.json()
# The response structure may be different depending on whether pagination is enabled
@@ -112,11 +112,11 @@ async def test_update_project(client: AsyncClient, logged_in_headers, basic_case
update_case["name"] = "Updated Project"
# Create a project first
response_ = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response_ = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
id_ = response_.json()["id"]
# Update the project
response = await client.patch(f"api/v1/projects/{id_}", json=update_case, headers=logged_in_headers)
response = await client.patch(f"v1/projects/{id_}", json=update_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -130,29 +130,29 @@ async def test_update_project(client: AsyncClient, logged_in_headers, basic_case
async def test_create_project_validation_error(client: AsyncClient, logged_in_headers, basic_case):
invalid_case = basic_case.copy()
invalid_case.pop("name")
response = await client.post("api/v1/projects/", json=invalid_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=invalid_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_delete_project_then_404(client: AsyncClient, logged_in_headers, basic_case):
create_resp = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_resp = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
proj_id = create_resp.json()["id"]
del_resp = await client.delete(f"api/v1/projects/{proj_id}", headers=logged_in_headers)
del_resp = await client.delete(f"v1/projects/{proj_id}", headers=logged_in_headers)
assert del_resp.status_code == status.HTTP_204_NO_CONTENT
get_resp = await client.get(f"api/v1/projects/{proj_id}", headers=logged_in_headers)
get_resp = await client.get(f"v1/projects/{proj_id}", headers=logged_in_headers)
assert get_resp.status_code == status.HTTP_404_NOT_FOUND
async def test_read_project_invalid_id_format(client: AsyncClient, logged_in_headers):
bad_id = "not-a-uuid"
response = await client.get(f"api/v1/projects/{bad_id}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{bad_id}", headers=logged_in_headers)
assert response.status_code in (status.HTTP_422_UNPROCESSABLE_ENTITY, status.HTTP_400_BAD_REQUEST)
async def test_read_projects_pagination(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/projects/?limit=1&offset=0", headers=logged_in_headers)
response = await client.get("v1/projects/?limit=1&offset=0", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
if isinstance(result, list):
@@ -165,7 +165,7 @@ async def test_read_projects_pagination(client: AsyncClient, logged_in_headers):
async def test_read_projects_empty(client: AsyncClient, logged_in_headers):
# Ensure DB is clean by fetching with a random header that forces each test transactional isolation
random_headers = {**logged_in_headers, "X-Transaction-ID": str(uuid4())}
response = await client.get("api/v1/projects/", headers=random_headers)
response = await client.get("v1/projects/", headers=random_headers)
if response.json():
pytest.skip("Pre-existing projects found; skipping empty list assertion")
assert response.status_code == status.HTTP_200_OK
@@ -182,7 +182,7 @@ async def test_create_and_read_project_cyrillic(client: AsyncClient, logged_in_h
}
# Create the project with Cyrillic characters
create_resp = await client.post("api/v1/projects/", json=payload, headers=logged_in_headers)
create_resp = await client.post("v1/projects/", json=payload, headers=logged_in_headers)
assert create_resp.status_code == status.HTTP_201_CREATED
created = create_resp.json()
assert created["name"] == CYRILLIC_NAME
@@ -190,7 +190,7 @@ async def test_create_and_read_project_cyrillic(client: AsyncClient, logged_in_h
proj_id = created["id"]
# Fetch the project back to verify round-trip UTF-8 integrity
get_resp = await client.get(f"api/v1/projects/{proj_id}", headers=logged_in_headers)
get_resp = await client.get(f"v1/projects/{proj_id}", headers=logged_in_headers)
assert get_resp.status_code == status.HTTP_200_OK
fetched = get_resp.json()
@@ -213,7 +213,7 @@ async def test_update_project_preserves_flows(client: AsyncClient, logged_in_hea
"flows_list": [],
"components_list": [],
}
create_resp = await client.post("api/v1/projects/", json=project_payload, headers=logged_in_headers)
create_resp = await client.post("v1/projects/", json=project_payload, headers=logged_in_headers)
assert create_resp.status_code == status.HTTP_201_CREATED
project = create_resp.json()
project_id = project["id"]
@@ -234,8 +234,8 @@ async def test_update_project_preserves_flows(client: AsyncClient, logged_in_hea
"is_component": False,
}
flow1_resp = await client.post("api/v1/flows/", json=flow1_payload, headers=logged_in_headers)
flow2_resp = await client.post("api/v1/flows/", json=flow2_payload, headers=logged_in_headers)
flow1_resp = await client.post("v1/flows/", json=flow1_payload, headers=logged_in_headers)
flow2_resp = await client.post("v1/flows/", json=flow2_payload, headers=logged_in_headers)
assert flow1_resp.status_code == status.HTTP_201_CREATED
assert flow2_resp.status_code == status.HTTP_201_CREATED
@@ -243,7 +243,7 @@ async def test_update_project_preserves_flows(client: AsyncClient, logged_in_hea
flow2_id = flow2_resp.json()["id"]
# Get project to verify flows are associated
get_resp = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers)
get_resp = await client.get(f"v1/projects/{project_id}", headers=logged_in_headers)
assert get_resp.status_code == status.HTTP_200_OK
project_data = get_resp.json()
@@ -259,7 +259,7 @@ async def test_update_project_preserves_flows(client: AsyncClient, logged_in_hea
# Update project name (the bug scenario)
update_payload = {"name": "Renamed Project with Flows", "description": "Testing flow preservation after rename"}
update_resp = await client.patch(f"api/v1/projects/{project_id}", json=update_payload, headers=logged_in_headers)
update_resp = await client.patch(f"v1/projects/{project_id}", json=update_payload, headers=logged_in_headers)
assert update_resp.status_code == status.HTTP_200_OK
# Verify project was renamed
@@ -267,7 +267,7 @@ async def test_update_project_preserves_flows(client: AsyncClient, logged_in_hea
assert updated_project["name"] == "Renamed Project with Flows"
# Critical test: Verify flows are still associated after rename
get_after_resp = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers)
get_after_resp = await client.get(f"v1/projects/{project_id}", headers=logged_in_headers)
assert get_after_resp.status_code == status.HTTP_200_OK
project_after = get_after_resp.json()
@@ -282,8 +282,8 @@ async def test_update_project_preserves_flows(client: AsyncClient, logged_in_hea
assert str(flow2_id) in flow_ids_after, "Flow 2 was lost after project rename!"
# Verify individual flows still exist and are accessible
flow1_get_resp = await client.get(f"api/v1/flows/{flow1_id}", headers=logged_in_headers)
flow2_get_resp = await client.get(f"api/v1/flows/{flow2_id}", headers=logged_in_headers)
flow1_get_resp = await client.get(f"v1/flows/{flow1_id}", headers=logged_in_headers)
flow2_get_resp = await client.get(f"v1/flows/{flow2_id}", headers=logged_in_headers)
assert flow1_get_resp.status_code == status.HTTP_200_OK
assert flow2_get_resp.status_code == status.HTTP_200_OK
@@ -303,7 +303,7 @@ async def test_update_project_preserves_components(client: AsyncClient, logged_i
"flows_list": [],
"components_list": [],
}
create_resp = await client.post("api/v1/projects/", json=project_payload, headers=logged_in_headers)
create_resp = await client.post("v1/projects/", json=project_payload, headers=logged_in_headers)
assert create_resp.status_code == status.HTTP_201_CREATED
project = create_resp.json()
project_id = project["id"]
@@ -324,8 +324,8 @@ async def test_update_project_preserves_components(client: AsyncClient, logged_i
"is_component": True, # This makes it a component
}
comp1_resp = await client.post("api/v1/flows/", json=comp1_payload, headers=logged_in_headers)
comp2_resp = await client.post("api/v1/flows/", json=comp2_payload, headers=logged_in_headers)
comp1_resp = await client.post("v1/flows/", json=comp1_payload, headers=logged_in_headers)
comp2_resp = await client.post("v1/flows/", json=comp2_payload, headers=logged_in_headers)
assert comp1_resp.status_code == status.HTTP_201_CREATED
assert comp2_resp.status_code == status.HTTP_201_CREATED
@@ -333,7 +333,7 @@ async def test_update_project_preserves_components(client: AsyncClient, logged_i
comp2_id = comp2_resp.json()["id"]
# Get project to verify components are associated
get_resp = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers)
get_resp = await client.get(f"v1/projects/{project_id}", headers=logged_in_headers)
assert get_resp.status_code == status.HTTP_200_OK
project_data = get_resp.json()
@@ -349,11 +349,11 @@ async def test_update_project_preserves_components(client: AsyncClient, logged_i
# Update project name
update_payload = {"name": "Renamed Project with Components"}
update_resp = await client.patch(f"api/v1/projects/{project_id}", json=update_payload, headers=logged_in_headers)
update_resp = await client.patch(f"v1/projects/{project_id}", json=update_payload, headers=logged_in_headers)
assert update_resp.status_code == status.HTTP_200_OK
# Verify components are still associated after rename
get_after_resp = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers)
get_after_resp = await client.get(f"v1/projects/{project_id}", headers=logged_in_headers)
assert get_after_resp.status_code == status.HTTP_200_OK
project_after = get_after_resp.json()
@@ -378,7 +378,7 @@ async def test_update_project_preserves_mixed_flows_and_components(client: Async
"flows_list": [],
"components_list": [],
}
create_resp = await client.post("api/v1/projects/", json=project_payload, headers=logged_in_headers)
create_resp = await client.post("v1/projects/", json=project_payload, headers=logged_in_headers)
assert create_resp.status_code == status.HTTP_201_CREATED
project = create_resp.json()
project_id = project["id"]
@@ -399,8 +399,8 @@ async def test_update_project_preserves_mixed_flows_and_components(client: Async
"is_component": True,
}
flow_resp = await client.post("api/v1/flows/", json=flow_payload, headers=logged_in_headers)
comp_resp = await client.post("api/v1/flows/", json=component_payload, headers=logged_in_headers)
flow_resp = await client.post("v1/flows/", json=flow_payload, headers=logged_in_headers)
comp_resp = await client.post("v1/flows/", json=component_payload, headers=logged_in_headers)
assert flow_resp.status_code == status.HTTP_201_CREATED
assert comp_resp.status_code == status.HTTP_201_CREATED
@@ -408,7 +408,7 @@ async def test_update_project_preserves_mixed_flows_and_components(client: Async
comp_id = comp_resp.json()["id"]
# Verify initial state
get_resp = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers)
get_resp = await client.get(f"v1/projects/{project_id}", headers=logged_in_headers)
project_data = get_resp.json()
flows_before = project_data.get("flows", [])
@@ -420,11 +420,11 @@ async def test_update_project_preserves_mixed_flows_and_components(client: Async
# Update project
update_payload = {"name": "Renamed Mixed Project"}
update_resp = await client.patch(f"api/v1/projects/{project_id}", json=update_payload, headers=logged_in_headers)
update_resp = await client.patch(f"v1/projects/{project_id}", json=update_payload, headers=logged_in_headers)
assert update_resp.status_code == status.HTTP_200_OK
# Verify both flows and components preserved
get_after_resp = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers)
get_after_resp = await client.get(f"v1/projects/{project_id}", headers=logged_in_headers)
project_after = get_after_resp.json()
flows_after = project_after.get("flows", [])
@@ -473,7 +473,7 @@ class TestProjectMCPIntegration:
mock_mcp_settings_disabled, # noqa: ARG002
):
"""Test project creation when MCP auto-add is disabled."""
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -520,7 +520,7 @@ class TestProjectMCPIntegration:
mock_update_server.return_value = None
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -572,7 +572,7 @@ class TestProjectMCPIntegration:
mock_update_server.return_value = None
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -617,7 +617,7 @@ class TestProjectMCPIntegration:
mock_validate.return_value = mock_validation_result
# The validation function should raise the HTTPException during project creation
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_409_CONFLICT
response_data = response.json()
@@ -663,7 +663,7 @@ class TestProjectMCPIntegration:
)
mock_validate.return_value = mock_validation_result
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_409_CONFLICT
response_data = response.json()
@@ -700,7 +700,7 @@ class TestProjectMCPIntegration:
mock_validation_result.server_name = "lf-new-project"
mock_validate.return_value = mock_validation_result
response = await client.post("api/v1/projects/", json=oauth_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=oauth_case, headers=logged_in_headers)
# Should still create project but log error about OAuth
assert response.status_code == status.HTTP_201_CREATED
@@ -734,7 +734,7 @@ class TestProjectMCPIntegration:
mock_validation_result.server_name = "lf-new-project"
mock_validate.return_value = mock_validation_result
response = await client.post("api/v1/projects/", json=oauth_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=oauth_case, headers=logged_in_headers)
# Should still create project but log error about OAuth
assert response.status_code == status.HTTP_201_CREATED
@@ -768,7 +768,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Now update the project name
@@ -801,7 +801,7 @@ class TestProjectMCPIntegration:
mock_validate.side_effect = [mock_old_validation, mock_new_validation]
response = await client.patch(f"api/v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
response = await client.patch(f"v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -845,7 +845,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Now update the project name
@@ -878,7 +878,7 @@ class TestProjectMCPIntegration:
mock_validate.side_effect = [mock_old_validation, mock_new_validation]
response = await client.patch(f"api/v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
response = await client.patch(f"v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -918,7 +918,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Try to update to conflicting name
@@ -950,7 +950,7 @@ class TestProjectMCPIntegration:
mock_validate.side_effect = [mock_old_validation, mock_new_validation]
response = await client.patch(f"api/v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
response = await client.patch(f"v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_409_CONFLICT
assert "conflict" in response.json()["detail"].lower()
@@ -988,7 +988,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Try to update to conflicting name
@@ -1020,7 +1020,7 @@ class TestProjectMCPIntegration:
mock_validate.side_effect = [mock_old_validation, mock_new_validation]
response = await client.patch(f"api/v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
response = await client.patch(f"v1/projects/{project_id}", json=update_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_409_CONFLICT
assert "conflict" in response.json()["detail"].lower()
@@ -1054,7 +1054,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Delete the project
@@ -1078,7 +1078,7 @@ class TestProjectMCPIntegration:
mock_validation.server_name = "lf-new-project"
mock_validate.return_value = mock_validation
response = await client.delete(f"api/v1/projects/{project_id}", headers=logged_in_headers)
response = await client.delete(f"v1/projects/{project_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -1122,7 +1122,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Delete the project
@@ -1146,7 +1146,7 @@ class TestProjectMCPIntegration:
mock_validation.server_name = "lf-new-project"
mock_validate.return_value = mock_validation
response = await client.delete(f"api/v1/projects/{project_id}", headers=logged_in_headers)
response = await client.delete(f"v1/projects/{project_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -1186,7 +1186,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Delete the project
@@ -1210,7 +1210,7 @@ class TestProjectMCPIntegration:
mock_validation.server_name = "lf-new-project"
mock_validate.return_value = mock_validation
response = await client.delete(f"api/v1/projects/{project_id}", headers=logged_in_headers)
response = await client.delete(f"v1/projects/{project_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -1251,7 +1251,7 @@ class TestProjectMCPIntegration:
mock_validation_create.server_name = "lf-new-project"
mock_validate_create.return_value = mock_validation_create
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
project_id = create_response.json()["id"]
# Delete the project
@@ -1275,7 +1275,7 @@ class TestProjectMCPIntegration:
mock_validation.server_name = "lf-new-project"
mock_validate.return_value = mock_validation
response = await client.delete(f"api/v1/projects/{project_id}", headers=logged_in_headers)
response = await client.delete(f"v1/projects/{project_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -1296,7 +1296,7 @@ class TestProjectMCPIntegration:
with patch("flow.api.v1.projects.encrypt_auth_settings") as mock_encrypt:
mock_encrypt.return_value = {"auth_type": "apikey"}
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
# Verify encrypt_auth_settings was called with apikey auth
@@ -1322,7 +1322,7 @@ class TestProjectMCPIntegration:
# Mock validation to raise an exception
mock_validate.side_effect = Exception("MCP validation failed")
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
# Project should still be created despite MCP error
assert response.status_code == status.HTTP_201_CREATED
@@ -1352,7 +1352,7 @@ class TestProjectMCPIntegration:
# Mock validation to raise an exception
mock_validate.side_effect = Exception("MCP validation failed")
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
# Project should still be created despite MCP error
assert response.status_code == status.HTTP_201_CREATED
@@ -1368,12 +1368,12 @@ class TestReadProjectBugFix:
async def test_read_project_without_pagination_params(self, client: AsyncClient, logged_in_headers, basic_case):
"""Test read_project returns correct response when no pagination params are provided."""
# Create a project first
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
project_id = create_response.json()["id"]
# Read project without pagination params
response = await client.get(f"api/v1/projects/{project_id}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{project_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -1389,12 +1389,12 @@ class TestReadProjectBugFix:
async def test_read_project_with_pagination_params(self, client: AsyncClient, logged_in_headers, basic_case):
"""Test read_project returns paginated response when pagination params are provided."""
# Create a project first
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
project_id = create_response.json()["id"]
# Read project with pagination params
response = await client.get(f"api/v1/projects/{project_id}?page=1&size=10", headers=logged_in_headers)
response = await client.get(f"v1/projects/{project_id}?page=1&size=10", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -1423,12 +1423,12 @@ class TestReadProjectBugFix:
):
"""Test read_project behavior when only some pagination params are provided."""
# Create a project first
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
project_id = create_response.json()["id"]
# Test with only page param (no size)
response = await client.get(f"api/v1/projects/{project_id}?page=1", headers=logged_in_headers)
response = await client.get(f"v1/projects/{project_id}?page=1", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -1439,7 +1439,7 @@ class TestReadProjectBugFix:
assert result["name"] == basic_case["name"]
# Test with only size param (no page)
response = await client.get(f"api/v1/projects/{project_id}?size=10", headers=logged_in_headers)
response = await client.get(f"v1/projects/{project_id}?size=10", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -1452,7 +1452,7 @@ class TestReadProjectBugFix:
async def test_read_project_with_filtering_params(self, client: AsyncClient, logged_in_headers, basic_case):
"""Test read_project with filtering parameters (is_component, is_flow, search)."""
# Create a project first
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
project_id = create_response.json()["id"]
@@ -1472,13 +1472,13 @@ class TestReadProjectBugFix:
"is_component": True,
}
flow_response = await client.post("api/v1/flows/", json=flow_payload, headers=logged_in_headers)
comp_response = await client.post("api/v1/flows/", json=component_payload, headers=logged_in_headers)
flow_response = await client.post("v1/flows/", json=flow_payload, headers=logged_in_headers)
comp_response = await client.post("v1/flows/", json=component_payload, headers=logged_in_headers)
assert flow_response.status_code == status.HTTP_201_CREATED
assert comp_response.status_code == status.HTTP_201_CREATED
# Test with filtering params but no pagination (should use non-paginated path)
response = await client.get(f"api/v1/projects/{project_id}?is_flow=true", headers=logged_in_headers)
response = await client.get(f"v1/projects/{project_id}?is_flow=true", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -1489,7 +1489,7 @@ class TestReadProjectBugFix:
# Test with filtering params AND pagination (should use paginated path)
response = await client.get(
f"api/v1/projects/{project_id}?is_flow=true&page=1&size=10", headers=logged_in_headers
f"v1/projects/{project_id}?is_flow=true&page=1&size=10", headers=logged_in_headers
)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -1503,7 +1503,7 @@ class TestReadProjectBugFix:
async def test_read_project_consistent_response_structure(self, client: AsyncClient, logged_in_headers, basic_case):
"""Test that read_project returns consistent response structure in all cases."""
# Create a project first
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
project_id = create_response.json()["id"]
@@ -1528,7 +1528,7 @@ class TestReadProjectBugFix:
]
for test_case in test_cases:
response = await client.get(f"api/v1/projects/{project_id}{test_case['params']}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{project_id}{test_case['params']}", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK, f"Failed for params: {test_case['params']}"
result = response.json()
@@ -1562,7 +1562,7 @@ class TestReadProjectBugFix:
]
for params in test_cases:
response = await client.get(f"api/v1/projects/{non_existent_id}{params}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{non_existent_id}{params}", headers=logged_in_headers)
assert response.status_code == status.HTTP_404_NOT_FOUND, f"Should return 404 for params: {params}"
result = response.json()
@@ -1590,7 +1590,7 @@ async def test_download_file_starter_project(client: AsyncClient, logged_in_head
"flows_list": [],
"components_list": [],
}
create_response = await client.post("api/v1/projects/", json=project_payload, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=project_payload, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
starter_project = create_response.json()
starter_project_id = starter_project["id"]
@@ -1670,7 +1670,7 @@ async def test_download_file_starter_project(client: AsyncClient, logged_in_head
# Download the starter project
response = await client.get(
f"api/v1/projects/download/{starter_project_id}",
f"v1/projects/download/{starter_project_id}",
headers=logged_in_headers,
)
@@ -1742,23 +1742,23 @@ async def test_download_file_starter_project(client: AsyncClient, logged_in_head
assert api_template["regular_field"]["value"] == "keep-this", "Regular field value should be kept"
# Clean up: delete the project (which will cascade delete flows)
delete_response = await client.delete(f"api/v1/projects/{starter_project_id}", headers=logged_in_headers)
delete_response = await client.delete(f"v1/projects/{starter_project_id}", headers=logged_in_headers)
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
async def test_download_project_missing_returns_404(client: AsyncClient, logged_in_headers):
response = await client.get(f"api/v1/projects/download/{uuid4()}", headers=logged_in_headers)
response = await client.get(f"v1/projects/download/{uuid4()}", headers=logged_in_headers)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["detail"] == "Project not found"
async def test_download_project_with_no_flows_returns_404(client: AsyncClient, logged_in_headers, basic_case):
response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
project_id = response.json()["id"]
download_response = await client.get(f"api/v1/projects/download/{project_id}", headers=logged_in_headers)
download_response = await client.get(f"v1/projects/download/{project_id}", headers=logged_in_headers)
assert download_response.status_code == status.HTTP_404_NOT_FOUND
assert download_response.json()["detail"] == "No flows found in project"
@@ -1767,7 +1767,7 @@ async def test_download_project_with_no_flows_returns_404(client: AsyncClient, l
async def test_download_project_sanitizes_windows_path_characters(
client: AsyncClient, logged_in_headers, basic_case, active_user
):
create_response = await client.post("api/v1/projects/", json=basic_case, headers=logged_in_headers)
create_response = await client.post("v1/projects/", json=basic_case, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
project_id = create_response.json()["id"]
@@ -1785,7 +1785,7 @@ async def test_download_project_sanitizes_windows_path_characters(
await session.refresh(flow)
await session.commit()
response = await client.get(f"api/v1/projects/download/{project_id}", headers=logged_in_headers)
response = await client.get(f"v1/projects/download/{project_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
with zipfile.ZipFile(io.BytesIO(response.content), "r") as zip_file:
@@ -1816,7 +1816,7 @@ async def _create_other_user(client: AsyncClient) -> tuple[str, dict]:
created_id = str(user.id)
response = await client.post(
"api/v1/login", data={"username": username, "password": "testpassword"}
"v1/login", data={"username": username, "password": "testpassword"}
) # pragma: allowlist secret
assert response.status_code == 200
token = response.json()["access_token"]
@@ -1875,7 +1875,7 @@ async def test_create_project_does_not_reassign_other_users_flows(
_, other_user_headers = await _create_other_user(client)
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "user-flow", "data": {}},
headers=logged_in_headers,
)
@@ -1884,18 +1884,18 @@ async def test_create_project_does_not_reassign_other_users_flows(
original_folder_id = flow_resp.json()["folder_id"]
proj_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "other-project", "flows_list": [flow_id]},
headers=other_user_headers,
)
assert proj_resp.status_code == status.HTTP_201_CREATED
other_project_id = proj_resp.json()["id"]
flow_after = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
flow_after = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
assert flow_after.status_code == status.HTTP_200_OK
assert flow_after.json()["folder_id"] == original_folder_id
proj_detail = await client.get(f"api/v1/projects/{other_project_id}", headers=other_user_headers)
proj_detail = await client.get(f"v1/projects/{other_project_id}", headers=other_user_headers)
assert proj_detail.status_code == status.HTTP_200_OK
assert len(proj_detail.json().get("flows", [])) == 0
@@ -1908,7 +1908,7 @@ async def test_read_project_paginated_only_returns_current_users_flows(
_, other_user_headers = await _create_other_user(client)
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "user-flow-paginated", "data": {}},
headers=logged_in_headers,
)
@@ -1917,7 +1917,7 @@ async def test_read_project_paginated_only_returns_current_users_flows(
original_folder_id = flow_resp.json()["folder_id"]
proj_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "other-project-paginated", "flows_list": [flow_id]},
headers=other_user_headers,
)
@@ -1925,7 +1925,7 @@ async def test_read_project_paginated_only_returns_current_users_flows(
other_project_id = proj_resp.json()["id"]
paginated = await client.get(
f"api/v1/projects/{other_project_id}",
f"v1/projects/{other_project_id}",
params={"page": 1, "size": 50},
headers=other_user_headers,
)
@@ -1933,7 +1933,7 @@ async def test_read_project_paginated_only_returns_current_users_flows(
items = paginated.json().get("flows", {}).get("items", [])
assert all(item["id"] != flow_id for item in items)
flow_after = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
flow_after = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
assert flow_after.json()["folder_id"] == original_folder_id
@@ -1943,7 +1943,7 @@ async def test_create_project_with_own_flows_assigns_them_correctly(
):
"""Test that flows_list in create_project correctly assigns flows owned by the requesting user."""
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "my-flow", "data": {}},
headers=logged_in_headers,
)
@@ -1951,19 +1951,19 @@ async def test_create_project_with_own_flows_assigns_them_correctly(
flow_id = flow_resp.json()["id"]
proj_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "my-project", "flows_list": [flow_id]},
headers=logged_in_headers,
)
assert proj_resp.status_code == status.HTTP_201_CREATED
project_id = proj_resp.json()["id"]
flow_after = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
flow_after = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
assert flow_after.status_code == status.HTTP_200_OK
assert flow_after.json()["folder_id"] == project_id
paginated = await client.get(
f"api/v1/projects/{project_id}",
f"v1/projects/{project_id}",
params={"page": 1, "size": 50},
headers=logged_in_headers,
)
@@ -1978,7 +1978,7 @@ async def test_create_project_with_deployed_flow_returns_409_guard(
active_user,
):
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={"name": "deployed-flow", "data": {"nodes": [], "edges": []}},
headers=logged_in_headers,
)
@@ -1994,7 +1994,7 @@ async def test_create_project_with_deployed_flow_returns_409_guard(
)
create_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "new-target", "flows_list": [str(flow_id)], "components_list": []},
headers=logged_in_headers,
)
@@ -2008,7 +2008,7 @@ async def test_update_project_with_deployed_component_returns_409_guard(
active_user,
):
project_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "component-project", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -2016,7 +2016,7 @@ async def test_update_project_with_deployed_component_returns_409_guard(
project_id = project_resp.json()["id"]
component_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={
"name": "deployed-component",
"folder_id": project_id,
@@ -2035,7 +2035,7 @@ async def test_update_project_with_deployed_component_returns_409_guard(
)
update_resp = await client.patch(
f"api/v1/projects/{project_id}",
f"v1/projects/{project_id}",
json={"name": "component-project-renamed"},
headers=logged_in_headers,
)
@@ -2049,7 +2049,7 @@ async def test_delete_project_with_deployments_returns_409_project_guard(
active_user,
):
project_resp = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": "delete-guard-project", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -2057,7 +2057,7 @@ async def test_delete_project_with_deployments_returns_409_project_guard(
project_id = project_resp.json()["id"]
flow_resp = await client.post(
"api/v1/flows/",
"v1/flows/",
json={
"name": "delete-guard-flow",
"folder_id": project_id,
@@ -2074,7 +2074,7 @@ async def test_delete_project_with_deployments_returns_409_project_guard(
project_id=UUID(project_id),
)
delete_resp = await client.delete(f"api/v1/projects/{project_id}", headers=logged_in_headers)
delete_resp = await client.delete(f"v1/projects/{project_id}", headers=logged_in_headers)
assert delete_resp.status_code == status.HTTP_409_CONFLICT
detail = delete_resp.json()["detail"]
assert "project cannot be deleted because it has deployments" in detail.lower()
@@ -14,17 +14,17 @@ async def test_duplicate_flow_name_basic(client: AsyncClient, logged_in_headers)
}
# Create first flow
response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED
assert response1.json()["name"] == "Test Flow"
# Create second flow with same name - should become "Test Flow (1)"
response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response2 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response2.status_code == status.HTTP_201_CREATED
assert response2.json()["name"] == "Test Flow (1)"
# Create third flow with same name - should become "Test Flow (2)"
response3 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response3 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response3.status_code == status.HTTP_201_CREATED
assert response3.json()["name"] == "Test Flow (2)"
@@ -40,17 +40,17 @@ async def test_duplicate_flow_name_with_numbers_in_original(client: AsyncClient,
}
# Create first flow
response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED
assert response1.json()["name"] == "Untitled document (7)"
# Create second flow with same name - should become "Untitled document (7) (1)"
response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response2 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response2.status_code == status.HTTP_201_CREATED
assert response2.json()["name"] == "Untitled document (7) (1)"
# Create third flow with same name - should become "Untitled document (7) (2)"
response3 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response3 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response3.status_code == status.HTTP_201_CREATED
assert response3.json()["name"] == "Untitled document (7) (2)"
@@ -66,20 +66,20 @@ async def test_duplicate_flow_name_with_non_numeric_suffixes(client: AsyncClient
}
# Create first flow
response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED
assert response1.json()["name"] == "My Flow"
# Create flow with non-numeric suffix
backup_flow = base_flow.copy()
backup_flow["name"] = "My Flow (Backup)"
response2 = await client.post("api/v1/flows/", json=backup_flow, headers=logged_in_headers)
response2 = await client.post("v1/flows/", json=backup_flow, headers=logged_in_headers)
assert response2.status_code == status.HTTP_201_CREATED
assert response2.json()["name"] == "My Flow (Backup)"
# Create another flow with original name - should become "My Flow (1)"
# because "My Flow (Backup)" doesn't match the numeric pattern
response3 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response3 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response3.status_code == status.HTTP_201_CREATED
assert response3.json()["name"] == "My Flow (1)"
@@ -95,7 +95,7 @@ async def test_duplicate_flow_name_gaps_in_numbering(client: AsyncClient, logged
}
# Create original flow
response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED
assert response1.json()["name"] == "Gapped Flow"
@@ -109,12 +109,12 @@ async def test_duplicate_flow_name_gaps_in_numbering(client: AsyncClient, logged
for flow_name in numbered_flows:
numbered_flow = base_flow.copy()
numbered_flow["name"] = flow_name
response = await client.post("api/v1/flows/", json=numbered_flow, headers=logged_in_headers)
response = await client.post("v1/flows/", json=numbered_flow, headers=logged_in_headers)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == flow_name
# Create another duplicate - should use max(1,5,7) + 1 = 8
response_final = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response_final = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response_final.status_code == status.HTTP_201_CREATED
assert response_final.json()["name"] == "Gapped Flow (8)"
@@ -130,12 +130,12 @@ async def test_duplicate_flow_name_special_characters(client: AsyncClient, logge
}
# Create first flow
response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED
assert response1.json()["name"] == "Flow-with_special@chars!"
# Create duplicate - should properly escape special characters in regex
response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response2 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response2.status_code == status.HTTP_201_CREATED
assert response2.json()["name"] == "Flow-with_special@chars! (1)"
@@ -151,11 +151,11 @@ async def test_duplicate_flow_name_regex_patterns(client: AsyncClient, logged_in
}
# Create first flow
response1 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response1.status_code == status.HTTP_201_CREATED
assert response1.json()["name"] == "Flow (.*) [test]"
# Create duplicate
response2 = await client.post("api/v1/flows/", json=base_flow, headers=logged_in_headers)
response2 = await client.post("v1/flows/", json=base_flow, headers=logged_in_headers)
assert response2.status_code == status.HTTP_201_CREATED
assert response2.json()["name"] == "Flow (.*) [test] (1)"
@@ -3,7 +3,7 @@ from httpx import AsyncClient
async def test_get_starter_projects(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/starter-projects/", headers=logged_in_headers)
response = await client.get("v1/starter-projects/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK, response.text
+1 -1
View File
@@ -3,7 +3,7 @@ from httpx import AsyncClient
async def test_check_if_store_is_enabled(client: AsyncClient):
response = await client.get("api/v1/store/check/")
response = await client.get("v1/store/check/")
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -465,14 +465,14 @@ class TestTransactionsEndpoint:
async def test_get_transactions_requires_auth(self, client: AsyncClient):
"""Test that GET /monitor/transactions requires authentication."""
response = await client.get("api/v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000")
response = await client.get("v1/monitor/transactions?flow_id=00000000-0000-0000-0000-000000000000")
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.usefixtures("active_user")
async def test_get_transactions_returns_paginated_response(self, client: AsyncClient, logged_in_headers):
"""Test that GET /monitor/transactions returns paginated response."""
flow_id = "00000000-0000-0000-0000-000000000000"
response = await client.get(f"api/v1/monitor/transactions?flow_id={flow_id}", headers=logged_in_headers)
response = await client.get(f"v1/monitor/transactions?flow_id={flow_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
@@ -488,7 +488,7 @@ class TestTransactionsEndpoint:
"""Test GET /monitor/transactions with custom pagination parameters."""
flow_id = "00000000-0000-0000-0000-000000000000"
response = await client.get(
f"api/v1/monitor/transactions?flow_id={flow_id}&page=1&size=10", headers=logged_in_headers
f"v1/monitor/transactions?flow_id={flow_id}&page=1&size=10", headers=logged_in_headers
)
assert response.status_code == status.HTTP_200_OK
@@ -499,20 +499,20 @@ class TestTransactionsEndpoint:
@pytest.mark.usefixtures("active_user")
async def test_get_transactions_requires_flow_id(self, client: AsyncClient, logged_in_headers):
"""Test that GET /monitor/transactions requires flow_id parameter."""
response = await client.get("api/v1/monitor/transactions", headers=logged_in_headers)
response = await client.get("v1/monitor/transactions", headers=logged_in_headers)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.usefixtures("active_user")
async def test_get_transactions_invalid_flow_id_format(self, client: AsyncClient, logged_in_headers):
"""Test GET /monitor/transactions with invalid flow_id format."""
response = await client.get("api/v1/monitor/transactions?flow_id=invalid-uuid", headers=logged_in_headers)
response = await client.get("v1/monitor/transactions?flow_id=invalid-uuid", headers=logged_in_headers)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.usefixtures("active_user")
async def test_get_transactions_response_structure(self, client: AsyncClient, logged_in_headers):
"""Test that transaction response items have the expected structure."""
flow_id = uuid4()
response = await client.get(f"api/v1/monitor/transactions?flow_id={flow_id}", headers=logged_in_headers)
response = await client.get(f"v1/monitor/transactions?flow_id={flow_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_200_OK
result = response.json()
+22 -22
View File
@@ -5,7 +5,7 @@ from httpx import AsyncClient
async def test_add_user_public_signup(client: AsyncClient):
"""Test public user registration (sign up) without authentication."""
basic_case = {"username": "newuser", "password": "newpassword123"}
response = await client.post("api/v1/users/", json=basic_case)
response = await client.post("v1/users/", json=basic_case)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -26,18 +26,18 @@ async def test_add_user_duplicate_username(client: AsyncClient):
"""Test that duplicate usernames are rejected."""
basic_case = {"username": "duplicateuser", "password": "password123"}
# Create first user
response1 = await client.post("api/v1/users/", json=basic_case)
response1 = await client.post("v1/users/", json=basic_case)
assert response1.status_code == status.HTTP_201_CREATED
# Try to create second user with same username
response2 = await client.post("api/v1/users/", json=basic_case)
response2 = await client.post("v1/users/", json=basic_case)
assert response2.status_code == status.HTTP_400_BAD_REQUEST
assert "unavailable" in response2.json()["detail"].lower()
async def test_add_user(client: AsyncClient, logged_in_headers_super_user):
basic_case = {"username": "string", "password": "string"}
response = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
response = await client.post("v1/users/", json=basic_case, headers=logged_in_headers_super_user)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -53,7 +53,7 @@ async def test_add_user(client: AsyncClient, logged_in_headers_super_user):
async def test_read_current_user(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/users/whoami", headers=logged_in_headers)
response = await client.get("v1/users/whoami", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -69,7 +69,7 @@ async def test_read_current_user(client: AsyncClient, logged_in_headers):
async def test_read_all_users(client: AsyncClient, logged_in_headers_super_user):
response = await client.get("api/v1/users/", headers=logged_in_headers_super_user)
response = await client.get("v1/users/", headers=logged_in_headers_super_user)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -82,10 +82,10 @@ async def test_patch_user(client: AsyncClient, logged_in_headers_super_user):
name = "string"
updated_name = "string2"
basic_case = {"username": name, "password": "string"}
response_ = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
response_ = await client.post("v1/users/", json=basic_case, headers=logged_in_headers_super_user)
id_ = response_.json()["id"]
basic_case["username"] = updated_name
response = await client.patch(f"api/v1/users/{id_}", json=basic_case, headers=logged_in_headers_super_user)
response = await client.patch(f"v1/users/{id_}", json=basic_case, headers=logged_in_headers_super_user)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -104,7 +104,7 @@ async def test_patch_user(client: AsyncClient, logged_in_headers_super_user):
async def test_reset_password(client: AsyncClient, logged_in_headers, active_user):
id_ = str(active_user.id)
basic_case = {"username": "string", "password": "new_password"}
response = await client.patch(f"api/v1/users/{id_}/reset-password", json=basic_case, headers=logged_in_headers)
response = await client.patch(f"v1/users/{id_}/reset-password", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -121,9 +121,9 @@ async def test_reset_password(client: AsyncClient, logged_in_headers, active_use
async def test_delete_user(client: AsyncClient, logged_in_headers_super_user):
basic_case = {"username": "string", "password": "string"}
response_ = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
response_ = await client.post("v1/users/", json=basic_case, headers=logged_in_headers_super_user)
id_ = response_.json()["id"]
response = await client.delete(f"api/v1/users/{id_}", headers=logged_in_headers_super_user)
response = await client.delete(f"v1/users/{id_}", headers=logged_in_headers_super_user)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -135,7 +135,7 @@ async def test_patch_user_self_deactivation_forbidden(client: AsyncClient, logge
"""Test that a user cannot deactivate their own account."""
user_id = str(active_user.id)
response = await client.patch(
f"api/v1/users/{user_id}",
f"v1/users/{user_id}",
json={"is_active": False},
headers=logged_in_headers,
)
@@ -151,7 +151,7 @@ async def test_patch_user_self_deactivation_forbidden_superuser(
"""Test that even a superuser cannot deactivate their own account."""
user_id = str(active_super_user.id)
response = await client.patch(
f"api/v1/users/{user_id}",
f"v1/users/{user_id}",
json={"is_active": False},
headers=logged_in_headers_super_user,
)
@@ -168,7 +168,7 @@ async def test_read_all_users_search(client: AsyncClient, logged_in_headers_supe
created_ids = []
for username in usernames:
response = await client.post(
"api/v1/users/",
"v1/users/",
json={"username": username, "password": "password123"},
headers=logged_in_headers_super_user,
)
@@ -177,7 +177,7 @@ async def test_read_all_users_search(client: AsyncClient, logged_in_headers_supe
# Search for "alice" — should return exactly one match
response = await client.get(
"api/v1/users/?search=alice",
"v1/users/?search=alice",
headers=logged_in_headers_super_user,
)
assert response.status_code == status.HTTP_200_OK
@@ -187,7 +187,7 @@ async def test_read_all_users_search(client: AsyncClient, logged_in_headers_supe
# Search for "_search" — should match all three created users
response = await client.get(
"api/v1/users/?search=_search",
"v1/users/?search=_search",
headers=logged_in_headers_super_user,
)
assert response.status_code == status.HTTP_200_OK
@@ -198,7 +198,7 @@ async def test_read_all_users_search(client: AsyncClient, logged_in_headers_supe
# Search for a non-existent username — should return zero results
response = await client.get(
"api/v1/users/?search=nonexistentuser",
"v1/users/?search=nonexistentuser",
headers=logged_in_headers_super_user,
)
assert response.status_code == status.HTTP_200_OK
@@ -208,7 +208,7 @@ async def test_read_all_users_search(client: AsyncClient, logged_in_headers_supe
# Search is case-insensitive
response = await client.get(
"api/v1/users/?search=ALICE",
"v1/users/?search=ALICE",
headers=logged_in_headers_super_user,
)
assert response.status_code == status.HTTP_200_OK
@@ -218,7 +218,7 @@ async def test_read_all_users_search(client: AsyncClient, logged_in_headers_supe
# Search combined with pagination: limit=1 should return 1 user but total_count=3
response = await client.get(
"api/v1/users/?search=_search&limit=1",
"v1/users/?search=_search&limit=1",
headers=logged_in_headers_super_user,
)
assert response.status_code == status.HTTP_200_OK
@@ -228,20 +228,20 @@ async def test_read_all_users_search(client: AsyncClient, logged_in_headers_supe
# Clean up
for user_id in created_ids:
await client.delete(f"api/v1/users/{user_id}", headers=logged_in_headers_super_user)
await client.delete(f"v1/users/{user_id}", headers=logged_in_headers_super_user)
async def test_patch_user_deactivate_other_user_allowed(client: AsyncClient, logged_in_headers_super_user):
"""Test that a superuser can deactivate another user's account."""
# Create a new user
basic_case = {"username": "user_to_deactivate", "password": "password123"}
create_response = await client.post("api/v1/users/", json=basic_case, headers=logged_in_headers_super_user)
create_response = await client.post("v1/users/", json=basic_case, headers=logged_in_headers_super_user)
assert create_response.status_code == status.HTTP_201_CREATED
user_id = create_response.json()["id"]
# Deactivate the other user
response = await client.patch(
f"api/v1/users/{user_id}",
f"v1/users/{user_id}",
json={"is_active": False},
headers=logged_in_headers_super_user,
)
@@ -10,7 +10,7 @@ from pprint import pprint
var = {"a": 1, "b": 2}
pprint(var)
"""
response = await client.post("api/v1/validate/code", json={"code": good_code}, headers=logged_in_headers)
response = await client.post("v1/validate/code", json={"code": good_code}, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -51,7 +51,7 @@ async def test_post_validate_prompt(client: AsyncClient, logged_in_headers):
"metadata": {},
},
}
response = await client.post("api/v1/validate/prompt", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/validate/prompt", json=basic_case, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -67,7 +67,7 @@ async def test_post_validate_prompt_with_invalid_data(client: AsyncClient, logge
# Missing required fields
"frontend_node": {"template": {}, "is_input": True},
}
response = await client.post("api/v1/validate/prompt", json=invalid_case, headers=logged_in_headers)
response = await client.post("v1/validate/prompt", json=invalid_case, headers=logged_in_headers)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@@ -75,5 +75,5 @@ async def test_post_validate_code_with_unauthenticated_user(client: AsyncClient)
code = """
print("Hello World")
"""
response = await client.post("api/v1/validate/code", json={"code": code}, headers={"Authorization": "Bearer fake"})
response = await client.post("v1/validate/code", json={"code": code}, headers={"Authorization": "Bearer fake"})
assert response.status_code == status.HTTP_401_UNAUTHORIZED
+61 -61
View File
@@ -30,7 +30,7 @@ def credential_variable():
@pytest.mark.usefixtures("active_user")
async def test_create_variable(client: AsyncClient, generic_variable, logged_in_headers):
response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -44,9 +44,9 @@ async def test_create_variable(client: AsyncClient, generic_variable, logged_in_
@pytest.mark.usefixtures("active_user")
async def test_create_variable__variable_name_already_exists(client: AsyncClient, generic_variable, logged_in_headers):
await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -60,7 +60,7 @@ async def test_create_variable__variable_name_and_value_cannot_be_empty(
generic_variable["name"] = ""
generic_variable["value"] = ""
response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -71,7 +71,7 @@ async def test_create_variable__variable_name_and_value_cannot_be_empty(
async def test_create_variable__variable_name_cannot_be_empty(client: AsyncClient, generic_variable, logged_in_headers):
generic_variable["name"] = ""
response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -84,7 +84,7 @@ async def test_create_variable__variable_value_cannot_be_empty(
):
generic_variable["value"] = ""
response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -98,7 +98,7 @@ async def test_create_variable__httpexception(client: AsyncClient, credential_va
with mock.patch("flow.services.auth.utils.encrypt_api_key") as m:
m.side_effect = HTTPException(status_code=status_code, detail=generic_message)
response = await client.post("api/v1/variables/", json=credential_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=credential_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_418_IM_A_TEAPOT
@@ -111,7 +111,7 @@ async def test_create_variable__exception(client: AsyncClient, credential_variab
with mock.patch("flow.services.auth.utils.encrypt_api_key") as m:
m.side_effect = Exception(generic_message)
response = await client.post("api/v1/variables/", json=credential_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=credential_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -121,14 +121,14 @@ async def test_create_variable__exception(client: AsyncClient, credential_variab
@pytest.mark.usefixtures("active_user")
async def test_read_variables(client: AsyncClient, generic_variable, credential_variable, logged_in_headers):
# Create a generic variable
create_response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
# Create a credential variable
create_response = await client.post("api/v1/variables/", json=credential_variable, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=credential_variable, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
response = await client.get("api/v1/variables/", headers=logged_in_headers)
response = await client.get("v1/variables/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -150,12 +150,12 @@ async def test_read_variables(client: AsyncClient, generic_variable, credential_
@pytest.mark.usefixtures("active_user")
async def test_read_variables__empty(client: AsyncClient, logged_in_headers):
all_variables = await client.get("api/v1/variables/", headers=logged_in_headers)
all_variables = await client.get("v1/variables/", headers=logged_in_headers)
all_variables = all_variables.json()
for variable in all_variables:
await client.delete(f"api/v1/variables/{variable.get('id')}", headers=logged_in_headers)
await client.delete(f"v1/variables/{variable.get('id')}", headers=logged_in_headers)
response = await client.get("api/v1/variables/", headers=logged_in_headers)
response = await client.get("v1/variables/", headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_200_OK
@@ -172,7 +172,7 @@ async def test_read_variables__(client: AsyncClient, logged_in_headers):
new_callable=mock.AsyncMock,
side_effect=Exception(generic_message),
):
response = await client.get("api/v1/variables/", headers=logged_in_headers)
response = await client.get("v1/variables/", headers=logged_in_headers)
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
assert generic_message in response.json().get("detail", "")
@@ -180,7 +180,7 @@ async def test_read_variables__(client: AsyncClient, logged_in_headers):
@pytest.mark.usefixtures("active_user")
async def test_update_variable(client: AsyncClient, generic_variable, logged_in_headers):
saved = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
saved = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
saved = saved.json()
generic_variable["id"] = saved.get("id")
generic_variable["name"] = "new_name"
@@ -189,7 +189,7 @@ async def test_update_variable(client: AsyncClient, generic_variable, logged_in_
generic_variable["default_fields"] = ["new_field"]
response = await client.patch(
f"api/v1/variables/{saved.get('id')}", json=generic_variable, headers=logged_in_headers
f"v1/variables/{saved.get('id')}", json=generic_variable, headers=logged_in_headers
)
result = response.json()
@@ -204,7 +204,7 @@ async def test_update_variable__exception(client: AsyncClient, generic_variable,
wrong_id = uuid4()
generic_variable["id"] = str(wrong_id)
response = await client.patch(f"api/v1/variables/{wrong_id}", json=generic_variable, headers=logged_in_headers)
response = await client.patch(f"v1/variables/{wrong_id}", json=generic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_404_NOT_FOUND
@@ -213,9 +213,9 @@ async def test_update_variable__exception(client: AsyncClient, generic_variable,
@pytest.mark.usefixtures("active_user")
async def test_delete_variable(client: AsyncClient, generic_variable, logged_in_headers):
response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
saved = response.json()
response = await client.delete(f"api/v1/variables/{saved.get('id')}", headers=logged_in_headers)
response = await client.delete(f"v1/variables/{saved.get('id')}", headers=logged_in_headers)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -224,7 +224,7 @@ async def test_delete_variable(client: AsyncClient, generic_variable, logged_in_
async def test_delete_variable__exception(client: AsyncClient, logged_in_headers):
wrong_id = uuid4()
response = await client.delete(f"api/v1/variables/{wrong_id}", headers=logged_in_headers)
response = await client.delete(f"v1/variables/{wrong_id}", headers=logged_in_headers)
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -233,10 +233,10 @@ async def test_delete_variable__exception(client: AsyncClient, logged_in_headers
async def test_create_variable__openai_api_key_validation_success(client: AsyncClient, logged_in_headers):
"""Test successful OpenAI API key validation."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "OPENAI_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
openai_variable = {
"name": "OPENAI_API_KEY",
@@ -248,7 +248,7 @@ async def test_create_variable__openai_api_key_validation_success(client: AsyncC
# Mock successful OpenAI API call
with mock.patch("langchain_openai.ChatOpenAI.invoke") as mock_invoke:
mock_invoke.return_value = "test response"
response = await client.post("api/v1/variables/", json=openai_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=openai_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -260,10 +260,10 @@ async def test_create_variable__openai_api_key_validation_success(client: AsyncC
async def test_create_variable__openai_api_key_validation_failure(client: AsyncClient, logged_in_headers):
"""Test failed OpenAI API key validation."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "OPENAI_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
openai_variable = {
"name": "OPENAI_API_KEY",
@@ -275,7 +275,7 @@ async def test_create_variable__openai_api_key_validation_failure(client: AsyncC
# Mock failed OpenAI API call with authentication error
with mock.patch("langchain_openai.ChatOpenAI.invoke") as mock_invoke:
mock_invoke.side_effect = Exception("401 authentication failed")
response = await client.post("api/v1/variables/", json=openai_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=openai_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -286,10 +286,10 @@ async def test_create_variable__openai_api_key_validation_failure(client: AsyncC
async def test_create_variable__anthropic_api_key_validation_success(client: AsyncClient, logged_in_headers):
"""Test successful Anthropic API key validation."""
# Clean up any existing ANTHROPIC_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "ANTHROPIC_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
anthropic_variable = {
"name": "ANTHROPIC_API_KEY",
@@ -301,7 +301,7 @@ async def test_create_variable__anthropic_api_key_validation_success(client: Asy
# Mock successful Anthropic API call
with mock.patch("langchain_anthropic.ChatAnthropic.invoke") as mock_invoke:
mock_invoke.return_value = "test response"
response = await client.post("api/v1/variables/", json=anthropic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=anthropic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -313,10 +313,10 @@ async def test_create_variable__anthropic_api_key_validation_success(client: Asy
async def test_create_variable__anthropic_api_key_validation_failure(client: AsyncClient, logged_in_headers):
"""Test failed Anthropic API key validation."""
# Clean up any existing ANTHROPIC_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "ANTHROPIC_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
anthropic_variable = {
"name": "ANTHROPIC_API_KEY",
@@ -328,7 +328,7 @@ async def test_create_variable__anthropic_api_key_validation_failure(client: Asy
# Mock failed Anthropic API call with authentication error
with mock.patch("langchain_anthropic.ChatAnthropic.invoke") as mock_invoke:
mock_invoke.side_effect = Exception("Invalid API key provided")
response = await client.post("api/v1/variables/", json=anthropic_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=anthropic_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -339,10 +339,10 @@ async def test_create_variable__anthropic_api_key_validation_failure(client: Asy
async def test_create_variable__google_api_key_validation_success(client: AsyncClient, logged_in_headers):
"""Test successful Google API key validation."""
# Clean up any existing GOOGLE_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "GOOGLE_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
google_variable = {
"name": "GOOGLE_API_KEY",
@@ -354,7 +354,7 @@ async def test_create_variable__google_api_key_validation_success(client: AsyncC
# Mock successful Google API call
with mock.patch("langchain_google_genai.ChatGoogleGenerativeAI.invoke") as mock_invoke:
mock_invoke.return_value = "test response"
response = await client.post("api/v1/variables/", json=google_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=google_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -366,10 +366,10 @@ async def test_create_variable__google_api_key_validation_success(client: AsyncC
async def test_create_variable__ollama_base_url_validation_success(client: AsyncClient, logged_in_headers):
"""Test successful Ollama base URL validation."""
# Clean up any existing OLLAMA_BASE_URL variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "OLLAMA_BASE_URL":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
ollama_variable = {
"name": "OLLAMA_BASE_URL",
@@ -382,7 +382,7 @@ async def test_create_variable__ollama_base_url_validation_success(client: Async
with mock.patch("requests.get") as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"models": []}
response = await client.post("api/v1/variables/", json=ollama_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=ollama_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_201_CREATED
@@ -394,10 +394,10 @@ async def test_create_variable__ollama_base_url_validation_success(client: Async
async def test_create_variable__ollama_base_url_validation_failure(client: AsyncClient, logged_in_headers):
"""Test failed Ollama base URL validation."""
# Clean up any existing OLLAMA_BASE_URL variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "OLLAMA_BASE_URL":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
ollama_variable = {
"name": "OLLAMA_BASE_URL",
@@ -409,7 +409,7 @@ async def test_create_variable__ollama_base_url_validation_failure(client: Async
# Mock failed Ollama API call
with mock.patch("requests.get") as mock_get:
mock_get.return_value.status_code = 404
response = await client.post("api/v1/variables/", json=ollama_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=ollama_variable, headers=logged_in_headers)
result = response.json()
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -420,10 +420,10 @@ async def test_create_variable__ollama_base_url_validation_failure(client: Async
async def test_create_variable__model_provider_network_error_allows_creation(client: AsyncClient, logged_in_headers):
"""Test that network errors don't prevent variable creation."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "OPENAI_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
openai_variable = {
"name": "OPENAI_API_KEY",
@@ -435,7 +435,7 @@ async def test_create_variable__model_provider_network_error_allows_creation(cli
# Mock network error (not an auth error)
with mock.patch("langchain_openai.ChatOpenAI.invoke") as mock_invoke:
mock_invoke.side_effect = Exception("Network timeout")
response = await client.post("api/v1/variables/", json=openai_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=openai_variable, headers=logged_in_headers)
# Should succeed despite network error
assert response.status_code == status.HTTP_201_CREATED
@@ -445,10 +445,10 @@ async def test_create_variable__model_provider_network_error_allows_creation(cli
async def test_delete_provider_credential_cleans_up_disabled_models(client: AsyncClient, logged_in_headers):
"""Test that deleting a provider credential cleans up disabled models for that provider."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "OPENAI_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
openai_variable = {
"name": "OPENAI_API_KEY",
@@ -460,14 +460,14 @@ async def test_delete_provider_credential_cleans_up_disabled_models(client: Asyn
# Mock successful OpenAI API call to create credential
with mock.patch("langchain_openai.ChatOpenAI.invoke") as mock_invoke:
mock_invoke.return_value = "test response"
create_response = await client.post("api/v1/variables/", json=openai_variable, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=openai_variable, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
created_var = create_response.json()
# Disable some OpenAI models
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key"):
disable_response = await client.post(
"api/v1/models/enabled_models",
"v1/models/enabled_models",
json=[
{"provider": "OpenAI", "model_id": "gpt-4", "enabled": False},
{"provider": "OpenAI", "model_id": "gpt-3.5-turbo", "enabled": False},
@@ -477,11 +477,11 @@ async def test_delete_provider_credential_cleans_up_disabled_models(client: Asyn
assert disable_response.status_code == status.HTTP_200_OK
# Delete the credential - should clean up disabled models
delete_response = await client.delete(f"api/v1/variables/{created_var['id']}", headers=logged_in_headers)
delete_response = await client.delete(f"v1/variables/{created_var['id']}", headers=logged_in_headers)
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
# Verify disabled models are cleaned up - check that the disabled models variable is gone or cleared
all_vars_after = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars_after = await client.get("v1/variables/", headers=logged_in_headers)
disabled_models_var = next(
(v for v in all_vars_after.json() if v.get("name") == "__disabled_models__"),
None,
@@ -499,10 +499,10 @@ async def test_delete_provider_credential_cleans_up_disabled_models(client: Asyn
async def test_delete_provider_credential_cleans_up_enabled_models(client: AsyncClient, logged_in_headers):
"""Test that deleting a provider credential cleans up explicitly enabled models for that provider."""
# Clean up any existing OPENAI_API_KEY variables
all_vars = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars = await client.get("v1/variables/", headers=logged_in_headers)
for var in all_vars.json():
if var.get("name") == "OPENAI_API_KEY":
await client.delete(f"api/v1/variables/{var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{var['id']}", headers=logged_in_headers)
openai_variable = {
"name": "OPENAI_API_KEY",
@@ -514,14 +514,14 @@ async def test_delete_provider_credential_cleans_up_enabled_models(client: Async
# Mock successful OpenAI API call to create credential
with mock.patch("langchain_openai.ChatOpenAI.invoke") as mock_invoke:
mock_invoke.return_value = "test response"
create_response = await client.post("api/v1/variables/", json=openai_variable, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=openai_variable, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
created_var = create_response.json()
# Enable some non-default OpenAI models (explicitly enable models that aren't default)
with mock.patch("lfx.base.models.unified_models.validate_model_provider_key"):
enable_response = await client.post(
"api/v1/models/enabled_models",
"v1/models/enabled_models",
json=[
{"provider": "OpenAI", "model_id": "gpt-4-turbo-preview", "enabled": True},
],
@@ -530,11 +530,11 @@ async def test_delete_provider_credential_cleans_up_enabled_models(client: Async
assert enable_response.status_code == status.HTTP_200_OK
# Delete the credential - should clean up enabled models
delete_response = await client.delete(f"api/v1/variables/{created_var['id']}", headers=logged_in_headers)
delete_response = await client.delete(f"v1/variables/{created_var['id']}", headers=logged_in_headers)
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
# Verify enabled models are cleaned up
all_vars_after = await client.get("api/v1/variables/", headers=logged_in_headers)
all_vars_after = await client.get("v1/variables/", headers=logged_in_headers)
enabled_models_var = next(
(v for v in all_vars_after.json() if v.get("name") == "__enabled_models__"),
None,
@@ -558,12 +558,12 @@ async def test_delete_non_provider_credential_does_not_cleanup_models(client: As
"default_fields": [],
}
create_response = await client.post("api/v1/variables/", json=generic_variable, headers=logged_in_headers)
create_response = await client.post("v1/variables/", json=generic_variable, headers=logged_in_headers)
assert create_response.status_code == status.HTTP_201_CREATED
created_var = create_response.json()
# Delete the variable - should not trigger any cleanup
delete_response = await client.delete(f"api/v1/variables/{created_var['id']}", headers=logged_in_headers)
delete_response = await client.delete(f"v1/variables/{created_var['id']}", headers=logged_in_headers)
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
@@ -596,7 +596,7 @@ async def test_detect_env_vars_endpoint__returns_detected_names(client: AsyncCli
mock.patch("flow.api.v1.variable.get_variable_service", return_value=variable_service),
):
response = await client.post(
"api/v1/variables/detections",
"v1/variables/detections",
json={"flow_version_ids": [str(flow_version_id)]},
headers=logged_in_headers,
)
@@ -620,7 +620,7 @@ async def test_detect_env_vars_endpoint__rejects_missing_nodes(client: AsyncClie
mock.patch("flow.api.v1.variable.get_variable_service", return_value=variable_service),
):
response = await client.post(
"api/v1/variables/detections",
"v1/variables/detections",
json={"flow_version_ids": [str(flow_version_id)]},
headers=logged_in_headers,
)
+66 -66
View File
@@ -139,7 +139,7 @@ async def test_upload_file(files_client, files_created_api_key):
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
@@ -160,7 +160,7 @@ async def test_should_not_persist_in_my_files_when_upload_is_ephemeral(files_cli
# Upload with ephemeral=true
response = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("playground_image.png", b"fake image content")},
params={"ephemeral": "true"},
headers=headers,
@@ -171,7 +171,7 @@ async def test_should_not_persist_in_my_files_when_upload_is_ephemeral(files_cli
assert "path" in upload_response
# The file must NOT appear in the user's file list
list_response = await files_client.get("api/v2/files", headers=headers)
list_response = await files_client.get("v1/files", headers=headers)
assert list_response.status_code == 200
file_names = [f["name"] for f in list_response.json()]
assert "playground_image" not in file_names, (
@@ -197,7 +197,7 @@ async def test_should_return_path_with_forward_slashes_when_uploading_file(files
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("test_path.txt", b"path test content")},
headers=headers,
)
@@ -210,7 +210,7 @@ async def test_should_return_path_with_forward_slashes_when_uploading_file(files
)
# Verify the upload path matches what GET /files returns
list_response = await files_client.get("api/v2/files", headers=headers)
list_response = await files_client.get("v1/files", headers=headers)
assert list_response.status_code == 200
listed_paths = [f["path"] for f in list_response.json()]
@@ -225,7 +225,7 @@ async def test_download_file(files_client, files_created_api_key):
# First upload a file
response = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
@@ -233,7 +233,7 @@ async def test_download_file(files_client, files_created_api_key):
upload_response = response.json()
# Then try to download it
response = await files_client.get(f"api/v2/files/{upload_response['id']}", headers=headers)
response = await files_client.get(f"v1/files/{upload_response['id']}", headers=headers)
assert response.status_code == 200
assert response.content == b"test content"
@@ -245,7 +245,7 @@ async def test_download_file_not_found(files_client, files_created_api_key):
# Try to download a file that doesn't exist
fake_file_id = "00000000-0000-0000-0000-000000000000"
response = await files_client.get(f"api/v2/files/{fake_file_id}", headers=headers)
response = await files_client.get(f"v1/files/{fake_file_id}", headers=headers)
assert response.status_code == 404
error_response = response.json()
@@ -257,14 +257,14 @@ async def test_list_files(files_client, files_created_api_key):
# First upload a file
response = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
assert response.status_code == 201
# Then list the files
response = await files_client.get("api/v2/files", headers=headers)
response = await files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
assert len(files) == 1
@@ -274,14 +274,14 @@ async def test_delete_file(files_client, files_created_api_key):
headers = {"x-api-key": files_created_api_key.api_key}
response = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
assert response.status_code == 201
upload_response = response.json()
response = await files_client.delete(f"api/v2/files/{upload_response['id']}", headers=headers)
response = await files_client.delete(f"v1/files/{upload_response['id']}", headers=headers)
assert response.status_code == 200
assert response.json() == {"detail": "File test deleted successfully"}
@@ -291,7 +291,7 @@ async def test_edit_file(files_client, files_created_api_key):
# First upload a file
response = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("test.txt", b"test content")},
headers=headers,
)
@@ -299,7 +299,7 @@ async def test_edit_file(files_client, files_created_api_key):
upload_response = response.json()
# Then list the files
response = await files_client.put(f"api/v2/files/{upload_response['id']}?name=potato.txt", headers=headers)
response = await files_client.put(f"v1/files/{upload_response['id']}?name=potato.txt", headers=headers)
assert response.status_code == 200
file = response.json()
assert file["name"] == "potato.txt"
@@ -310,7 +310,7 @@ async def test_upload_list_delete_and_validate_files(files_client, files_created
# Upload two files
response1 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("file1.txt", b"content1")},
headers=headers,
)
@@ -318,7 +318,7 @@ async def test_upload_list_delete_and_validate_files(files_client, files_created
file1 = response1.json()
response2 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("file2.txt", b"content2")},
headers=headers,
)
@@ -326,7 +326,7 @@ async def test_upload_list_delete_and_validate_files(files_client, files_created
file2 = response2.json()
# List files and validate both are present
response = await files_client.get("api/v2/files", headers=headers)
response = await files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
file_names = [f["name"] for f in files]
@@ -338,11 +338,11 @@ async def test_upload_list_delete_and_validate_files(files_client, files_created
assert len(files) == 2
# Delete one file
response = await files_client.delete(f"api/v2/files/{file1['id']}", headers=headers)
response = await files_client.delete(f"v1/files/{file1['id']}", headers=headers)
assert response.status_code == 200
# List files again and validate only the other remains
response = await files_client.get("api/v2/files", headers=headers)
response = await files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
file_names = [f["name"] for f in files]
@@ -360,7 +360,7 @@ async def test_upload_files_with_same_name_creates_unique_names(files_client, fi
# Upload first file
response1 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("duplicate.txt", b"content1")},
headers=headers,
)
@@ -370,7 +370,7 @@ async def test_upload_files_with_same_name_creates_unique_names(files_client, fi
# Upload second file with same name
response2 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("duplicate.txt", b"content2")},
headers=headers,
)
@@ -380,7 +380,7 @@ async def test_upload_files_with_same_name_creates_unique_names(files_client, fi
# Upload third file with same name
response3 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("duplicate.txt", b"content3")},
headers=headers,
)
@@ -389,20 +389,20 @@ async def test_upload_files_with_same_name_creates_unique_names(files_client, fi
assert file3["name"] == "duplicate (2)"
# Verify all files can be downloaded with their unique content
download1 = await files_client.get(f"api/v2/files/{file1['id']}", headers=headers)
download1 = await files_client.get(f"v1/files/{file1['id']}", headers=headers)
assert download1.status_code == 200
assert download1.content == b"content1"
download2 = await files_client.get(f"api/v2/files/{file2['id']}", headers=headers)
download2 = await files_client.get(f"v1/files/{file2['id']}", headers=headers)
assert download2.status_code == 200
assert download2.content == b"content2"
download3 = await files_client.get(f"api/v2/files/{file3['id']}", headers=headers)
download3 = await files_client.get(f"v1/files/{file3['id']}", headers=headers)
assert download3.status_code == 200
assert download3.content == b"content3"
# List files and verify all three are present with unique names
response = await files_client.get("api/v2/files", headers=headers)
response = await files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
file_names = [f["name"] for f in files]
@@ -418,7 +418,7 @@ async def test_upload_files_without_extension_creates_unique_names(files_client,
# Upload first file without extension
response1 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("noextension", b"content1")},
headers=headers,
)
@@ -428,7 +428,7 @@ async def test_upload_files_without_extension_creates_unique_names(files_client,
# Upload second file with same name
response2 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("noextension", b"content2")},
headers=headers,
)
@@ -437,11 +437,11 @@ async def test_upload_files_without_extension_creates_unique_names(files_client,
assert file2["name"] == "noextension (1)"
# Verify both files can be downloaded
download1 = await files_client.get(f"api/v2/files/{file1['id']}", headers=headers)
download1 = await files_client.get(f"v1/files/{file1['id']}", headers=headers)
assert download1.status_code == 200
assert download1.content == b"content1"
download2 = await files_client.get(f"api/v2/files/{file2['id']}", headers=headers)
download2 = await files_client.get(f"v1/files/{file2['id']}", headers=headers)
assert download2.status_code == 200
assert download2.content == b"content2"
@@ -452,7 +452,7 @@ async def test_upload_files_with_different_extensions_same_name(files_client, fi
# Upload file with .txt extension
response1 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("document.txt", b"text content")},
headers=headers,
)
@@ -462,7 +462,7 @@ async def test_upload_files_with_different_extensions_same_name(files_client, fi
# Upload file with .md extension and same root name
response2 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("document.md", b"markdown content")},
headers=headers,
)
@@ -472,7 +472,7 @@ async def test_upload_files_with_different_extensions_same_name(files_client, fi
# Upload another .txt file with same root name
response3 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("document.txt", b"more text content")},
headers=headers,
)
@@ -490,7 +490,7 @@ async def test_mcp_servers_file_replacement(files_client, files_created_api_key,
# Upload first _mcp_servers file
response1 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": (mcp_file_ext, b'{"servers": ["server1"]}')},
headers=headers,
)
@@ -500,7 +500,7 @@ async def test_mcp_servers_file_replacement(files_client, files_created_api_key,
# Upload second _mcp_servers file - should replace the first one
response2 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": (mcp_file_ext, b'{"servers": ["server2"]}')},
headers=headers,
)
@@ -510,19 +510,19 @@ async def test_mcp_servers_file_replacement(files_client, files_created_api_key,
# Note: _mcp_servers files are filtered out from the regular file list
# This is expected behavior since they're managed separately
response = await files_client.get("api/v2/files", headers=headers)
response = await files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
mcp_files = [f for f in files if f["name"] == mcp_file]
assert len(mcp_files) == 0 # MCP servers files are filtered out from regular list
# Verify the second file can be downloaded with the updated content
download2 = await files_client.get(f"api/v2/files/{file2['id']}", headers=headers)
download2 = await files_client.get(f"v1/files/{file2['id']}", headers=headers)
assert download2.status_code == 200
assert download2.content == b'{"servers": ["server2"]}'
# Verify the first file no longer exists (should return 404)
download1 = await files_client.get(f"api/v2/files/{file1['id']}", headers=headers)
download1 = await files_client.get(f"v1/files/{file1['id']}", headers=headers)
assert download1.status_code == 404
# Verify the file IDs are different (new file replaced old one)
@@ -535,7 +535,7 @@ async def test_unique_filename_counter_handles_gaps(files_client, files_created_
# Upload original file
response1 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("gaptest.txt", b"content1")},
headers=headers,
)
@@ -545,7 +545,7 @@ async def test_unique_filename_counter_handles_gaps(files_client, files_created_
# Upload second file (should be gaptest (1))
response2 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("gaptest.txt", b"content2")},
headers=headers,
)
@@ -555,7 +555,7 @@ async def test_unique_filename_counter_handles_gaps(files_client, files_created_
# Upload third file (should be gaptest (2))
response3 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("gaptest.txt", b"content3")},
headers=headers,
)
@@ -564,12 +564,12 @@ async def test_unique_filename_counter_handles_gaps(files_client, files_created_
assert file3["name"] == "gaptest (2)"
# Delete the middle file (gaptest (1))
delete_response = await files_client.delete(f"api/v2/files/{file2['id']}", headers=headers)
delete_response = await files_client.delete(f"v1/files/{file2['id']}", headers=headers)
assert delete_response.status_code == 200
# Upload another file - should be gaptest (3), not filling the gap
response4 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("gaptest.txt", b"content4")},
headers=headers,
)
@@ -578,7 +578,7 @@ async def test_unique_filename_counter_handles_gaps(files_client, files_created_
assert file4["name"] == "gaptest (3)"
# Verify final state
response = await files_client.get("api/v2/files", headers=headers)
response = await files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
file_names = [f["name"] for f in files]
@@ -595,7 +595,7 @@ async def test_unique_filename_path_storage(files_client, files_created_api_key)
# Upload two files with same name
response1 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("pathtest.txt", b"path content 1")},
headers=headers,
)
@@ -603,7 +603,7 @@ async def test_unique_filename_path_storage(files_client, files_created_api_key)
file1 = response1.json()
response2 = await files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("pathtest.txt", b"path content 2")},
headers=headers,
)
@@ -613,11 +613,11 @@ async def test_unique_filename_path_storage(files_client, files_created_api_key)
# Verify both files have different paths and can be downloaded independently
assert file1["path"] != file2["path"]
download1 = await files_client.get(f"api/v2/files/{file1['id']}", headers=headers)
download1 = await files_client.get(f"v1/files/{file1['id']}", headers=headers)
assert download1.status_code == 200
assert download1.content == b"path content 1"
download2 = await files_client.get(f"api/v2/files/{file2['id']}", headers=headers)
download2 = await files_client.get(f"v1/files/{file2['id']}", headers=headers)
assert download2.status_code == 200
assert download2.content == b"path content 2"
@@ -778,7 +778,7 @@ class TestS3FileOperations:
headers = {"x-api-key": s3_files_created_api_key.api_key}
response = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_test.txt", b"S3 test content")},
headers=headers,
)
@@ -794,7 +794,7 @@ class TestS3FileOperations:
# Upload file
response = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_download_test.txt", b"S3 download content")},
headers=headers,
)
@@ -802,7 +802,7 @@ class TestS3FileOperations:
upload_response = response.json()
# Download file
response = await s3_files_client.get(f"api/v2/files/{upload_response['id']}", headers=headers)
response = await s3_files_client.get(f"v1/files/{upload_response['id']}", headers=headers)
assert response.status_code == 200
assert response.content == b"S3 download content"
@@ -813,14 +813,14 @@ class TestS3FileOperations:
# Upload a file
response = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_list_test.txt", b"S3 list content")},
headers=headers,
)
assert response.status_code == 201
# List files
response = await s3_files_client.get("api/v2/files", headers=headers)
response = await s3_files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
assert len(files) >= 1
@@ -833,7 +833,7 @@ class TestS3FileOperations:
# Upload a file
response = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_delete_test.txt", b"S3 delete content")},
headers=headers,
)
@@ -841,19 +841,19 @@ class TestS3FileOperations:
upload_response = response.json()
# Delete the file
response = await s3_files_client.delete(f"api/v2/files/{upload_response['id']}", headers=headers)
response = await s3_files_client.delete(f"v1/files/{upload_response['id']}", headers=headers)
assert response.status_code == 200
assert response.json() == {"detail": "File s3_delete_test deleted successfully"}
# Verify file is deleted from database
response = await s3_files_client.get("api/v2/files", headers=headers)
response = await s3_files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
file_names = [f["name"] for f in files]
assert "s3_delete_test" not in file_names
# Verify file is deleted from S3 (should return 404)
response = await s3_files_client.get(f"api/v2/files/{upload_response['id']}", headers=headers)
response = await s3_files_client.get(f"v1/files/{upload_response['id']}", headers=headers)
assert response.status_code == 404
async def test_s3_upload_list_delete_multiple_files(self, s3_files_client, s3_files_created_api_key):
@@ -862,7 +862,7 @@ class TestS3FileOperations:
# Upload two files
response1 = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_file1.txt", b"S3 content1")},
headers=headers,
)
@@ -870,7 +870,7 @@ class TestS3FileOperations:
file1 = response1.json()
response2 = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_file2.txt", b"S3 content2")},
headers=headers,
)
@@ -878,7 +878,7 @@ class TestS3FileOperations:
file2 = response2.json()
# List files and validate both are present
response = await s3_files_client.get("api/v2/files", headers=headers)
response = await s3_files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
file_names = [f["name"] for f in files]
@@ -889,11 +889,11 @@ class TestS3FileOperations:
assert file2["id"] in file_ids
# Delete one file
response = await s3_files_client.delete(f"api/v2/files/{file1['id']}", headers=headers)
response = await s3_files_client.delete(f"v1/files/{file1['id']}", headers=headers)
assert response.status_code == 200
# List files again and validate only the other remains
response = await s3_files_client.get("api/v2/files", headers=headers)
response = await s3_files_client.get("v1/files", headers=headers)
assert response.status_code == 200
files = response.json()
file_names = [f["name"] for f in files]
@@ -912,7 +912,7 @@ class TestS3FileOperations:
# Upload binary file
response = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_binary.bin", binary_data)},
headers=headers,
)
@@ -920,7 +920,7 @@ class TestS3FileOperations:
upload_response = response.json()
# Download and verify binary data
response = await s3_files_client.get(f"api/v2/files/{upload_response['id']}", headers=headers)
response = await s3_files_client.get(f"v1/files/{upload_response['id']}", headers=headers)
assert response.status_code == 200
assert response.content == binary_data
@@ -930,7 +930,7 @@ class TestS3FileOperations:
# Upload a file
response = await s3_files_client.post(
"api/v2/files",
"v1/files",
files={"file": ("s3_cleanup_test.txt", b"S3 cleanup content")},
headers=headers,
)
@@ -942,7 +942,7 @@ class TestS3FileOperations:
user_id = file_path.split("/")[0]
# Delete the file
response = await s3_files_client.delete(f"api/v2/files/{upload_response['id']}", headers=headers)
response = await s3_files_client.delete(f"v1/files/{upload_response['id']}", headers=headers)
assert response.status_code == 200
# Verify file is actually deleted from S3 by checking directly
+58 -58
View File
@@ -70,7 +70,7 @@ class TestWorkflowDeveloperAPIProtection:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows",
"v1/workflows",
json=request_data,
headers=headers,
)
@@ -91,7 +91,7 @@ class TestWorkflowDeveloperAPIProtection:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json=request_data,
headers=headers,
)
@@ -127,7 +127,7 @@ class TestWorkflowDeveloperAPIProtection:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows",
"v1/workflows",
json=request_data,
headers=headers,
)
@@ -147,7 +147,7 @@ class TestWorkflowDeveloperAPIProtection:
"""Test GET workflow endpoint is allowed when developer API is enabled - job not found."""
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(
"api/v2/workflows?job_id=550e8400-e29b-41d4-a716-446655440001", # Non-existent job ID
"v1/workflows?job_id=550e8400-e29b-41d4-a716-446655440001", # Non-existent job ID
headers=headers,
)
@@ -170,7 +170,7 @@ class TestWorkflowDeveloperAPIProtection:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json=request_data,
headers=headers,
)
@@ -191,7 +191,7 @@ class TestWorkflowDeveloperAPIProtection:
"""Test GET workflow endpoint is blocked when developer API is disabled."""
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(
"api/v2/workflows?job_id=550e8400-e29b-41d4-a716-446655440001",
"v1/workflows?job_id=550e8400-e29b-41d4-a716-446655440001",
headers=headers,
)
@@ -226,7 +226,7 @@ class TestWorkflowDeveloperAPIProtection:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows",
"v1/workflows",
json=request_data,
headers=headers,
)
@@ -261,7 +261,7 @@ class TestWorkflowDeveloperAPIProtection:
# The endpoint should return 501 regardless of whether the job exists
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(
"api/v2/workflows?job_id=550e8400-e29b-41d4-a716-446655440002",
"v1/workflows?job_id=550e8400-e29b-41d4-a716-446655440002",
headers=headers,
)
@@ -283,7 +283,7 @@ class TestWorkflowDeveloperAPIProtection:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json=request_data,
headers=headers,
)
@@ -308,7 +308,7 @@ class TestWorkflowDeveloperAPIProtection:
}
response = await client.post(
"api/v2/workflows",
"v1/workflows",
json=request_data,
)
# The API returns 403 Forbidden for missing API keys (not 401 Unauthorized)
@@ -342,7 +342,7 @@ class TestWorkflowErrorHandling:
request_data = {"flow_id": flow_id, "background": False, "stream": False, "inputs": None}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 404
result = response.json()
@@ -365,7 +365,7 @@ class TestWorkflowErrorHandling:
mock_get_flow.side_effect = OperationalError("statement", "params", "orig")
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 503
result = response.json()
@@ -399,7 +399,7 @@ class TestWorkflowErrorHandling:
request_data = {"flow_id": str(flow_id), "background": False, "stream": False, "inputs": None}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 400
result = response.json()
@@ -440,7 +440,7 @@ class TestWorkflowErrorHandling:
request_data = {"flow_id": str(flow_id), "background": False, "stream": False, "inputs": None}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 400
result = response.json()
@@ -493,7 +493,7 @@ class TestWorkflowErrorHandling:
patch("flow.api.v2.workflow.EXECUTION_TIMEOUT", 0.5), # 0.5 second timeout
):
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 408
result = response.json()
@@ -540,7 +540,7 @@ class TestWorkflowErrorHandling:
}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
# Now background mode is partially implemented and should NOT return 501
# It should return a WorkflowJobResponse (wrapped in WorkflowExecutionResponse or similar)
@@ -590,7 +590,7 @@ class TestWorkflowErrorHandling:
}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 501
result = response.json()
@@ -615,7 +615,7 @@ class TestWorkflowErrorHandling:
request_data = {"flow_id": flow_id, "background": False, "stream": False, "inputs": None}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 404
result = response.json()
@@ -663,7 +663,7 @@ class TestWorkflowErrorHandling:
mock_execute.side_effect = WorkflowValidationError("Test validation error")
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 400
result = response.json()
@@ -678,7 +678,7 @@ class TestWorkflowErrorHandling:
await session.delete(flow)
# Test GET /workflow without API key
response = await client.get("api/v2/workflows?job_id=550e8400-e29b-41d4-a716-446655440001")
response = await client.get("v1/workflows?job_id=550e8400-e29b-41d4-a716-446655440001")
assert response.status_code == 403
assert "API key must be passed" in response.json()["detail"]
@@ -722,7 +722,7 @@ class TestWorkflowSyncExecution:
request_data = {"flow_id": str(flow_id), "background": False, "stream": False, "inputs": None}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -775,7 +775,7 @@ class TestWorkflowSyncExecution:
mock_run.side_effect = Exception("Component execution failed: LLM API key not configured")
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
# Component errors should return 200 with error in body
assert response.status_code == 200
@@ -843,7 +843,7 @@ class TestWorkflowSyncExecution:
mock_run.return_value = ([mock_run_output], "session-456")
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -924,7 +924,7 @@ class TestWorkflowSyncExecution:
mock_run.return_value = ([mock_run_output], "session-789")
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -989,7 +989,7 @@ class TestWorkflowSyncExecution:
mock_run.return_value = ([mock_run_output], "session-101")
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1048,7 +1048,7 @@ class TestWorkflowSyncExecution:
mock_run.return_value = ([mock_chat_output, mock_file_output], "session-202")
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1089,7 +1089,7 @@ class TestWorkflowSyncExecution:
request_data = {"flow_id": str(flow_id), "background": False, "stream": False, "inputs": None}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1132,7 +1132,7 @@ class TestWorkflowSyncExecution:
# Test POST /workflow/stop without API key
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json={"job_id": "550e8400-e29b-41d4-a716-446655440001"},
)
assert response.status_code == 403
@@ -1196,7 +1196,7 @@ class TestWorkflowBackgroundQueueing:
mock_task_service.fire_and_forget_task.return_value.set_result(mock_job_id)
mock_get_task_service.return_value = mock_task_service
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1227,7 +1227,7 @@ class TestWorkflowBackgroundQueueing:
"background": True,
}
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 404
detail = response.json()["detail"]
message = detail["message"] if isinstance(detail, dict) else detail
@@ -1260,7 +1260,7 @@ class TestWorkflowBackgroundQueueing:
mock_task_service.fire_and_forget_task.side_effect = Exception("Queueing failed")
mock_get_task_service.return_value = mock_task_service
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 500
detail = response.json()["detail"]
message = detail["message"] if isinstance(detail, dict) else detail
@@ -1296,7 +1296,7 @@ class TestWorkflowBackgroundQueueing:
with patch("flow.api.v2.workflow.run_graph_internal") as mock_run:
mock_run.side_effect = Exception("Internal execution engine failure")
response = await client.post("api/v2/workflows", json=request_data, headers=headers)
response = await client.post("v1/workflows", json=request_data, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1348,7 +1348,7 @@ class TestWorkflowStatus:
mock_get_job_service.return_value = mock_service
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1371,7 +1371,7 @@ class TestWorkflowStatus:
mock_get_job_service.return_value = mock_service
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 404
result = response.json()
@@ -1398,7 +1398,7 @@ class TestWorkflowStatus:
mock_get_job_service.return_value = mock_service
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 500
result = response.json()
@@ -1438,7 +1438,7 @@ class TestWorkflowStatus:
mock_reconstruct.return_value = {"flow_id": str(flow_id), "status": "completed", "outputs": {}}
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1469,7 +1469,7 @@ class TestWorkflowStatus:
headers = {"x-api-key": created_api_key.api_key}
# Add timeout to client.get to avoid hanging if something goes wrong
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 408
result = response.json()
@@ -1521,7 +1521,7 @@ class TestWorkflowStop:
mock_get_task_service.return_value = mock_task_service
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows/stop", json={"job_id": job_id}, headers=headers)
response = await client.post("v1/workflows/stop", json={"job_id": job_id}, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1545,7 +1545,7 @@ class TestWorkflowStop:
mock_get_job_service.return_value = mock_service
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows/stop", json={"job_id": job_id}, headers=headers)
response = await client.post("v1/workflows/stop", json={"job_id": job_id}, headers=headers)
assert response.status_code == 404
result = response.json()
@@ -1572,7 +1572,7 @@ class TestWorkflowStop:
mock_get_job_service.return_value = mock_service
headers = {"x-api-key": created_api_key.api_key}
response = await client.post("api/v2/workflows/stop", json={"job_id": job_id}, headers=headers)
response = await client.post("v1/workflows/stop", json={"job_id": job_id}, headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1606,7 +1606,7 @@ class TestWorkflowIDORProtection:
created_user_two_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""GET /api/v2/workflows returns 404 when the job belongs to a different user.
"""GET /v1/workflows returns 404 when the job belongs to a different user.
GHSA-qfw4-cjhf-3g3q: job status must not be visible cross-user.
Ownership is enforced at the SQL level — unauthorized access returns 404.
@@ -1627,7 +1627,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 404
result = response.json()
@@ -1646,7 +1646,7 @@ class TestWorkflowIDORProtection:
created_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""GET /api/v2/workflows returns 200 when the job belongs to the requesting user."""
"""GET /v1/workflows returns 200 when the job belongs to the requesting user."""
job_id = uuid4()
owner_user_id = created_api_key.user_id
@@ -1663,7 +1663,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1682,7 +1682,7 @@ class TestWorkflowIDORProtection:
created_user_two_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""POST /api/v2/workflows/stop returns 404 when the job belongs to a different user.
"""POST /v1/workflows/stop returns 404 when the job belongs to a different user.
GHSA-qfw4-cjhf-3g3q: job cancellation must not be allowed cross-user.
Ownership is enforced at the SQL level — unauthorized access returns 404.
@@ -1704,7 +1704,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json={"job_id": str(job_id)},
headers=headers,
)
@@ -1726,7 +1726,7 @@ class TestWorkflowIDORProtection:
created_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""POST /api/v2/workflows/stop succeeds when the job belongs to the requesting user."""
"""POST /v1/workflows/stop succeeds when the job belongs to the requesting user."""
job_id = uuid4()
owner_user_id = created_api_key.user_id
@@ -1744,7 +1744,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json={"job_id": str(job_id)},
headers=headers,
)
@@ -1763,7 +1763,7 @@ class TestWorkflowIDORProtection:
created_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""GET /api/v2/workflows does NOT block legacy jobs where user_id is NULL.
"""GET /v1/workflows does NOT block legacy jobs where user_id is NULL.
Jobs created before the fix have user_id=None and must not be broken.
"""
@@ -1782,7 +1782,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 200
result = response.json()
@@ -1800,7 +1800,7 @@ class TestWorkflowIDORProtection:
created_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""POST /api/v2/workflows/stop does NOT block legacy jobs where user_id is NULL.
"""POST /v1/workflows/stop does NOT block legacy jobs where user_id is NULL.
Jobs created before the ownership fix have user_id=None and must not be
broken by the ownership check (parity with the equivalent GET test).
@@ -1821,7 +1821,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json={"job_id": str(job_id)},
headers=headers,
)
@@ -1842,7 +1842,7 @@ class TestWorkflowIDORProtection:
created_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""POST /api/v2/workflows/stop returns 404 for non-WORKFLOW job types.
"""POST /v1/workflows/stop returns 404 for non-WORKFLOW job types.
Prevents stop endpoint from cancelling ingestion or evaluation jobs.
"""
@@ -1862,7 +1862,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows/stop",
"v1/workflows/stop",
json={"job_id": str(job_id)},
headers=headers,
)
@@ -1883,7 +1883,7 @@ class TestWorkflowIDORProtection:
created_api_key,
mock_settings_dev_api_enabled, # noqa: ARG002
):
"""GET /api/v2/workflows returns 404 for non-WORKFLOW job types.
"""GET /v1/workflows returns 404 for non-WORKFLOW job types.
Prevents status endpoint from exposing ingestion or evaluation jobs.
"""
@@ -1902,7 +1902,7 @@ class TestWorkflowIDORProtection:
try:
headers = {"x-api-key": created_api_key.api_key}
response = await client.get(f"api/v2/workflows?job_id={job_id}", headers=headers)
response = await client.get(f"v1/workflows?job_id={job_id}", headers=headers)
assert response.status_code == 404
result = response.json()
@@ -1953,7 +1953,7 @@ class TestWorkflowIDORProtection:
alice_headers = {"x-api-key": created_api_key.api_key}
response = await client.post(
"api/v2/workflows",
"v1/workflows",
json={
"flow_id": str(flow_id),
"background": True,
@@ -1971,7 +1971,7 @@ class TestWorkflowIDORProtection:
# Bob queries Alice's job — must be 404 (ownership enforced at SQL level)
bob_headers = {"x-api-key": created_user_two_api_key.api_key}
response = await client.get(
f"api/v2/workflows?job_id={job_id_str}",
f"v1/workflows?job_id={job_id_str}",
headers=bob_headers,
)
+3 -3
View File
@@ -9,7 +9,7 @@ from lfx.log.logger import logger
async def create_flow(client: AsyncClient, flow_data: str, headers: dict[str, str]) -> UUID:
"""Create a flow and return its ID."""
response = await client.post("api/v1/flows/", json=json.loads(flow_data), headers=headers)
response = await client.post("v1/flows/", json=json.loads(flow_data), headers=headers)
assert response.status_code == codes.CREATED
return UUID(response.json()["id"])
@@ -20,7 +20,7 @@ async def build_flow(
"""Start a flow build and return the job_id."""
if json is None:
json = {}
response = await client.post(f"api/v1/build/{flow_id}/flow", json=json, headers=headers)
response = await client.post(f"v1/build/{flow_id}/flow", json=json, headers=headers)
assert response.status_code == codes.OK
return response.json()
@@ -29,7 +29,7 @@ async def get_build_events(client: AsyncClient, job_id: str, headers: dict[str,
"""Get events for a build job."""
# Add Accept header for NDJSON format
headers_with_accept = {**headers, "Accept": "application/x-ndjson"}
return await client.get(f"api/v1/build/{job_id}/events", headers=headers_with_accept)
return await client.get(f"v1/build/{job_id}/events", headers=headers_with_accept)
async def consume_and_assert_stream(response, job_id, timeout=30.0):
@@ -55,7 +55,7 @@ class TestLoopComponentWithAPI(ComponentTestBaseWithClient):
vector_store = orjson.loads(json_loop_test)
data = vector_store["data"]
vector_store = FlowCreate(name="Flow", description="description", data=data, endpoint_name="f")
response = await client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers)
response.raise_for_status()
return response.json()["id"]
@@ -483,12 +483,12 @@ class TestMigrationWithRealDatabase:
"type": CREDENTIAL_TYPE,
"default_fields": [],
}
response = await client.post("api/v1/variables/", json=credential_variable, headers=logged_in_headers)
response = await client.post("v1/variables/", json=credential_variable, headers=logged_in_headers)
assert response.status_code == 201
created_var = response.json()
# Read the variable back
response = await client.get("api/v1/variables/", headers=logged_in_headers)
response = await client.get("v1/variables/", headers=logged_in_headers)
assert response.status_code == 200
all_vars = response.json()
@@ -501,7 +501,7 @@ class TestMigrationWithRealDatabase:
assert our_var["value"] is None or our_var["value"] != credential_variable["value"]
# Cleanup
await client.delete(f"api/v1/variables/{created_var['id']}", headers=logged_in_headers)
await client.delete(f"v1/variables/{created_var['id']}", headers=logged_in_headers)
async def test_create_folder_via_api(
self,
@@ -517,12 +517,12 @@ class TestMigrationWithRealDatabase:
"name": f"Test Project {uuid4().hex[:8]}",
"description": "Test project for migration",
}
response = await client.post("api/v1/folders/", json=project_data, headers=logged_in_headers)
response = await client.post("v1/folders/", json=project_data, headers=logged_in_headers)
assert response.status_code == 201
created_folder = response.json()
# Cleanup
await client.delete(f"api/v1/folders/{created_folder['id']}", headers=logged_in_headers)
await client.delete(f"v1/folders/{created_folder['id']}", headers=logged_in_headers)
@pytest.mark.usefixtures("client")
+4 -4
View File
@@ -11,14 +11,14 @@ async def api_key(
):
api_key = ApiKeyCreate(name="test-api-key")
response = await client.post("api/v1/api_key/", json=api_key.model_dump(mode="json"), headers=logged_in_headers)
response = await client.post("v1/api_key/", json=api_key.model_dump(mode="json"), headers=logged_in_headers)
assert response.status_code == 200, response.text
return response.json()
@pytest.mark.usefixtures("api_key")
async def test_get_api_keys(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/api_key/", headers=logged_in_headers)
response = await client.get("v1/api_key/", headers=logged_in_headers)
assert response.status_code == 200, response.text
data = response.json()
assert "total_count" in data
@@ -30,7 +30,7 @@ async def test_get_api_keys(client: AsyncClient, logged_in_headers):
async def test_create_api_key(client: AsyncClient, logged_in_headers):
api_key_name = "test-api-key"
response = await client.post("api/v1/api_key/", json={"name": api_key_name}, headers=logged_in_headers)
response = await client.post("v1/api_key/", json={"name": api_key_name}, headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert "name" in data
@@ -42,7 +42,7 @@ async def test_create_api_key(client: AsyncClient, logged_in_headers):
@pytest.mark.usefixtures("active_user")
async def test_delete_api_key(client, logged_in_headers, api_key):
api_key_id = api_key["id"]
response = await client.delete(f"api/v1/api_key/{api_key_id}", headers=logged_in_headers)
response = await client.delete(f"v1/api_key/{api_key_id}", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert data["detail"] == "API Key deleted"
+52 -52
View File
@@ -41,7 +41,7 @@ async def test_build_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
async def test_build_flow_from_request_data(client, json_memory_chatbot_no_llm, logged_in_headers):
"""Test building a flow from request data."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
response = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
flow_data = response.json()
# Start the build and get job_id
@@ -62,7 +62,7 @@ async def test_build_flow_validates_request_data_instead_of_stale_db_flow(
):
"""When request data is provided, preflight validation should use it instead of the saved flow."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
response = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
flow_data = response.json()
request_data = json.loads(json.dumps(flow_data["data"]))
request_data["nodes"][0]["data"]["node"]["display_name"] = "Updated Request Flow"
@@ -78,7 +78,7 @@ async def test_build_flow_validates_request_data_instead_of_stale_db_flow(
)
response = await client.post(
f"api/v1/build/{flow_id}/flow",
f"v1/build/{flow_id}/flow",
json={"data": request_data},
headers=logged_in_headers,
)
@@ -91,13 +91,13 @@ async def test_build_flow_with_frozen_path(client, json_memory_chatbot_no_llm, l
"""Test building a flow with a frozen path."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
response = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
flow_data = response.json()
flow_data["data"]["nodes"][0]["data"]["node"]["frozen"] = True
# Update the flow with frozen path
response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json=FlowUpdate(name="Flow", description="description", data=flow_data["data"]).model_dump(),
headers=logged_in_headers,
)
@@ -144,7 +144,7 @@ async def test_build_flow_invalid_job_id(client, logged_in_headers):
async def test_build_flow_invalid_flow_id(client, logged_in_headers):
"""Test starting a build with an invalid flow ID."""
invalid_flow_id = uuid.uuid4()
response = await client.post(f"api/v1/build/{invalid_flow_id}/flow", json={}, headers=logged_in_headers)
response = await client.post(f"v1/build/{invalid_flow_id}/flow", json={}, headers=logged_in_headers)
assert response.status_code == codes.NOT_FOUND
@@ -225,7 +225,7 @@ async def test_build_flow_polling(client, json_memory_chatbot_no_llm, logged_in_
# Set a timeout for the request
response = await asyncio.wait_for(
self.client.get(
f"api/v1/build/{self.job_id}/events?event_delivery=polling",
f"v1/build/{self.job_id}/events?event_delivery=polling",
headers=headers,
),
timeout=self.poll_timeout,
@@ -330,7 +330,7 @@ async def test_cancel_build_unexpected_error(client, json_memory_chatbot_no_llm,
try:
# Try to cancel the build - should return 500 Internal Server Error
cancel_response = await client.post(f"api/v1/build/{job_id}/cancel", headers=logged_in_headers)
cancel_response = await client.post(f"v1/build/{job_id}/cancel", headers=logged_in_headers)
assert cancel_response.status_code == codes.INTERNAL_SERVER_ERROR
# Verify the error message
@@ -365,7 +365,7 @@ async def test_cancel_build_success(client, json_memory_chatbot_no_llm, logged_i
try:
# Try to cancel the build (should return success)
cancel_response = await client.post(f"api/v1/build/{job_id}/cancel", headers=logged_in_headers)
cancel_response = await client.post(f"v1/build/{job_id}/cancel", headers=logged_in_headers)
assert cancel_response.status_code == codes.OK
# Verify the response structure indicates success
@@ -386,7 +386,7 @@ async def test_cancel_nonexistent_build(client, logged_in_headers):
invalid_job_id = str(uuid.uuid4())
# Try to cancel a non-existent build
response = await client.post(f"api/v1/build/{invalid_job_id}/cancel", headers=logged_in_headers)
response = await client.post(f"v1/build/{invalid_job_id}/cancel", headers=logged_in_headers)
assert response.status_code == codes.NOT_FOUND
assert "Job not found" in response.json()["detail"]
@@ -415,7 +415,7 @@ async def test_cancel_build_failure(client, json_memory_chatbot_no_llm, logged_i
try:
# Try to cancel the build (should return failure but success=False)
cancel_response = await client.post(f"api/v1/build/{job_id}/cancel", headers=logged_in_headers)
cancel_response = await client.post(f"v1/build/{job_id}/cancel", headers=logged_in_headers)
assert cancel_response.status_code == codes.OK
# Verify the response structure indicates failure
@@ -456,7 +456,7 @@ async def test_cancel_build_with_cancelled_error(client, json_memory_chatbot_no_
try:
# Try to cancel the build - should return failure when CancelledError is raised
# since our implementation treats CancelledError as a failed cancellation
cancel_response = await client.post(f"api/v1/build/{job_id}/cancel", headers=logged_in_headers)
cancel_response = await client.post(f"v1/build/{job_id}/cancel", headers=logged_in_headers)
assert cancel_response.status_code == codes.OK
# Verify the response structure indicates failure
@@ -489,7 +489,7 @@ async def test_should_have_public_events_endpoint_accessible_without_auth(client
# Assert 1 — the PUBLIC events endpoint is accessible without auth
# Returns 404 "Job not found" (route exists, but job doesn't) — NOT 401/403
events_response = await client.get(
f"api/v1/build_public_tmp/{fake_job_id}/events?event_delivery=polling",
f"v1/build_public_tmp/{fake_job_id}/events?event_delivery=polling",
headers={"Accept": "application/x-ndjson"},
)
assert events_response.status_code == codes.NOT_FOUND
@@ -513,7 +513,7 @@ async def test_should_have_public_cancel_endpoint_accessible_without_auth(client
# The PUBLIC cancel endpoint is accessible without auth
# Returns 404 "Job not found" (route exists, but job doesn't) — NOT 401/403
cancel_response = await client.post(
f"api/v1/build_public_tmp/{fake_job_id}/cancel",
f"v1/build_public_tmp/{fake_job_id}/cancel",
headers={"Content-Type": "application/json"},
)
assert cancel_response.status_code == codes.NOT_FOUND
@@ -533,7 +533,7 @@ async def test_build_public_tmp_ignores_data_parameter(client, json_memory_chatb
# Make the flow public
response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -547,7 +547,7 @@ async def test_build_public_tmp_ignores_data_parameter(client, json_memory_chatb
# Attempt to build with malicious data - FastAPI will silently ignore it
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={
"inputs": {"session": "test_session"},
"data": malicious_data, # This will be silently ignored by FastAPI
@@ -579,7 +579,7 @@ async def test_build_public_tmp_checks_public_access_before_validation(
)
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": "test_session"}},
headers={"Content-Type": "application/json"},
)
@@ -599,11 +599,11 @@ async def test_build_flow_cross_user_blocked(client, json_memory_chatbot_no_llm,
victim_flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
login_data = {"username": user_two.username, "password": "hashed_password"} # pragma: allowlist secret
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
attacker_headers = {"Authorization": f"Bearer {response.json()['access_token']}"}
response = await client.post(f"api/v1/build/{victim_flow_id}/flow", json={}, headers=attacker_headers)
response = await client.post(f"v1/build/{victim_flow_id}/flow", json={}, headers=attacker_headers)
assert response.status_code == 404
@@ -614,7 +614,7 @@ async def test_build_flow_unauthenticated_blocked(client, json_memory_chatbot_no
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
# Clear any cookies retained from previous tests to ensure a truly unauthenticated request.
client.cookies.clear()
response = await client.post(f"api/v1/build/{flow_id}/flow", json={})
response = await client.post(f"v1/build/{flow_id}/flow", json={})
assert response.status_code == 403
@@ -623,7 +623,7 @@ async def test_build_flow_unauthenticated_blocked(client, json_memory_chatbot_no
async def test_build_flow_nonexistent_flow_returns_404(client, logged_in_headers):
"""Non-existent flow UUID must return 404."""
nonexistent_id = uuid.uuid4()
response = await client.post(f"api/v1/build/{nonexistent_id}/flow", json={}, headers=logged_in_headers)
response = await client.post(f"v1/build/{nonexistent_id}/flow", json={}, headers=logged_in_headers)
assert response.status_code == 404
@@ -640,7 +640,7 @@ async def test_build_events_cross_user_blocked(client, json_memory_chatbot_no_ll
job_id = build_response["job_id"]
login_data = {"username": user_two.username, "password": "hashed_password"} # pragma: allowlist secret
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
attacker_headers = {"Authorization": f"Bearer {response.json()['access_token']}"}
@@ -660,18 +660,18 @@ async def test_build_flow_public_flow_accessible_by_other_user(
"""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
assert patch_response.status_code == 200
login_data = {"username": user_two.username, "password": "hashed_password"} # pragma: allowlist secret
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
other_headers = {"Authorization": f"Bearer {response.json()['access_token']}"}
response = await client.post(f"api/v1/build/{flow_id}/flow", json={}, headers=other_headers)
response = await client.post(f"v1/build/{flow_id}/flow", json={}, headers=other_headers)
assert response.status_code == 200
@@ -688,11 +688,11 @@ async def test_cancel_build_cross_user_blocked(client, json_memory_chatbot_no_ll
job_id = build_response["job_id"]
login_data = {"username": user_two.username, "password": "hashed_password"} # pragma: allowlist secret
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
attacker_headers = {"Authorization": f"Bearer {response.json()['access_token']}"}
response = await client.post(f"api/v1/build/{job_id}/cancel", headers=attacker_headers)
response = await client.post(f"v1/build/{job_id}/cancel", headers=attacker_headers)
assert response.status_code == 404
@@ -709,7 +709,7 @@ async def test_build_public_tmp_without_data_parameter(client, json_memory_chatb
# Make the flow public
response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -720,7 +720,7 @@ async def test_build_public_tmp_without_data_parameter(client, json_memory_chatb
# Build without providing data parameter
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": "test_session"}},
headers={"Content-Type": "application/json"},
)
@@ -742,7 +742,7 @@ async def test_get_build_events_public_tmp_job_accessible_by_any_auth_user(
"""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -750,7 +750,7 @@ async def test_get_build_events_public_tmp_job_accessible_by_any_auth_user(
client.cookies.set("client_id", "test-public-tmp-events-client")
start_response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={},
headers={"Content-Type": "application/json"},
)
@@ -758,7 +758,7 @@ async def test_get_build_events_public_tmp_job_accessible_by_any_auth_user(
job_id = start_response.json()["job_id"]
login_data = {"username": user_two.username, "password": "hashed_password"} # pragma: allowlist secret
login_response = await client.post("api/v1/login", data=login_data)
login_response = await client.post("v1/login", data=login_data)
assert login_response.status_code == codes.OK
other_headers = {"Authorization": f"Bearer {login_response.json()['access_token']}"}
@@ -785,7 +785,7 @@ async def test_cancel_build_public_tmp_job_accessible_by_any_auth_user(
"""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -793,7 +793,7 @@ async def test_cancel_build_public_tmp_job_accessible_by_any_auth_user(
client.cookies.set("client_id", "test-public-tmp-cancel-client")
start_response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={},
headers={"Content-Type": "application/json"},
)
@@ -801,7 +801,7 @@ async def test_cancel_build_public_tmp_job_accessible_by_any_auth_user(
job_id = start_response.json()["job_id"]
login_data = {"username": user_two.username, "password": "hashed_password"} # pragma: allowlist secret
login_response = await client.post("api/v1/login", data=login_data)
login_response = await client.post("v1/login", data=login_data)
assert login_response.status_code == codes.OK
other_headers = {"Authorization": f"Bearer {login_response.json()['access_token']}"}
@@ -812,7 +812,7 @@ async def test_cancel_build_public_tmp_job_accessible_by_any_auth_user(
monkeypatch.setattr(flow.api.v1.chat, "cancel_flow_build", mock_cancel_flow_build)
cancel_response = await client.post(f"api/v1/build/{job_id}/cancel", headers=other_headers)
cancel_response = await client.post(f"v1/build/{job_id}/cancel", headers=other_headers)
assert cancel_response.status_code == codes.OK
assert cancel_response.json()["success"] is True
@@ -884,7 +884,7 @@ async def test_build_public_tmp_namespaces_caller_session(
):
"""Caller-supplied session equal to the real flow UUID is wrapped under the namespace.
The threat: /api/v1/run hands out session_id == flow_id by default, and the
The threat: /v1/run hands out session_id == flow_id by default, and the
flow UUID is visible in URLs. Without namespacing, an unauthenticated caller
can pass that UUID as inputs.session and a Memory component reads its history.
"""
@@ -892,7 +892,7 @@ async def test_build_public_tmp_namespaces_caller_session(
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -906,7 +906,7 @@ async def test_build_public_tmp_namespaces_caller_session(
victim_session = str(flow_id)
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": victim_session}},
headers={"Content-Type": "application/json"},
)
@@ -929,7 +929,7 @@ async def test_build_public_tmp_session_already_namespaced_unchanged(
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -944,7 +944,7 @@ async def test_build_public_tmp_session_already_namespaced_unchanged(
already_scoped = f"{namespace}:thread-1"
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": already_scoped}},
headers={"Content-Type": "application/json"},
)
@@ -960,7 +960,7 @@ async def test_build_public_tmp_isolates_disjoint_clients(
"""Different client_ids submitting the same session string land in disjoint namespaces."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -973,7 +973,7 @@ async def test_build_public_tmp_isolates_disjoint_clients(
_send_unauthenticated(client, "client-A")
response_a = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": shared_session}},
headers={"Content-Type": "application/json"},
)
@@ -983,7 +983,7 @@ async def test_build_public_tmp_isolates_disjoint_clients(
captured.clear()
_send_unauthenticated(client, "client-B")
response_b = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": shared_session}},
headers={"Content-Type": "application/json"},
)
@@ -1003,7 +1003,7 @@ async def test_build_public_tmp_no_session_passthrough(
"""No inputs supplied: namespacing is skipped; downstream falls back to the virtual flow ID."""
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -1014,7 +1014,7 @@ async def test_build_public_tmp_no_session_passthrough(
_send_unauthenticated(client, "ns-default-client")
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": None},
headers={"Content-Type": "application/json"},
)
@@ -1037,7 +1037,7 @@ async def test_build_public_tmp_empty_session_is_namespaced(
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -1050,7 +1050,7 @@ async def test_build_public_tmp_empty_session_is_namespaced(
_send_unauthenticated(client, client_id)
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": ""}},
headers={"Content-Type": "application/json"},
)
@@ -1072,7 +1072,7 @@ async def test_build_public_tmp_authenticated_namespace_uses_user_id(
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -1083,7 +1083,7 @@ async def test_build_public_tmp_authenticated_namespace_uses_user_id(
client.cookies.set("client_id", "should-be-ignored")
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": "thread-A"}},
headers={**logged_in_headers, "Content-Type": "application/json"},
)
@@ -1111,7 +1111,7 @@ async def test_build_public_tmp_namespacing_blocks_memory_query_collision(
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
patch_response = await client.patch(
f"api/v1/flows/{flow_id}",
f"v1/flows/{flow_id}",
json={"access_type": "PUBLIC"},
headers=logged_in_headers,
)
@@ -1130,7 +1130,7 @@ async def test_build_public_tmp_namespacing_blocks_memory_query_collision(
_send_unauthenticated(client, "leak-test-client")
response = await client.post(
f"api/v1/build_public_tmp/{flow_id}/flow",
f"v1/build_public_tmp/{flow_id}/flow",
json={"inputs": {"session": victim_session}},
headers={"Content-Type": "application/json"},
)
+95 -95
View File
@@ -37,13 +37,13 @@ async def test_create_flow(client: AsyncClient, json_flow: str, logged_in_header
flow = orjson.loads(json_flow)
data = flow["data"]
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
# flow is optional so we can create a flow without a flow
flow = FlowCreate(name=str(uuid4()))
response = await client.post("api/v1/flows/", json=flow.model_dump(exclude_unset=True), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(exclude_unset=True), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
@@ -54,25 +54,25 @@ async def test_read_flows(client: AsyncClient, json_flow: str, logged_in_headers
flow_data = orjson.loads(json_flow)
data = flow_data["data"]
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
flow = FlowCreate(name=str(uuid4()), description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
response = await client.get("api/v1/flows/", headers=logged_in_headers)
response = await client.get("v1/flows/", headers=logged_in_headers)
assert response.status_code == 200
assert len(response.json()) > 0
async def test_read_flows_pagination_with_params(client: AsyncClient, logged_in_headers):
response = await client.get(
"api/v1/flows/", headers=logged_in_headers, params={"page": 3, "size": 10, "get_all": False}
"v1/flows/", headers=logged_in_headers, params={"page": 3, "size": 10, "get_all": False}
)
assert response.status_code == 200
assert response.json()["page"] == 3
@@ -87,12 +87,12 @@ async def test_read_flows_pagination_with_flows(client: AsyncClient, logged_in_h
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
flow_ids = []
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_ids.append(response.json()["id"])
response = await client.get(
"api/v1/flows/", headers=logged_in_headers, params={"page": 3, "size": 10, "get_all": False}
"v1/flows/", headers=logged_in_headers, params={"page": 3, "size": 10, "get_all": False}
)
assert response.status_code == 200
assert response.json()["page"] == 3
@@ -102,7 +102,7 @@ async def test_read_flows_pagination_with_flows(client: AsyncClient, logged_in_h
assert len(response.json()["items"]) == 10
response = await client.get(
"api/v1/flows/", headers=logged_in_headers, params={"page": 4, "size": 10, "get_all": False}
"v1/flows/", headers=logged_in_headers, params={"page": 4, "size": 10, "get_all": False}
)
assert response.status_code == 200
assert response.json()["page"] == 4
@@ -116,11 +116,11 @@ async def test_read_flows_custom_page_size(client: AsyncClient, logged_in_header
number_of_flows = 30
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
response = await client.get(
"api/v1/flows/", headers=logged_in_headers, params={"page": 1, "size": 15, "get_all": False}
"v1/flows/", headers=logged_in_headers, params={"page": 1, "size": 15, "get_all": False}
)
assert response.status_code == 200
assert response.json()["page"] == 1
@@ -135,12 +135,12 @@ async def test_read_flows_invalid_page(client: AsyncClient, logged_in_headers):
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
flow_ids = []
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_ids.append(response.json()["id"])
response = await client.get(
"api/v1/flows/", headers=logged_in_headers, params={"page": 0, "size": 10, "get_all": False}
"v1/flows/", headers=logged_in_headers, params={"page": 0, "size": 10, "get_all": False}
)
assert response.status_code == 422 # Assuming 422 is the status code for invalid input
@@ -150,12 +150,12 @@ async def test_read_flows_invalid_size(client: AsyncClient, logged_in_headers):
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
flow_ids = []
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_ids.append(response.json()["id"])
response = await client.get(
"api/v1/flows/", headers=logged_in_headers, params={"page": 1, "size": 0, "get_all": False}
"v1/flows/", headers=logged_in_headers, params={"page": 1, "size": 0, "get_all": False}
)
assert response.status_code == 422 # Assuming 422 is the status code for invalid input
@@ -164,10 +164,10 @@ async def test_read_flows_no_pagination_params(client: AsyncClient, logged_in_he
number_of_flows = 30
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
response = await client.get("api/v1/flows/", headers=logged_in_headers, params={"get_all": False})
response = await client.get("v1/flows/", headers=logged_in_headers, params={"get_all": False})
assert response.status_code == 200
# Assert default pagination values, adjust these according to your API's default behavior
assert response.json()["page"] == 1
@@ -185,11 +185,11 @@ async def test_read_flows_components_only_paginated(client: AsyncClient, logged_
]
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
response = await client.get(
"api/v1/flows/", headers=logged_in_headers, params={"components_only": True, "get_all": False}
"v1/flows/", headers=logged_in_headers, params={"components_only": True, "get_all": False}
)
assert response.status_code == 200
@@ -208,9 +208,9 @@ async def test_read_flows_components_only(client: AsyncClient, logged_in_headers
for i in range(number_of_flows)
]
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
response = await client.get("api/v1/flows/", headers=logged_in_headers, params={"components_only": True})
response = await client.get("v1/flows/", headers=logged_in_headers, params={"components_only": True})
assert response.status_code == 200
response_json = response.json()
assert all(flow["is_component"] is True for flow in response_json)
@@ -221,12 +221,12 @@ async def test_read_flow(client: AsyncClient, json_flow: str, logged_in_headers)
data = flow["data"]
unique_name = str(uuid4())
flow = FlowCreate(name=unique_name, description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
flow_id = response.json()["id"] # flow_id should be a UUID but is a string
# turn it into a UUID
flow_id = UUID(flow_id)
response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
response = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["name"] == flow.name
assert response.json()["data"] == flow.data
@@ -238,7 +238,7 @@ async def test_update_flow(client: AsyncClient, json_flow: str, logged_in_header
data = flow["data"]
flow = FlowCreate(name="Test Flow", description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
flow_id = response.json()["id"]
updated_flow = FlowUpdate(
@@ -246,7 +246,7 @@ async def test_update_flow(client: AsyncClient, json_flow: str, logged_in_header
description="updated description",
data=data,
)
response = await client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
response = await client.patch(f"v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["name"] == updated_flow.name
@@ -259,9 +259,9 @@ async def test_delete_flow(client: AsyncClient, json_flow: str, logged_in_header
flow = orjson.loads(json_flow)
data = flow["data"]
flow = FlowCreate(name="Test Flow", description="description", data=data)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
flow_id = response.json()["id"]
response = await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
response = await client.delete(f"v1/flows/{flow_id}", headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["message"] == "Flow deleted successfully"
@@ -273,11 +273,11 @@ async def test_delete_flows(client: AsyncClient, logged_in_headers):
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
flow_ids = []
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_ids.append(response.json()["id"])
response = await client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids)
response = await client.request("DELETE", "v1/flows/", headers=logged_in_headers, json=flow_ids)
assert response.status_code == 200, response.content
assert response.json().get("deleted") == number_of_flows
@@ -289,7 +289,7 @@ async def test_delete_flows_with_transaction_and_build(client: AsyncClient, logg
flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)]
flow_ids = []
for flow in flows:
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_ids.append(response.json()["id"])
@@ -321,13 +321,13 @@ async def test_delete_flows_with_transaction_and_build(client: AsyncClient, logg
artifacts=build.get("artifacts"),
)
response = await client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids)
response = await client.request("DELETE", "v1/flows/", headers=logged_in_headers, json=flow_ids)
assert response.status_code == 200, response.content
assert response.json().get("deleted") == number_of_flows
for flow_id in flow_ids:
response = await client.request(
"GET", "api/v1/monitor/transactions", params={"flow_id": flow_id}, headers=logged_in_headers
"GET", "v1/monitor/transactions", params={"flow_id": flow_id}, headers=logged_in_headers
)
assert response.status_code == 200
json_response = response.json()
@@ -335,7 +335,7 @@ async def test_delete_flows_with_transaction_and_build(client: AsyncClient, logg
for flow_id in flow_ids:
response = await client.request(
"GET", "api/v1/monitor/builds", params={"flow_id": flow_id}, headers=logged_in_headers
"GET", "v1/monitor/builds", params={"flow_id": flow_id}, headers=logged_in_headers
)
assert response.status_code == 200
assert response.json() == {"vertex_builds": {}}
@@ -347,7 +347,7 @@ async def test_delete_folder_with_flows_with_transaction_and_build(client: Async
folder_name = f"Test Project {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description", components_list=[], flows_list=[])
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}"
created_folder = response.json()
@@ -359,7 +359,7 @@ async def test_delete_folder_with_flows_with_transaction_and_build(client: Async
flow_ids = []
for flow in flows:
flow.folder_id = folder_id
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_ids.append(response.json()["id"])
@@ -395,12 +395,12 @@ async def test_delete_folder_with_flows_with_transaction_and_build(client: Async
artifacts=build.get("artifacts"),
)
response = await client.request("DELETE", f"api/v1/projects/{folder_id}", headers=logged_in_headers)
response = await client.request("DELETE", f"v1/projects/{folder_id}", headers=logged_in_headers)
assert response.status_code == 204
for flow_id in flow_ids:
response = await client.request(
"GET", "api/v1/monitor/transactions", params={"flow_id": flow_id}, headers=logged_in_headers
"GET", "v1/monitor/transactions", params={"flow_id": flow_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.json()
json_response = response.json()
@@ -408,7 +408,7 @@ async def test_delete_folder_with_flows_with_transaction_and_build(client: Async
for flow_id in flow_ids:
response = await client.request(
"GET", "api/v1/monitor/builds", params={"flow_id": flow_id}, headers=logged_in_headers
"GET", "v1/monitor/builds", params={"flow_id": flow_id}, headers=logged_in_headers
)
assert response.status_code == 200
assert response.json() == {"vertex_builds": {}}
@@ -419,14 +419,14 @@ async def test_get_flows_from_folder_pagination(client: AsyncClient, logged_in_h
folder_name = f"Test Project {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description", components_list=[], flows_list=[])
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}"
created_folder = response.json()
folder_id = created_folder["id"]
response = await client.get(
f"api/v1/projects/{folder_id}", headers=logged_in_headers, params={"page": 1, "size": 50}
f"v1/projects/{folder_id}", headers=logged_in_headers, params={"page": 1, "size": 50}
)
assert response.status_code == 200
assert response.json()["folder"]["name"] == folder_name
@@ -443,14 +443,14 @@ async def test_get_flows_from_folder_pagination_with_params(client: AsyncClient,
folder_name = f"Test Project {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description", components_list=[], flows_list=[])
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}"
created_folder = response.json()
folder_id = created_folder["id"]
response = await client.get(
f"api/v1/projects/{folder_id}", headers=logged_in_headers, params={"page": 3, "size": 10}
f"v1/projects/{folder_id}", headers=logged_in_headers, params={"page": 3, "size": 10}
)
assert response.status_code == 200
assert response.json()["folder"]["name"] == folder_name
@@ -476,7 +476,7 @@ async def test_create_flows(client: AsyncClient, json_flow: str, logged_in_heade
]
)
# Make request to endpoint
response = await client.post("api/v1/flows/batch/", json=flow_list.dict(), headers=logged_in_headers)
response = await client.post("v1/flows/batch/", json=flow_list.dict(), headers=logged_in_headers)
# Check response status code
assert response.status_code == 201
# Check response data
@@ -505,7 +505,7 @@ async def test_upload_file(client: AsyncClient, json_flow: str, logged_in_header
)
file_contents = orjson_dumps(flow_list.dict())
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("examples.json", file_contents, "application/json")},
headers=logged_in_headers,
)
@@ -553,7 +553,7 @@ async def test_download_file(
flow_ids = [str(db_flow.id) for db_flow in saved_flows] # Convert UUIDs to strings
flow_ids_json = json.dumps(flow_ids)
response = await client.post(
"api/v1/flows/download/",
"v1/flows/download/",
data=flow_ids_json,
headers={**logged_in_headers, "Content-Type": "application/json"},
)
@@ -590,12 +590,12 @@ async def test_download_single_flow_returns_normalized_json(client: AsyncClient,
},
)
create_response = await client.post("api/v1/flows/", json=flow_payload.model_dump(), headers=logged_in_headers)
create_response = await client.post("v1/flows/", json=flow_payload.model_dump(), headers=logged_in_headers)
assert create_response.status_code == 201
flow_id = create_response.json()["id"]
download_response = await client.post(
"api/v1/flows/download/",
"v1/flows/download/",
data=json.dumps([flow_id]),
headers={**logged_in_headers, "Content-Type": "application/json"},
)
@@ -634,7 +634,7 @@ async def test_upload_zip_file_to_flows(client: AsyncClient, json_flow: str, log
zip_buffer.seek(0)
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("flows.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -665,7 +665,7 @@ async def test_upload_zip_file_to_projects(client: AsyncClient, json_flow: str,
zip_buffer.seek(0)
response = await client.post(
"api/v1/projects/upload/",
"v1/projects/upload/",
files={"file": ("My Project.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -678,7 +678,7 @@ async def test_upload_zip_file_to_projects(client: AsyncClient, json_flow: str,
# Verify the project name was derived from the ZIP filename
folder_id = folder_ids.pop()
project_response = await client.get(f"api/v1/projects/{folder_id}", headers=logged_in_headers)
project_response = await client.get(f"v1/projects/{folder_id}", headers=logged_in_headers)
assert project_response.status_code == 200
assert project_response.json()["name"].startswith("My Project")
@@ -692,7 +692,7 @@ async def test_upload_empty_zip_returns_400(client: AsyncClient, logged_in_heade
zip_buffer.seek(0)
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("empty.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -730,7 +730,7 @@ async def test_download_then_upload_roundtrip(client: AsyncClient, json_flow: st
# Download as ZIP
download_response = await client.post(
"api/v1/flows/download/",
"v1/flows/download/",
data=json.dumps(flow_ids),
headers={**logged_in_headers, "Content-Type": "application/json"},
)
@@ -739,7 +739,7 @@ async def test_download_then_upload_roundtrip(client: AsyncClient, json_flow: st
# Re-upload the ZIP
upload_response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("flows.zip", download_response.content, "application/zip")},
headers=logged_in_headers,
)
@@ -762,7 +762,7 @@ async def test_upload_zip_with_invalid_json(client: AsyncClient, json_flow: str,
zip_buffer.seek(0)
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("mixed.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -789,7 +789,7 @@ async def test_upload_zip_exceeding_max_entries(client: AsyncClient, json_flow:
zip_buffer.seek(0)
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("too_many.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -821,7 +821,7 @@ async def test_upload_zip_with_oversized_entry(client: AsyncClient, json_flow: s
zip_buffer.seek(0)
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("oversized.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -856,7 +856,7 @@ async def test_upload_zip_with_mixed_valid_invalid(client: AsyncClient, json_flo
zip_buffer.seek(0)
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("mixed.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -880,7 +880,7 @@ async def test_upload_zip_to_projects_filename_none(client: AsyncClient, json_fl
# filename=".zip" → rsplit gives ("", "zip") → "" is falsy → "Imported Project"
response = await client.post(
"api/v1/projects/upload/",
"v1/projects/upload/",
files={"file": (".zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -889,7 +889,7 @@ async def test_upload_zip_to_projects_filename_none(client: AsyncClient, json_fl
assert len(response_data) == 1
folder_id = response_data[0]["folder_id"]
project_response = await client.get(f"api/v1/projects/{folder_id}", headers=logged_in_headers)
project_response = await client.get(f"v1/projects/{folder_id}", headers=logged_in_headers)
assert project_response.status_code == 200
assert project_response.json()["name"].startswith("Imported Project")
@@ -926,7 +926,7 @@ async def test_upload_json_file_to_projects_rejoins_code_lines(client: AsyncClie
}
response = await client.post(
"api/v1/projects/upload/",
"v1/projects/upload/",
files={"file": ("project.json", json.dumps(payload).encode("utf-8"), "application/json")},
headers=logged_in_headers,
)
@@ -939,7 +939,7 @@ async def test_upload_json_file_to_projects_rejoins_code_lines(client: AsyncClie
== "print('alpha')\nprint('beta')"
)
project_response = await client.get(f"api/v1/projects/{response_data[0]['folder_id']}", headers=logged_in_headers)
project_response = await client.get(f"v1/projects/{response_data[0]['folder_id']}", headers=logged_in_headers)
assert project_response.status_code == 200
assert project_response.json()["name"].startswith(project_name)
@@ -948,7 +948,7 @@ async def test_upload_json_file_to_projects_rejoins_code_lines(client: AsyncClie
async def test_download_project_zip_sanitizes_flow_names(client: AsyncClient, json_flow: str, logged_in_headers):
"""Project ZIP downloads must sanitize flow names to prevent Zip Slip paths."""
project_response = await client.post(
"api/v1/projects/",
"v1/projects/",
json={"name": f"Download Project {uuid4()}", "description": "", "flows_list": [], "components_list": []},
headers=logged_in_headers,
)
@@ -957,7 +957,7 @@ async def test_download_project_zip_sanitizes_flow_names(client: AsyncClient, js
flow = orjson.loads(json_flow)
create_response = await client.post(
"api/v1/flows/",
"v1/flows/",
json={
"name": "../escaped-flow",
"description": "path traversal test",
@@ -969,7 +969,7 @@ async def test_download_project_zip_sanitizes_flow_names(client: AsyncClient, js
)
assert create_response.status_code == 201
download_response = await client.get(f"api/v1/projects/download/{project_id}", headers=logged_in_headers)
download_response = await client.get(f"v1/projects/download/{project_id}", headers=logged_in_headers)
assert download_response.status_code == 200
with zipfile.ZipFile(io.BytesIO(download_response.content), "r") as zip_file:
@@ -994,7 +994,7 @@ async def test_upload_bad_zip_file_returns_400(client: AsyncClient, logged_in_he
corrupt_zip = b"garbage" * 10 + valid_zip[-22:]
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("corrupt.zip", corrupt_zip, "application/zip")},
headers=logged_in_headers,
)
@@ -1007,7 +1007,7 @@ async def test_upload_bad_zip_file_returns_400(client: AsyncClient, logged_in_he
async def test_upload_no_file_to_flows_returns_400(client: AsyncClient, logged_in_headers):
"""Uploading with no file to flows endpoint returns 400."""
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
headers=logged_in_headers,
)
assert response.status_code == 400
@@ -1018,7 +1018,7 @@ async def test_upload_no_file_to_flows_returns_400(client: AsyncClient, logged_i
async def test_upload_no_file_to_projects_returns_400(client: AsyncClient, logged_in_headers):
"""Uploading with no file to projects endpoint returns 400."""
response = await client.post(
"api/v1/projects/upload/",
"v1/projects/upload/",
headers=logged_in_headers,
)
assert response.status_code == 400
@@ -1029,7 +1029,7 @@ async def test_upload_no_file_to_projects_returns_400(client: AsyncClient, logge
async def test_upload_empty_file_to_flows_returns_400(client: AsyncClient, logged_in_headers):
"""Uploading an empty file to flows endpoint returns 400."""
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("empty.json", b"", "application/json")},
headers=logged_in_headers,
)
@@ -1041,7 +1041,7 @@ async def test_upload_empty_file_to_flows_returns_400(client: AsyncClient, logge
async def test_upload_empty_file_to_projects_returns_400(client: AsyncClient, logged_in_headers):
"""Uploading an empty file to projects endpoint returns 400."""
response = await client.post(
"api/v1/projects/upload/",
"v1/projects/upload/",
files={"file": ("empty.json", b"", "application/json")},
headers=logged_in_headers,
)
@@ -1053,7 +1053,7 @@ async def test_upload_empty_file_to_projects_returns_400(client: AsyncClient, lo
async def test_upload_invalid_json_to_flows_returns_400(client: AsyncClient, logged_in_headers):
"""Uploading invalid JSON content to flows endpoint returns 400."""
response = await client.post(
"api/v1/flows/upload/",
"v1/flows/upload/",
files={"file": ("bad.json", b"this is not json", "application/json")},
headers=logged_in_headers,
)
@@ -1065,7 +1065,7 @@ async def test_upload_invalid_json_to_flows_returns_400(client: AsyncClient, log
async def test_upload_invalid_json_to_projects_returns_400(client: AsyncClient, logged_in_headers):
"""Uploading invalid JSON content to projects endpoint returns 400."""
response = await client.post(
"api/v1/projects/upload/",
"v1/projects/upload/",
files={"file": ("bad.json", b"this is not json", "application/json")},
headers=logged_in_headers,
)
@@ -1086,7 +1086,7 @@ async def test_upload_zip_to_projects_batch_name_dedup(client: AsyncClient, json
zip_buffer.seek(0)
response = await client.post(
"api/v1/projects/upload/",
"v1/projects/upload/",
files={"file": ("dedup_test.zip", zip_buffer.getvalue(), "application/zip")},
headers=logged_in_headers,
)
@@ -1101,14 +1101,14 @@ async def test_upload_zip_to_projects_batch_name_dedup(client: AsyncClient, json
@pytest.mark.usefixtures("active_user")
async def test_create_flow_with_invalid_data(client: AsyncClient, logged_in_headers):
flow = {"name": "a" * 256, "data": "Invalid flow data"}
response = await client.post("api/v1/flows/", json=flow, headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow, headers=logged_in_headers)
assert response.status_code == 422
@pytest.mark.usefixtures("active_user")
async def test_get_nonexistent_flow(client: AsyncClient, logged_in_headers):
uuid = uuid4()
response = await client.get(f"api/v1/flows/{uuid}", headers=logged_in_headers)
response = await client.get(f"v1/flows/{uuid}", headers=logged_in_headers)
assert response.status_code == 404
@@ -1117,11 +1117,11 @@ async def test_update_flow_idempotency(client: AsyncClient, json_flow: str, logg
flow_data = orjson.loads(json_flow)
data = flow_data["data"]
flow_data = FlowCreate(name="Test Flow", description="description", data=data)
response = await client.post("api/v1/flows/", json=flow_data.dict(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow_data.dict(), headers=logged_in_headers)
flow_id = response.json()["id"]
updated_flow = FlowCreate(name="Updated Flow", description="description", data=data)
response1 = await client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
response2 = await client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
response1 = await client.put(f"v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
response2 = await client.put(f"v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers)
assert response1.json() == response2.json()
@@ -1135,20 +1135,20 @@ async def test_update_nonexistent_flow(client: AsyncClient, json_flow: str, logg
description="description",
data=data,
)
response = await client.patch(f"api/v1/flows/{uuid}", json=updated_flow.model_dump(), headers=logged_in_headers)
response = await client.patch(f"v1/flows/{uuid}", json=updated_flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 404, response.text
@pytest.mark.usefixtures("active_user")
async def test_delete_nonexistent_flow(client: AsyncClient, logged_in_headers):
uuid = uuid4()
response = await client.delete(f"api/v1/flows/{uuid}", headers=logged_in_headers)
response = await client.delete(f"v1/flows/{uuid}", headers=logged_in_headers)
assert response.status_code == 404
@pytest.mark.usefixtures("active_user")
async def test_read_only_starter_projects(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/flows/basic_examples/", headers=logged_in_headers)
response = await client.get("v1/flows/basic_examples/", headers=logged_in_headers)
starter_projects = await load_starter_projects()
assert response.status_code == 200
assert len(response.json()) == len(starter_projects)
@@ -1197,13 +1197,13 @@ async def test_read_folder(client: AsyncClient, logged_in_headers):
# Create a new project
folder_name = f"Test Project {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description")
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
created_folder = response.json()
folder_id = created_folder["id"]
# Read the project
response = await client.get(f"api/v1/projects/{folder_id}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{folder_id}", headers=logged_in_headers)
assert response.status_code == 200
folder_data = response.json()
assert folder_data["name"] == folder_name
@@ -1217,14 +1217,14 @@ async def test_read_folder_with_pagination(client: AsyncClient, logged_in_header
# Create a new project
folder_name = f"Test Project {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description")
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
created_folder = response.json()
folder_id = created_folder["id"]
# Read the project with pagination
response = await client.get(
f"api/v1/projects/{folder_id}", headers=logged_in_headers, params={"page": 1, "size": 10}
f"v1/projects/{folder_id}", headers=logged_in_headers, params={"page": 1, "size": 10}
)
assert response.status_code == 200
folder_data = response.json()
@@ -1244,7 +1244,7 @@ async def test_read_folder_with_flows(client: AsyncClient, json_flow: str, logge
folder_name = f"Test Project {uuid4()}"
flow_name = f"Test Flow {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description")
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
created_folder = response.json()
folder_id = created_folder["id"]
@@ -1254,11 +1254,11 @@ async def test_read_folder_with_flows(client: AsyncClient, json_flow: str, logge
data = flow_data["data"]
flow = FlowCreate(name=flow_name, description="description", data=data)
flow.folder_id = folder_id
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
# Read the project with flows
response = await client.get(f"api/v1/projects/{folder_id}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{folder_id}", headers=logged_in_headers)
assert response.status_code == 200
folder_data = response.json()
assert folder_data["name"] == folder_name
@@ -1270,7 +1270,7 @@ async def test_read_folder_with_flows(client: AsyncClient, json_flow: str, logge
@pytest.mark.usefixtures("active_user")
async def test_read_nonexistent_folder(client: AsyncClient, logged_in_headers):
nonexistent_id = str(uuid4())
response = await client.get(f"api/v1/projects/{nonexistent_id}", headers=logged_in_headers)
response = await client.get(f"v1/projects/{nonexistent_id}", headers=logged_in_headers)
assert response.status_code == 404
assert response.json()["detail"] == "Project not found"
@@ -1280,7 +1280,7 @@ async def test_read_folder_with_search(client: AsyncClient, json_flow: str, logg
# Create a new project
folder_name = f"Test Project {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description")
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
created_folder = response.json()
folder_id = created_folder["id"]
@@ -1298,12 +1298,12 @@ async def test_read_folder_with_search(client: AsyncClient, json_flow: str, logg
)
flow1.folder_id = folder_id
flow2.folder_id = folder_id
await client.post("api/v1/flows/", json=flow1.model_dump(), headers=logged_in_headers)
await client.post("api/v1/flows/", json=flow2.model_dump(), headers=logged_in_headers)
await client.post("v1/flows/", json=flow1.model_dump(), headers=logged_in_headers)
await client.post("v1/flows/", json=flow2.model_dump(), headers=logged_in_headers)
# Read the project with search
response = await client.get(
f"api/v1/projects/{folder_id}", headers=logged_in_headers, params={"search": "Test", "page": 1, "size": 10}
f"v1/projects/{folder_id}", headers=logged_in_headers, params={"search": "Test", "page": 1, "size": 10}
)
assert response.status_code == 200
folder_data = response.json()
@@ -1316,7 +1316,7 @@ async def test_read_folder_with_component_filter(client: AsyncClient, json_flow:
# Create a new project
folder_name = f"Test Project {uuid4()}"
project = FolderCreate(name=folder_name, description="Test project description")
response = await client.post("api/v1/projects/", json=project.model_dump(), headers=logged_in_headers)
response = await client.post("v1/projects/", json=project.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
created_folder = response.json()
folder_id = created_folder["id"]
@@ -1332,11 +1332,11 @@ async def test_read_folder_with_component_filter(client: AsyncClient, json_flow:
is_component=True,
)
component_flow.folder_id = folder_id
await client.post("api/v1/flows/", json=component_flow.model_dump(), headers=logged_in_headers)
await client.post("v1/flows/", json=component_flow.model_dump(), headers=logged_in_headers)
# Read the project with component filter
response = await client.get(
f"api/v1/projects/{folder_id}", headers=logged_in_headers, params={"is_component": True, "page": 1, "size": 10}
f"v1/projects/{folder_id}", headers=logged_in_headers, params={"is_component": True, "page": 1, "size": 10}
)
assert response.status_code == 200
folder_data = response.json()
+23 -23
View File
@@ -32,7 +32,7 @@ async def run_post(client, flow_id, headers, post_data):
AssertionError: If the response status code is not 200.
"""
response = await client.post(
f"api/v1/process/{flow_id}",
f"v1/process/{flow_id}",
headers=headers,
json=post_data,
)
@@ -134,11 +134,11 @@ PROMPT_REQUEST = {
async def test_get_all(client: AsyncClient, logged_in_headers):
"""Tests the retrieval of all available components from the API.
Sends a GET request to the `api/v1/all` endpoint and verifies that the returned component names
Sends a GET request to the `v1/all` endpoint and verifies that the returned component names
correspond to files in the components directory. Also checks for the presence of specific components
such as "ChatInput", "Prompt", and "ChatOutput" in the response.
"""
response = await client.get("api/v1/all", headers=logged_in_headers)
response = await client.get("v1/all", headers=logged_in_headers)
assert response.status_code == 200
dir_reader = DirectoryReader(BASE_COMPONENTS_PATH)
files = dir_reader.get_files()
@@ -163,7 +163,7 @@ import math
def square(x):
return x ** 2
"""
response1 = await client.post("api/v1/validate/code", json={"code": code1}, headers=logged_in_headers)
response1 = await client.post("v1/validate/code", json={"code": code1}, headers=logged_in_headers)
assert response1.status_code == 200
assert response1.json() == {"imports": {"errors": []}, "function": {"errors": []}}
@@ -174,7 +174,7 @@ import non_existent_module
def square(x):
return x ** 2
"""
response2 = await client.post("api/v1/validate/code", json={"code": code2}, headers=logged_in_headers)
response2 = await client.post("v1/validate/code", json={"code": code2}, headers=logged_in_headers)
assert response2.status_code == 200
assert response2.json() == {
"imports": {"errors": ["No module named 'non_existent_module'"]},
@@ -188,7 +188,7 @@ import math
def square(x)
return x ** 2
"""
response3 = await client.post("api/v1/validate/code", json={"code": code3}, headers=logged_in_headers)
response3 = await client.post("v1/validate/code", json={"code": code3}, headers=logged_in_headers)
assert response3.status_code == 200
assert response3.json() == {
"imports": {"errors": []},
@@ -196,11 +196,11 @@ def square(x)
}
# Test case with invalid JSON payload
response4 = await client.post("api/v1/validate/code", json={"invalid_key": code1}, headers=logged_in_headers)
response4 = await client.post("v1/validate/code", json={"invalid_key": code1}, headers=logged_in_headers)
assert response4.status_code == 422
# Test case with an empty code string
response5 = await client.post("api/v1/validate/code", json={"code": ""}, headers=logged_in_headers)
response5 = await client.post("v1/validate/code", json={"code": ""}, headers=logged_in_headers)
assert response5.status_code == 200
assert response5.json() == {"imports": {"errors": []}, "function": {"errors": []}}
@@ -211,7 +211,7 @@ import math
def square(x)
return x ** 2
"""
response6 = await client.post("api/v1/validate/code", json={"code": code6}, headers=logged_in_headers)
response6 = await client.post("v1/validate/code", json={"code": code6}, headers=logged_in_headers)
assert response6.status_code == 200
assert response6.json() == {
"imports": {"errors": []},
@@ -238,7 +238,7 @@ INVALID_PROMPT = "This is an invalid prompt without any input variable."
async def test_valid_prompt(client: AsyncClient, logged_in_headers):
PROMPT_REQUEST["template"] = VALID_PROMPT
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
response = await client.post("v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["input_variables"] == ["product"]
@@ -246,7 +246,7 @@ async def test_valid_prompt(client: AsyncClient, logged_in_headers):
async def test_invalid_prompt(client: AsyncClient, logged_in_headers):
PROMPT_REQUEST["template"] = INVALID_PROMPT
response = await client.post(
"api/v1/validate/prompt",
"v1/validate/prompt",
json=PROMPT_REQUEST,
headers=logged_in_headers,
)
@@ -265,7 +265,7 @@ async def test_invalid_prompt(client: AsyncClient, logged_in_headers):
)
async def test_various_prompts(client, logged_in_headers, prompt, expected_input_variables):
PROMPT_REQUEST["template"] = prompt
response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
response = await client.post("v1/validate/prompt", json=PROMPT_REQUEST, headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["input_variables"] == expected_input_variables
@@ -295,7 +295,7 @@ async def test_get_vertices_blocks_custom_components_when_disabled(
monkeypatch.setattr(get_settings_service().settings, "allow_custom_components", False)
flow_id = added_flow_webhook_test["id"]
response = await client.post(f"/api/v1/build/{flow_id}/vertices", headers=logged_in_headers)
response = await client.post(f"/v1/build/{flow_id}/vertices", headers=logged_in_headers)
assert response.status_code == 400
assert "outdated components must be updated before running" in response.json()["detail"]
@@ -582,7 +582,7 @@ async def test_invalid_flow_id(client, created_api_key):
@pytest.mark.benchmark
async def test_starter_projects(client, created_api_key):
headers = {"x-api-key": created_api_key.api_key}
response = await client.get("api/v1/starter-projects/", headers=headers)
response = await client.get("v1/starter-projects/", headers=headers)
assert response.status_code == status.HTTP_200_OK, response.text
@@ -720,7 +720,7 @@ async def test_user_cannot_run_other_users_flow(client: AsyncClient, simple_api_
LE-639: post-fix behavior is 404 (not 403) so we don't leak flow existence
via a 403-vs-404 oracle. See ``get_flow_for_api_key_user`` in
``api/v1/endpoints.py``.
``v1/endpoints.py``.
"""
# simple_api_test belongs to active_user, but we're using user_two's API key
headers = {"x-api-key": user_two_api_key}
@@ -860,14 +860,14 @@ async def test_user_cannot_run_other_users_flow_session_endpoint(
)
try:
login_response = await client.post(
"api/v1/login",
"v1/login",
data={"username": other_username, "password": other_password},
)
assert login_response.status_code == 200
other_headers = {"Authorization": f"Bearer {login_response.json()['access_token']}"}
flow_id = simple_api_test["id"] # owned by active_user via logged_in_headers
response = await client.post(f"/api/v1/run/session/{flow_id}", headers=other_headers)
response = await client.post(f"/v1/run/session/{flow_id}", headers=other_headers)
assert response.status_code == status.HTTP_404_NOT_FOUND, response.text
assert "permission" not in response.text.lower()
@@ -897,7 +897,7 @@ async def test_run_rejects_malformed_user_id_query_param(client: AsyncClient, si
for bad in ("not-a-uuid", "", "12345"):
response = await client.post(
f"/api/v1/run/{flow_id}",
f"/v1/run/{flow_id}",
headers=headers,
params={"user_id": bad},
)
@@ -964,12 +964,12 @@ async def test_user_can_access_multiple_own_flows(
# Create two flows for the same user
flow1_data = orjson.loads(json_simple_api_test)
flow1 = FlowCreate(name="Flow 1", data=flow1_data["data"], description="First flow")
response1 = await client.post("api/v1/flows/", json=flow1.model_dump(), headers=logged_in_headers)
response1 = await client.post("v1/flows/", json=flow1.model_dump(), headers=logged_in_headers)
assert response1.status_code == 201
flow1_id = response1.json()["id"]
flow2 = FlowCreate(name="Flow 2", data=flow1_data["data"], description="Second flow")
response2 = await client.post("api/v1/flows/", json=flow2.model_dump(), headers=logged_in_headers)
response2 = await client.post("v1/flows/", json=flow2.model_dump(), headers=logged_in_headers)
assert response2.status_code == 201
flow2_id = response2.json()["id"]
@@ -983,8 +983,8 @@ async def test_user_can_access_multiple_own_flows(
assert response_flow2.status_code == status.HTTP_200_OK, response_flow2.text
# Cleanup
await client.delete(f"api/v1/flows/{flow1_id}", headers=logged_in_headers)
await client.delete(f"api/v1/flows/{flow2_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{flow1_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{flow2_id}", headers=logged_in_headers)
# ============================================================================
@@ -1129,7 +1129,7 @@ async def test_openai_responses_rejects_cross_user_flow_access(
"stream": False,
}
response = await client.post(
"/api/v1/responses",
"/v1/responses",
json=payload,
headers={"x-api-key": attacker_api_key},
)
+4 -4
View File
@@ -546,7 +546,7 @@ class TestExpandFlowEndpoint:
"edges": [],
}
response = await client.post("api/v1/flows/expand/", json=compact_data)
response = await client.post("v1/flows/expand/", json=compact_data)
# Should return 401 or 403 without auth
assert response.status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]
@@ -559,7 +559,7 @@ class TestExpandFlowEndpoint:
"edges": [],
}
response = await client.post("api/v1/flows/expand/", json=compact_data, headers=logged_in_headers)
response = await client.post("v1/flows/expand/", json=compact_data, headers=logged_in_headers)
# Component might not exist in test env, but endpoint should work
assert response.status_code in [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST]
@@ -570,7 +570,7 @@ class TestExpandFlowEndpoint:
"edges": [],
}
response = await client.post("api/v1/flows/expand/", json=compact_data, headers=logged_in_headers)
response = await client.post("v1/flows/expand/", json=compact_data, headers=logged_in_headers)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "not found" in response.json()["detail"]
@@ -588,7 +588,7 @@ class TestExpandFlowEndpoint:
],
}
response = await client.post("api/v1/flows/expand/", json=compact_data, headers=logged_in_headers)
response = await client.post("v1/flows/expand/", json=compact_data, headers=logged_in_headers)
# Should fail due to missing source node
assert response.status_code == status.HTTP_400_BAD_REQUEST
+4 -4
View File
@@ -325,7 +325,7 @@ async def test_sync_flows_from_fs(client: AsyncClient, logged_in_headers):
"locked": False,
"fs_path": flow_filename,
}
response = await client.post("api/v1/flows/", json=basic_case, headers=logged_in_headers)
response = await client.post("v1/flows/", json=basic_case, headers=logged_in_headers)
assert response.status_code == 201, f"Failed to create flow: {response.text}"
created_flow = response.json()
flow_id = created_flow["id"]
@@ -350,7 +350,7 @@ async def test_sync_flows_from_fs(client: AsyncClient, logged_in_headers):
result = {}
for i in range(10):
response = await client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
response = await client.get(f"v1/flows/{flow_id}", headers=logged_in_headers)
result = response.json()
if result["name"] == "new name":
break
@@ -477,7 +477,7 @@ async def test_copy_profile_pictures_source_exists():
@pytest.mark.usefixtures("client")
async def test_profile_pictures_available_via_api(client: AsyncClient, logged_in_headers):
"""Test that profile pictures are available via the API after app startup."""
response = await client.get("api/v1/files/profile_pictures/list", headers=logged_in_headers)
response = await client.get("v1/files/profile_pictures/list", headers=logged_in_headers)
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.json()}"
@@ -499,7 +499,7 @@ async def test_profile_pictures_available_via_api(client: AsyncClient, logged_in
async def test_profile_picture_can_be_downloaded(client: AsyncClient, logged_in_headers):
"""Test that a profile picture can be downloaded via the API."""
response = await client.get(
"api/v1/files/profile_pictures/Space/046-rocket.svg",
"v1/files/profile_pictures/Space/046-rocket.svg",
headers=logged_in_headers,
)
@@ -300,7 +300,7 @@ class TestDeleteEndpoint:
mock_root.return_value = tmp_path
(tmp_path / "activeuser" / "My_KB").mkdir(parents=True)
response = await client.delete("api/v1/knowledge_bases/My_KB", headers=logged_in_headers)
response = await client.delete("v1/knowledge_bases/My_KB", headers=logged_in_headers)
assert response.status_code == 200
@@ -312,14 +312,14 @@ class TestDeleteEndpoint:
mock_root.return_value = tmp_path
(tmp_path / "activeuser" / "My_KB").mkdir(parents=True)
response = await client.delete("api/v1/knowledge_bases/My_KB", headers=logged_in_headers)
response = await client.delete("v1/knowledge_bases/My_KB", headers=logged_in_headers)
assert response.status_code == 500
assert "may be in use" in response.json()["detail"]
mock_delete.assert_called_once()
async def test_should_return_404_when_kb_not_found(self, client, logged_in_headers):
response = await client.delete("api/v1/knowledge_bases/NonExistent_KB", headers=logged_in_headers)
response = await client.delete("v1/knowledge_bases/NonExistent_KB", headers=logged_in_headers)
assert response.status_code == 404
@@ -340,7 +340,7 @@ class TestBulkDeleteEndpoint:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["KB1", "KB2"]},
)
@@ -362,7 +362,7 @@ class TestBulkDeleteEndpoint:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["KB1", "KB2"]},
)
@@ -381,7 +381,7 @@ class TestBulkDeleteEndpoint:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["KB1", "Ghost"]},
)
@@ -173,7 +173,7 @@ class TestPreviewChunks:
async def test_preview_chunks_basic(self, client: AsyncClient, logged_in_headers, sample_text_file):
file_name, file_content = sample_text_file
response = await client.post(
"api/v1/knowledge_bases/preview-chunks",
"v1/knowledge_bases/preview-chunks",
headers=logged_in_headers,
files={"files": (file_name, io.BytesIO(file_content.encode()), "text/plain")},
data={
@@ -200,7 +200,7 @@ class TestKnowledgeBaseAPI:
mock_root.return_value = tmp_path
kb_name = "New_KB"
response = await client.post(
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={
"name": kb_name,
@@ -231,7 +231,7 @@ class TestKnowledgeBaseAPI:
victim_dir = tmp_path / "victim_user" / "evil_kb"
response = await client.post(
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={
"name": "../victim_user/evil_kb",
@@ -261,7 +261,7 @@ class TestKnowledgeBaseAPI:
evil_dir = tmp_path / "evil_absolute"
response = await client.post(
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={
"name": str(evil_dir),
@@ -295,7 +295,7 @@ class TestKnowledgeBaseAPI:
victim_kb.mkdir(parents=True)
response = await client.post(
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={
"name": "../activeuser_evil/secret_kb",
@@ -324,7 +324,7 @@ class TestKnowledgeBaseAPI:
(tmp_path / "victim_user" / "secret_kb").mkdir(parents=True)
await client.post(
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={
"name": "../victim_user/secret_kb",
@@ -341,7 +341,7 @@ class TestKnowledgeBaseAPI:
async def test_create_kb_name_too_short(self, client: AsyncClient, logged_in_headers):
response = await client.post(
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={
"name": "ab",
@@ -360,7 +360,7 @@ class TestKnowledgeBaseAPI:
(kb_user_path / "Duplicate_KB").mkdir()
response = await client.post(
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={
"name": "Duplicate KB",
@@ -399,7 +399,7 @@ class TestKnowledgeBaseAPI:
mock_job_service.return_value = mock_job_service_inst
mock_job_service_inst.get_latest_jobs_by_asset_ids = AsyncMock(return_value={})
response = await client.get("api/v1/knowledge_bases", headers=logged_in_headers)
response = await client.get("v1/knowledge_bases", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
@@ -422,7 +422,7 @@ class TestKnowledgeBaseAPI:
}
(kb_path / "embedding_metadata.json").write_text(json.dumps(meta))
response = await client.get("api/v1/knowledge_bases/Detail_KB", headers=logged_in_headers)
response = await client.get("v1/knowledge_bases/Detail_KB", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert data["chunks"] == 5
@@ -436,7 +436,7 @@ class TestKnowledgeBaseAPI:
mock_root.return_value = tmp_path
(tmp_path / "activeuser" / "To_Delete").mkdir(parents=True, exist_ok=True)
response = await client.delete("api/v1/knowledge_bases/To_Delete", headers=logged_in_headers)
response = await client.delete("v1/knowledge_bases/To_Delete", headers=logged_in_headers)
assert response.status_code == 200
mock_delete.assert_called_once()
@@ -453,7 +453,7 @@ class TestKnowledgeBaseAPI:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["KB1", "KB2", "NonExistent"]},
)
@@ -476,7 +476,7 @@ class TestKnowledgeBaseAPI:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["../victim_user/secret_kb"]},
)
@@ -499,7 +499,7 @@ class TestKnowledgeBaseAPI:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["../../other_root/secret_kb"]},
)
@@ -524,7 +524,7 @@ class TestKnowledgeBaseAPI:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["../activeuser_evil/secret_kb"]},
)
@@ -553,7 +553,7 @@ class TestKnowledgeBaseAPI:
response = await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["%2e%2e%2fvictim_user%2fsecret_kb"]},
)
@@ -575,7 +575,7 @@ class TestKnowledgeBaseAPI:
await client.request(
"DELETE",
"api/v1/knowledge_bases",
"v1/knowledge_bases",
headers=logged_in_headers,
json={"kb_names": ["../victim_user/secret_kb"]},
)
@@ -621,7 +621,7 @@ class TestKnowledgeBaseAPI:
mock_job_inst.create_job = AsyncMock(return_value=MagicMock(job_id=uuid.uuid4()))
response = await client.post(
"api/v1/knowledge_bases/Ingest-KB/ingest",
"v1/knowledge_bases/Ingest-KB/ingest",
headers=logged_in_headers,
files={"files": (file_name, io.BytesIO(file_content.encode()), "text/plain")},
data={"source_name": "test-source"},
@@ -635,7 +635,7 @@ class TestKnowledgeBaseAPI:
async def test_ingest_non_existent_kb(self, mock_root, client: AsyncClient, logged_in_headers, tmp_path):
mock_root.return_value = tmp_path
response = await client.post(
"api/v1/knowledge_bases/NonExistent/ingest",
"v1/knowledge_bases/NonExistent/ingest",
headers=logged_in_headers,
files={"files": ("test.txt", io.BytesIO(b"content"), "text/plain")},
)
@@ -649,7 +649,7 @@ class TestKnowledgeBaseAPI:
mock_meta.return_value = {"embedding_provider": None, "embedding_model": None}
response = await client.post(
"api/v1/knowledge_bases/Invalid-KB/ingest",
"v1/knowledge_bases/Invalid-KB/ingest",
headers=logged_in_headers,
files={"files": ("test.txt", io.BytesIO(b"content"), "text/plain")},
)
@@ -678,7 +678,7 @@ class TestKnowledgeBaseAPI:
mock_chroma.return_value._collection = mock_collection
# Test search
response = await client.get("api/v1/knowledge_bases/KB1/chunks?search=content", headers=logged_in_headers)
response = await client.get("v1/knowledge_bases/KB1/chunks?search=content", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert len(data["chunks"]) == 2
@@ -693,7 +693,7 @@ class TestKnowledgeBaseAPI:
"documents": ["page 2 content"],
"metadatas": [{}],
}
response = await client.get("api/v1/knowledge_bases/KB1/chunks?page=2&limit=10", headers=logged_in_headers)
response = await client.get("v1/knowledge_bases/KB1/chunks?page=2&limit=10", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert data["page"] == 2
@@ -709,7 +709,7 @@ class TestKnowledgeBaseAPI:
):
mock_root.return_value = tmp_path
response = await client.get("api/v1/knowledge_bases/MissingKB/chunks", headers=logged_in_headers)
response = await client.get("v1/knowledge_bases/MissingKB/chunks", headers=logged_in_headers)
assert response.status_code == 404
@@ -851,7 +851,7 @@ class TestCancelIngestion:
with mock_patch("flow.services.deps.get_service", side_effect=get_service_side_effect):
response = await client.post(
"api/v1/knowledge_bases/Test_KB/cancel",
"v1/knowledge_bases/Test_KB/cancel",
headers=logged_in_headers,
)
@@ -877,7 +877,7 @@ class TestCancelIngestion:
mock_job_service_inst.get_latest_jobs_by_asset_ids = AsyncMock(return_value={})
response = await client.post(
"api/v1/knowledge_bases/Test_KB/cancel",
"v1/knowledge_bases/Test_KB/cancel",
headers=logged_in_headers,
)
@@ -889,7 +889,7 @@ class TestCancelIngestion:
mock_root.return_value = tmp_path
response = await client.post(
"api/v1/knowledge_bases/NonExistent_KB/cancel",
"v1/knowledge_bases/NonExistent_KB/cancel",
headers=logged_in_headers,
)
+6 -6
View File
@@ -23,13 +23,13 @@ async def test_login_successful(client, test_user):
except IntegrityError:
pass
response = await client.post("api/v1/login", data={"username": "testuser", "password": "testpassword"})
response = await client.post("v1/login", data={"username": "testuser", "password": "testpassword"})
assert response.status_code == 200
assert "access_token" in response.json()
async def test_login_unsuccessful_wrong_username(client):
response = await client.post("api/v1/login", data={"username": "wrongusername", "password": "testpassword"})
response = await client.post("v1/login", data={"username": "wrongusername", "password": "testpassword"})
assert response.status_code == 401
assert response.json()["detail"] == "Incorrect username or password"
@@ -39,14 +39,14 @@ async def test_login_unsuccessful_wrong_password(client, test_user, async_sessio
async_session.add(test_user)
await async_session.commit()
response = await client.post("api/v1/login", data={"username": "testuser", "password": "wrongpassword"})
response = await client.post("v1/login", data={"username": "testuser", "password": "wrongpassword"})
assert response.status_code == 401
assert response.json()["detail"] == "Incorrect username or password"
async def test_session_endpoint_unauthenticated(client):
"""Test /session endpoint returns authenticated=False for unauthenticated requests."""
response = await client.get("api/v1/session")
response = await client.get("v1/session")
assert response.status_code == 200
data = response.json()
assert data["authenticated"] is False
@@ -56,7 +56,7 @@ async def test_session_endpoint_unauthenticated(client):
async def test_session_endpoint_authenticated(client, logged_in_headers):
"""Test /session endpoint returns user info for authenticated requests."""
response = await client.get("api/v1/session", headers=logged_in_headers)
response = await client.get("v1/session", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert data["authenticated"] is True
@@ -71,7 +71,7 @@ async def test_session_endpoint_no_api_key_in_response(client, logged_in_headers
This is a security check to ensure API keys are not exposed in HTTP response bodies,
even to authenticated users. API keys should only be stored in httponly cookies.
"""
response = await client.get("api/v1/session", headers=logged_in_headers)
response = await client.get("v1/session", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert data["authenticated"] is True
@@ -54,7 +54,7 @@ async def other_active_user(client): # noqa: ARG001
@pytest.fixture
async def other_logged_in_headers(client: AsyncClient, other_active_user):
login_data = {"username": other_active_user.username, "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
tokens = response.json()
return {"Authorization": f"Bearer {tokens['access_token']}"}
@@ -159,7 +159,7 @@ async def messages_with_datetime_session_id(session, active_user): # noqa: ARG0
@pytest.mark.api_key_required
async def test_delete_messages(client: AsyncClient, created_messages, logged_in_headers):
response = await client.request(
"DELETE", "api/v1/monitor/messages", json=[str(msg.id) for msg in created_messages], headers=logged_in_headers
"DELETE", "v1/monitor/messages", json=[str(msg.id) for msg in created_messages], headers=logged_in_headers
)
assert response.status_code == 204, response.text
assert response.reason_phrase == "No Content"
@@ -169,13 +169,13 @@ async def test_delete_messages(client: AsyncClient, created_messages, logged_in_
async def test_get_messages_does_not_return_other_users_messages(
client: AsyncClient, logged_in_headers, other_logged_in_headers, cross_user_messages
):
response = await client.get("api/v1/monitor/messages", headers=logged_in_headers)
response = await client.get("v1/monitor/messages", headers=logged_in_headers)
assert response.status_code == 200, response.text
returned_ids = {message["id"] for message in response.json()}
assert str(cross_user_messages["owned_message"].id) in returned_ids
assert str(cross_user_messages["foreign_message"].id) not in returned_ids
other_response = await client.get("api/v1/monitor/messages", headers=other_logged_in_headers)
other_response = await client.get("v1/monitor/messages", headers=other_logged_in_headers)
assert other_response.status_code == 200, other_response.text
other_returned_ids = {message["id"] for message in other_response.json()}
assert str(cross_user_messages["foreign_message"].id) in other_returned_ids
@@ -187,7 +187,7 @@ async def test_update_message(client: AsyncClient, logged_in_headers, created_me
message_id = created_message.id
message_update = MessageUpdate(text="Updated content")
response = await client.put(
f"api/v1/monitor/messages/{message_id}", json=message_update.model_dump(), headers=logged_in_headers
f"v1/monitor/messages/{message_id}", json=message_update.model_dump(), headers=logged_in_headers
)
assert response.status_code == 200, response.text
updated_message = MessageRead(**response.json())
@@ -199,7 +199,7 @@ async def test_update_message_not_found(client: AsyncClient, logged_in_headers):
non_existent_id = UUID("00000000-0000-0000-0000-000000000000")
message_update = MessageUpdate(text="Updated content")
response = await client.put(
f"api/v1/monitor/messages/{non_existent_id}", json=message_update.model_dump(), headers=logged_in_headers
f"v1/monitor/messages/{non_existent_id}", json=message_update.model_dump(), headers=logged_in_headers
)
assert response.status_code == 404, response.text
assert response.json()["detail"] == "Message not found"
@@ -213,17 +213,17 @@ async def test_delete_messages_cannot_delete_other_users_messages(
response = await client.request(
"DELETE",
"api/v1/monitor/messages",
"v1/monitor/messages",
json=[foreign_message_id],
headers=logged_in_headers,
)
assert response.status_code == 204, response.text
own_view = await client.get("api/v1/monitor/messages", headers=logged_in_headers)
own_view = await client.get("v1/monitor/messages", headers=logged_in_headers)
assert own_view.status_code == 200, own_view.text
assert str(cross_user_messages["owned_message"].id) in {message["id"] for message in own_view.json()}
other_view = await client.get("api/v1/monitor/messages", headers=other_logged_in_headers)
other_view = await client.get("v1/monitor/messages", headers=other_logged_in_headers)
assert other_view.status_code == 200, other_view.text
assert foreign_message_id in {message["id"] for message in other_view.json()}
@@ -234,14 +234,14 @@ async def test_update_message_cannot_update_other_users_message(
):
foreign_message_id = cross_user_messages["foreign_message"].id
response = await client.put(
f"api/v1/monitor/messages/{foreign_message_id}",
f"v1/monitor/messages/{foreign_message_id}",
json=MessageUpdate(text="Hijacked").model_dump(),
headers=logged_in_headers,
)
assert response.status_code == 404, response.text
assert response.json()["detail"] == "Message not found"
other_view = await client.get("api/v1/monitor/messages", headers=other_logged_in_headers)
other_view = await client.get("v1/monitor/messages", headers=other_logged_in_headers)
assert other_view.status_code == 200, other_view.text
foreign_message = next(message for message in other_view.json() if message["id"] == str(foreign_message_id))
assert foreign_message["text"] == "Foreign message"
@@ -250,12 +250,12 @@ async def test_update_message_cannot_update_other_users_message(
@pytest.mark.api_key_required
async def test_delete_messages_session(client: AsyncClient, created_messages, logged_in_headers):
session_id = "session_id2"
response = await client.delete(f"api/v1/monitor/messages/session/{session_id}", headers=logged_in_headers)
response = await client.delete(f"v1/monitor/messages/session/{session_id}", headers=logged_in_headers)
assert response.status_code == 204
assert response.reason_phrase == "No Content"
assert len(created_messages) == 3
response = await client.get("api/v1/monitor/messages", headers=logged_in_headers)
response = await client.get("v1/monitor/messages", headers=logged_in_headers)
assert response.status_code == 200
assert len(response.json()) == 0
@@ -265,17 +265,17 @@ async def test_delete_messages_session_cannot_delete_other_users_messages(
client: AsyncClient, logged_in_headers, cross_user_messages, other_logged_in_headers
):
response = await client.delete(
f"api/v1/monitor/messages/session/{cross_user_messages['foreign_session_id']}",
f"v1/monitor/messages/session/{cross_user_messages['foreign_session_id']}",
headers=logged_in_headers,
)
assert response.status_code == 204, response.text
own_view = await client.get("api/v1/monitor/messages", headers=logged_in_headers)
own_view = await client.get("v1/monitor/messages", headers=logged_in_headers)
assert own_view.status_code == 200, own_view.text
assert str(cross_user_messages["owned_message"].id) in {message["id"] for message in own_view.json()}
other_view = await client.get(
"api/v1/monitor/messages",
"v1/monitor/messages",
headers=other_logged_in_headers,
params={"session_id": cross_user_messages["foreign_session_id"]},
)
@@ -291,7 +291,7 @@ async def test_successfully_update_session_id(client, logged_in_headers, created
new_session_id = "new_session_id"
response = await client.patch(
f"api/v1/monitor/messages/session/{old_session_id}",
f"v1/monitor/messages/session/{old_session_id}",
params={"new_session_id": new_session_id},
headers=logged_in_headers,
)
@@ -303,7 +303,7 @@ async def test_successfully_update_session_id(client, logged_in_headers, created
assert message["session_id"] == new_session_id
response = await client.get(
"api/v1/monitor/messages", headers=logged_in_headers, params={"session_id": new_session_id}
"v1/monitor/messages", headers=logged_in_headers, params={"session_id": new_session_id}
)
assert response.status_code == 200
assert len(response.json()) == len(created_messages)
@@ -327,14 +327,14 @@ async def test_update_session_id_cannot_modify_other_users_messages(
client: AsyncClient, logged_in_headers, cross_user_messages, other_logged_in_headers
):
response = await client.patch(
f"api/v1/monitor/messages/session/{cross_user_messages['foreign_session_id']}",
f"v1/monitor/messages/session/{cross_user_messages['foreign_session_id']}",
params={"new_session_id": "hijacked-session"},
headers=logged_in_headers,
)
assert response.status_code == 404, response.text
assert response.json()["detail"] == "No messages found with the given session ID"
other_view = await client.get("api/v1/monitor/messages", headers=other_logged_in_headers)
other_view = await client.get("v1/monitor/messages", headers=other_logged_in_headers)
assert other_view.status_code == 200, other_view.text
foreign_message = next(
message for message in other_view.json() if message["id"] == str(cross_user_messages["foreign_message"].id)
@@ -369,7 +369,7 @@ async def test_get_messages_with_url_encoded_datetime_session_id(
# Test with URL-encoded session ID
response = await client.get(
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
"v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
@@ -394,7 +394,7 @@ async def test_get_messages_with_non_encoded_datetime_session_id(
# Test with non-encoded session ID (should still work due to unquote being safe for non-encoded strings)
response = await client.get(
"api/v1/monitor/messages", params={"session_id": datetime_session_id}, headers=logged_in_headers
"v1/monitor/messages", params={"session_id": datetime_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
@@ -430,7 +430,7 @@ async def test_get_messages_with_various_encoded_characters(client: AsyncClient,
# Test with URL-encoded session ID
response = await client.get(
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
"v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
@@ -447,7 +447,7 @@ async def test_get_messages_empty_result_with_encoded_nonexistent_session(client
encoded_session_id = quote(nonexistent_session_id)
response = await client.get(
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
"v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
@@ -468,7 +468,7 @@ async def test_delete_messages_sessions_bulk(
session_ids = ["bulk_session_a", "bulk_session_b"]
response = await client.request(
"DELETE",
"api/v1/monitor/messages/sessions",
"v1/monitor/messages/sessions",
json=session_ids,
headers=logged_in_headers,
)
@@ -479,13 +479,13 @@ async def test_delete_messages_sessions_bulk(
# Verify that messages for the deleted sessions are gone
for sid in session_ids:
response = await client.get("api/v1/monitor/messages", params={"session_id": sid}, headers=logged_in_headers)
response = await client.get("v1/monitor/messages", params={"session_id": sid}, headers=logged_in_headers)
assert response.status_code == 200
assert response.json() == [], f"Expected no messages for session {sid!r}"
# Verify that messages for the untouched session are still present
response = await client.get(
"api/v1/monitor/messages", params={"session_id": "bulk_session_c"}, headers=logged_in_headers
"v1/monitor/messages", params={"session_id": "bulk_session_c"}, headers=logged_in_headers
)
assert response.status_code == 200
assert len(response.json()) == 1
@@ -501,7 +501,7 @@ async def test_delete_messages_sessions_all(
session_ids = ["bulk_session_a", "bulk_session_b", "bulk_session_c"]
response = await client.request(
"DELETE",
"api/v1/monitor/messages/sessions",
"v1/monitor/messages/sessions",
json=session_ids,
headers=logged_in_headers,
)
@@ -510,7 +510,7 @@ async def test_delete_messages_sessions_all(
assert data["deleted_count"] == 3
# All messages should be gone
response = await client.get("api/v1/monitor/messages", headers=logged_in_headers)
response = await client.get("v1/monitor/messages", headers=logged_in_headers)
assert response.status_code == 200
assert response.json() == []
@@ -520,7 +520,7 @@ async def test_delete_messages_sessions_empty_list(client: AsyncClient, logged_i
"""Bulk-delete with an empty list should succeed without error."""
response = await client.request(
"DELETE",
"api/v1/monitor/messages/sessions",
"v1/monitor/messages/sessions",
json=[],
headers=logged_in_headers,
)
@@ -535,7 +535,7 @@ async def test_delete_messages_sessions_nonexistent(client: AsyncClient, logged_
"""Bulk-delete with session IDs that don't exist should succeed (no-op) and return 0 deleted."""
response = await client.request(
"DELETE",
"api/v1/monitor/messages/sessions",
"v1/monitor/messages/sessions",
json=["nonexistent_session_1", "nonexistent_session_2"],
headers=logged_in_headers,
)
@@ -555,7 +555,7 @@ async def test_delete_messages_sessions_partial_match(
session_ids = ["bulk_session_a", "does_not_exist"]
response = await client.request(
"DELETE",
"api/v1/monitor/messages/sessions",
"v1/monitor/messages/sessions",
json=session_ids,
headers=logged_in_headers,
)
@@ -572,7 +572,7 @@ async def test_delete_messages_sessions_exceeds_limit(client: AsyncClient, logge
session_ids = [f"session_{i}" for i in range(501)]
response = await client.request(
"DELETE",
"api/v1/monitor/messages/sessions",
"v1/monitor/messages/sessions",
json=session_ids,
headers=logged_in_headers,
)
+3 -3
View File
@@ -9,7 +9,7 @@ def _flatten_models(result_json):
@pytest.mark.asyncio
async def test_models_endpoint_default(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/models", headers=logged_in_headers)
response = await client.get("v1/models", headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
providers = {entry["provider"] for entry in data}
@@ -23,7 +23,7 @@ async def test_models_endpoint_default(client: AsyncClient, logged_in_headers):
@pytest.mark.asyncio
async def test_models_endpoint_filter_provider(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/models", params={"provider": "Anthropic"}, headers=logged_in_headers)
response = await client.get("v1/models", params={"provider": "Anthropic"}, headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
assert len(data) == 1
@@ -32,7 +32,7 @@ async def test_models_endpoint_filter_provider(client: AsyncClient, logged_in_he
@pytest.mark.asyncio
async def test_models_endpoint_filter_model_type(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/models", params={"model_type": "embeddings"}, headers=logged_in_headers)
response = await client.get("v1/models", params={"model_type": "embeddings"}, headers=logged_in_headers)
assert response.status_code == 200
data = response.json()
models = list(_flatten_models(data))
@@ -16,7 +16,7 @@ async def test_upload_file_rejects_directory_traversal(client: AsyncClient, logg
files = {"file": (malicious_filename, b"malicious content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -31,7 +31,7 @@ async def test_upload_file_rejects_backslash_traversal(client: AsyncClient, logg
files = {"file": (malicious_filename, b"malicious content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -46,7 +46,7 @@ async def test_upload_file_rejects_absolute_path(client: AsyncClient, logged_in_
files = {"file": (malicious_filename, b"malicious content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -66,7 +66,7 @@ async def test_upload_file_rejects_complex_traversal(client: AsyncClient, logged
for filename in malicious_filenames:
files = {"file": (filename, b"malicious content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -87,7 +87,7 @@ async def test_upload_file_accepts_valid_filename(client: AsyncClient, logged_in
for filename in valid_filenames:
files = {"file": (filename, b"legitimate content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -102,7 +102,7 @@ async def test_upload_file_rejects_path_with_slashes(client: AsyncClient, logged
# Filenames with slashes should be rejected to prevent path traversal
files = {"file": ("subdir/nested/file.txt", b"content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -116,7 +116,7 @@ async def test_upload_file_rejects_empty_filename(client: AsyncClient, logged_in
"""Test that empty filename is rejected."""
files = {"file": ("", b"content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -133,7 +133,7 @@ async def test_upload_and_download_legitimate_file(client: AsyncClient, logged_i
# Upload
files = {"file": (filename, content, "application/pdf")}
upload_response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -146,7 +146,7 @@ async def test_upload_and_download_legitimate_file(client: AsyncClient, logged_i
# Download
file_id = data["id"]
download_response = await client.get(
f"api/v2/files/{file_id}",
f"v1/files/{file_id}",
headers=logged_in_headers,
)
@@ -165,7 +165,7 @@ async def test_upload_file_no_path_escape_via_null_bytes(client: AsyncClient, lo
for filename in malicious_filenames:
files = {"file": (filename, b"malicious", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -195,7 +195,7 @@ async def test_storage_layer_path_containment_check(client: AsyncClient, logged_
files = {"file": (malicious_filename, b"you got pwned", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -225,7 +225,7 @@ async def test_upload_file_with_unicode_filename(client: AsyncClient, logged_in_
for filename in unicode_filenames:
files = {"file": (filename, b"content", "text/plain")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -238,7 +238,7 @@ async def test_upload_file_preserves_extension(client: AsyncClient, logged_in_he
"""Test that file extensions are preserved correctly."""
files = {"file": ("document.pdf", b"PDF content", "application/pdf")}
response = await client.post(
"api/v2/files/",
"v1/files/",
files=files,
headers=logged_in_headers,
)
@@ -51,7 +51,7 @@ async def messages_with_flow_ids(session): # noqa: ARG001
@pytest.mark.api_key_required
async def test_get_sessions_all(client: AsyncClient, logged_in_headers, messages_with_flow_ids):
"""Test getting all sessions without any filter."""
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
response = await client.get("v1/monitor/messages/sessions", headers=logged_in_headers)
assert response.status_code == 200, response.text
sessions = response.json()
@@ -71,7 +71,7 @@ async def test_get_sessions_with_flow_id_filter(client: AsyncClient, logged_in_h
flow_id_1 = messages_with_flow_ids["flow_id_1"]
response = await client.get(
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers
"v1/monitor/messages/sessions", params={"flow_id": str(flow_id_1)}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
@@ -91,7 +91,7 @@ async def test_get_sessions_with_different_flow_id(client: AsyncClient, logged_i
flow_id_2 = messages_with_flow_ids["flow_id_2"]
response = await client.get(
"api/v1/monitor/messages/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers
"v1/monitor/messages/sessions", params={"flow_id": str(flow_id_2)}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
@@ -111,7 +111,7 @@ async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logge
non_existent_flow_id = uuid4()
response = await client.get(
"api/v1/monitor/messages/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers
"v1/monitor/messages/sessions", params={"flow_id": str(non_existent_flow_id)}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
@@ -123,7 +123,7 @@ async def test_get_sessions_with_non_existent_flow_id(client: AsyncClient, logge
@pytest.mark.api_key_required
async def test_get_sessions_empty_database(client: AsyncClient, logged_in_headers):
"""Test getting sessions when no messages exist in database."""
response = await client.get("api/v1/monitor/messages/sessions", headers=logged_in_headers)
response = await client.get("v1/monitor/messages/sessions", headers=logged_in_headers)
assert response.status_code == 200, response.text
sessions = response.json()
@@ -135,7 +135,7 @@ async def test_get_sessions_empty_database(client: AsyncClient, logged_in_header
async def test_get_sessions_invalid_flow_id_format(client: AsyncClient, logged_in_headers):
"""Test getting sessions with invalid flow_id format returns 422."""
response = await client.get(
"api/v1/monitor/messages/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers
"v1/monitor/messages/sessions", params={"flow_id": "invalid-uuid"}, headers=logged_in_headers
)
assert response.status_code == 422, response.text
+16 -16
View File
@@ -45,7 +45,7 @@ async def super_user_headers(
else auth_settings.SUPERUSER_PASSWORD
),
}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
tokens = response.json()
a_token = tokens["access_token"]
@@ -93,7 +93,7 @@ async def test_user_waiting_for_approval(client):
await session.commit()
login_data = {"username": "waitingforapproval", "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 400
assert response.json()["detail"] == "Waiting for approval"
@@ -110,7 +110,7 @@ async def test_user_waiting_for_approval(client):
@pytest.mark.api_key_required
async def test_deactivated_user_cannot_login(client: AsyncClient, deactivated_user):
login_data = {"username": deactivated_user.username, "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 401, response.json()
assert response.json()["detail"] == "Inactive user", response.text
@@ -118,7 +118,7 @@ async def test_deactivated_user_cannot_login(client: AsyncClient, deactivated_us
@pytest.mark.usefixtures("deactivated_user")
async def test_deactivated_user_cannot_access(client: AsyncClient, logged_in_headers):
# Assuming the headers for deactivated_user
response = await client.get("api/v1/users/", headers=logged_in_headers)
response = await client.get("v1/users/", headers=logged_in_headers)
assert response.status_code == 403, response.status_code
assert response.json()["detail"] == "The user doesn't have enough privileges", response.text
@@ -132,7 +132,7 @@ async def test_data_consistency_after_update(client: AsyncClient, active_user, l
assert response.status_code == 200, response.json()
# Fetch the updated user from the database
response = await client.get("api/v1/users/whoami", headers=logged_in_headers)
response = await client.get("v1/users/whoami", headers=logged_in_headers)
assert response.status_code == 401, response.json()
assert response.json()["detail"] == "User not found or is inactive."
@@ -144,7 +144,7 @@ async def test_data_consistency_after_delete(client: AsyncClient, test_user, sup
assert response.status_code == 200, response.json()
# Attempt to fetch the deleted user from the database
response = await client.get("api/v1/users/", headers=super_user_headers)
response = await client.get("v1/users/", headers=super_user_headers)
assert response.status_code == 200
assert all(user["id"] != user_id for user in response.json()["users"])
@@ -163,7 +163,7 @@ async def test_inactive_user(client: AsyncClient):
await session.commit()
login_data = {"username": "inactiveuser", "password": "testpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 401
assert response.json()["detail"] == "Inactive user"
@@ -175,14 +175,14 @@ def test_add_user(test_user):
@pytest.mark.api_key_required
async def test_read_all_users(client: AsyncClient, super_user_headers):
response = await client.get("api/v1/users/", headers=super_user_headers)
response = await client.get("v1/users/", headers=super_user_headers)
assert response.status_code == 200, response.json()
assert isinstance(response.json()["users"], list)
@pytest.mark.api_key_required
async def test_normal_user_cant_read_all_users(client: AsyncClient, logged_in_headers):
response = await client.get("api/v1/users/", headers=logged_in_headers)
response = await client.get("v1/users/", headers=logged_in_headers)
assert response.status_code == 403, response.json()
assert response.json() == {"detail": "The user doesn't have enough privileges"}
@@ -219,7 +219,7 @@ async def test_patch_reset_password(client: AsyncClient, active_user, logged_in_
assert response.status_code == 200, response.json()
# Now we need to test if the new password works
login_data = {"username": active_user.username, "password": "newpassword"}
response = await client.post("api/v1/login", data=login_data)
response = await client.post("v1/login", data=login_data)
assert response.status_code == 200
@@ -358,7 +358,7 @@ async def test_user_can_update_profile_picture(client: AsyncClient, active_user,
assert response.status_code == 200, f"Failed to update profile picture: {response.json()}"
# Verify the profile image was updated
response = await client.get("api/v1/users/whoami", headers=logged_in_headers)
response = await client.get("v1/users/whoami", headers=logged_in_headers)
assert response.status_code == 200
user_data = response.json()
assert user_data["profile_image"] == profile_image
@@ -377,7 +377,7 @@ async def test_user_profile_picture_persists(client: AsyncClient, active_user, l
# Check it persists in multiple requests
for _ in range(3):
response = await client.get("api/v1/users/whoami", headers=logged_in_headers)
response = await client.get("v1/users/whoami", headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["profile_image"] == profile_image
@@ -400,7 +400,7 @@ async def test_user_can_change_profile_picture_multiple_times(client: AsyncClien
assert response.status_code == 200
# Verify the update
response = await client.get("api/v1/users/whoami", headers=logged_in_headers)
response = await client.get("v1/users/whoami", headers=logged_in_headers)
assert response.status_code == 200
assert response.json()["profile_image"] == profile_image
@@ -408,7 +408,7 @@ async def test_user_can_change_profile_picture_multiple_times(client: AsyncClien
@pytest.mark.api_key_required
async def test_profile_pictures_endpoint_returns_files(client: AsyncClient, logged_in_headers):
"""Test that the profile pictures list endpoint returns files after app startup."""
response = await client.get("api/v1/files/profile_pictures/list", headers=logged_in_headers)
response = await client.get("v1/files/profile_pictures/list", headers=logged_in_headers)
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.json()}"
@@ -426,7 +426,7 @@ async def test_profile_pictures_endpoint_returns_files(client: AsyncClient, logg
async def test_profile_picture_image_can_be_accessed(client: AsyncClient, logged_in_headers):
"""Test that profile picture images can be accessed/downloaded."""
# First get the list of available profile pictures
response = await client.get("api/v1/files/profile_pictures/list", headers=logged_in_headers)
response = await client.get("v1/files/profile_pictures/list", headers=logged_in_headers)
assert response.status_code == 200
files = response.json()["files"]
@@ -436,6 +436,6 @@ async def test_profile_picture_image_can_be_accessed(client: AsyncClient, logged
first_file = files[0]
folder, filename = first_file.split("/", 1)
response = await client.get(f"api/v1/files/profile_pictures/{folder}/{filename}", headers=logged_in_headers)
response = await client.get(f"v1/files/profile_pictures/{folder}/{filename}", headers=logged_in_headers)
assert response.status_code == 200, f"Failed to access profile picture: {first_file}"
assert len(response.content) > 0, "Profile picture should have content"
+28 -28
View File
@@ -20,7 +20,7 @@ def _check_openai_api_key_in_environment_variables():
async def test_webhook_endpoint_returns_202_accepted(client, added_webhook_test, created_api_key):
"""Test that webhook endpoint returns 202 Accepted on valid request."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
payload = {"test_key": "test_value"}
response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=payload)
@@ -33,7 +33,7 @@ async def test_webhook_endpoint_returns_202_accepted(client, added_webhook_test,
async def test_webhook_endpoint_by_flow_id(client, added_webhook_test, created_api_key):
"""Test that webhook can be accessed by flow ID."""
flow_id = added_webhook_test["id"]
endpoint = f"api/v1/webhook/{flow_id}"
endpoint = f"v1/webhook/{flow_id}"
payload = {"data": "test"}
response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=payload)
@@ -44,7 +44,7 @@ async def test_webhook_endpoint_by_flow_id(client, added_webhook_test, created_a
async def test_webhook_with_json_payload(client, added_webhook_test, created_api_key):
"""Test webhook with various JSON payload types."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
# Test with nested JSON
payload = {"nested": {"key": "value", "array": [1, 2, 3]}}
@@ -69,7 +69,7 @@ async def test_webhook_endpoint_requires_api_key_when_auto_login_false(client, a
settings_service.auth_settings.WEBHOOK_AUTH_ENABLE = True
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
payload = {"path": "/tmp/test_file.txt"} # noqa: S108
@@ -84,7 +84,7 @@ async def test_webhook_endpoint_requires_api_key_when_auto_login_false(client, a
async def test_webhook_endpoint_with_valid_api_key(client, added_webhook_test, created_api_key):
"""Test that webhook works when valid API key is provided."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
# Create a temporary file
async with aiofiles.tempfile.TemporaryDirectory() as tmp:
@@ -117,7 +117,7 @@ async def test_webhook_endpoint_unauthorized_user_flow(client, added_webhook_tes
# This test would need a different user's API key to test authorization
# For now, we'll use an invalid API key to simulate this
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
payload = {"path": "/tmp/test_file.txt"} # noqa: S108
@@ -132,7 +132,7 @@ async def test_webhook_endpoint_unauthorized_user_flow(client, added_webhook_tes
async def test_webhook_flow_on_run_endpoint(client, added_webhook_test, created_api_key):
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/run/{endpoint_name}?stream=false"
endpoint = f"v1/run/{endpoint_name}?stream=false"
# Just test that "Random Payload" returns 202
# returns 202
payload = {
@@ -154,7 +154,7 @@ async def test_webhook_with_auto_login_enabled(client, added_webhook_test):
settings_service.auth_settings.WEBHOOK_AUTH_ENABLE = False
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
payload = {"path": "/tmp/test_auto_login.txt"} # noqa: S108
@@ -185,7 +185,7 @@ async def test_webhook_rejects_unauthenticated_request_by_default(client, added_
# Confirm the runtime default matches the secure-by-default value before exercising it.
assert settings_service.auth_settings.WEBHOOK_AUTH_ENABLE is True
endpoint = f"api/v1/webhook/{added_webhook_test['endpoint_name']}"
endpoint = f"v1/webhook/{added_webhook_test['endpoint_name']}"
response = await client.post(endpoint, json={"test": "unauthenticated_trigger"})
assert response.status_code == 403
@@ -206,7 +206,7 @@ async def test_webhook_with_random_payload_requires_auth(client, added_webhook_t
settings_service.auth_settings.WEBHOOK_AUTH_ENABLE = True
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
# Should fail without API key
response = await client.post(endpoint, json="Random Payload")
@@ -230,7 +230,7 @@ async def test_webhook_with_random_payload_requires_auth(client, added_webhook_t
async def test_webhook_not_found_invalid_endpoint(client, created_api_key):
"""Test that webhook returns 404 for non-existent endpoint."""
endpoint = "api/v1/webhook/non-existent-endpoint-12345"
endpoint = "v1/webhook/non-existent-endpoint-12345"
payload = {"test": "data"}
response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=payload)
@@ -239,7 +239,7 @@ async def test_webhook_not_found_invalid_endpoint(client, created_api_key):
# async def test_webhook_not_found_invalid_flow_id(client, created_api_key):
# """Test that webhook returns 404 for invalid flow ID."""
# endpoint = "api/v1/webhook/00000000-0000-0000-0000-000000000000"
# endpoint = "v1/webhook/00000000-0000-0000-0000-000000000000"
# payload = {"test": "data"}
# response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=payload)
@@ -267,7 +267,7 @@ async def test_webhook_invalid_api_key(client, added_webhook_test):
with patch("flow.api.v1.endpoints.get_auth_service", return_value=mock_auth_service):
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
payload = {"test": "data"}
response = await client.post(endpoint, headers={"x-api-key": "invalid-api-key"}, json=payload)
@@ -298,7 +298,7 @@ async def test_webhook_missing_api_key_when_required(client, added_webhook_test)
with patch("flow.api.v1.endpoints.get_auth_service", return_value=mock_auth_service):
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
payload = {"test": "data"}
response = await client.post(endpoint, json=payload)
@@ -314,7 +314,7 @@ async def test_webhook_missing_api_key_when_required(client, added_webhook_test)
async def test_webhook_with_empty_payload(client, added_webhook_test, created_api_key):
"""Test webhook with empty JSON payload."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json={})
assert response.status_code == 202
@@ -323,7 +323,7 @@ async def test_webhook_with_empty_payload(client, added_webhook_test, created_ap
async def test_webhook_with_string_payload(client, added_webhook_test, created_api_key):
"""Test webhook with string payload instead of JSON object."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json="plain string")
assert response.status_code == 202
@@ -332,7 +332,7 @@ async def test_webhook_with_string_payload(client, added_webhook_test, created_a
async def test_webhook_with_null_payload_returns_bad_request(client, added_webhook_test, created_api_key):
"""Test webhook with null payload returns 400 Bad Request."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=None)
# Null payload is not valid JSON body, returns 400
@@ -342,7 +342,7 @@ async def test_webhook_with_null_payload_returns_bad_request(client, added_webho
async def test_webhook_with_large_payload(client, added_webhook_test, created_api_key):
"""Test webhook with large payload."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
# Create a large payload
large_payload = {"data": "x" * 10000, "items": list(range(1000))}
@@ -353,7 +353,7 @@ async def test_webhook_with_large_payload(client, added_webhook_test, created_ap
async def test_webhook_with_special_characters_in_payload(client, added_webhook_test, created_api_key):
"""Test webhook with special characters in payload."""
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
payload = {
"unicode": "こんにちは世界 🌍",
@@ -374,7 +374,7 @@ async def test_webhook_creates_vertex_builds(client, added_webhook_test, created
"""Test that webhook execution creates vertex builds in the database."""
flow_id = added_webhook_test["id"]
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
# Execute the webhook
payload = {"test": "vertex_build_test"}
@@ -385,7 +385,7 @@ async def test_webhook_creates_vertex_builds(client, added_webhook_test, created
await asyncio.sleep(2)
# Check vertex builds were created
builds_endpoint = f"api/v1/monitor/builds?flow_id={flow_id}"
builds_endpoint = f"v1/monitor/builds?flow_id={flow_id}"
builds_response = await client.get(builds_endpoint, headers={"x-api-key": created_api_key.api_key})
assert builds_response.status_code == 200
@@ -399,7 +399,7 @@ async def test_webhook_vertex_builds_contain_expected_data(client, added_webhook
"""Test that vertex builds contain expected structure and data."""
flow_id = added_webhook_test["id"]
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
# Execute the webhook
payload = {"verify": "structure"}
@@ -410,7 +410,7 @@ async def test_webhook_vertex_builds_contain_expected_data(client, added_webhook
await asyncio.sleep(2)
# Check vertex builds
builds_endpoint = f"api/v1/monitor/builds?flow_id={flow_id}"
builds_endpoint = f"v1/monitor/builds?flow_id={flow_id}"
builds_response = await client.get(builds_endpoint, headers={"x-api-key": created_api_key.api_key})
assert builds_response.status_code == 200
@@ -431,7 +431,7 @@ async def test_webhook_multiple_executions_create_multiple_builds(client, added_
"""Test that multiple webhook executions create multiple vertex builds."""
flow_id = added_webhook_test["id"]
endpoint_name = added_webhook_test["endpoint_name"]
endpoint = f"api/v1/webhook/{endpoint_name}"
endpoint = f"v1/webhook/{endpoint_name}"
# Execute webhook multiple times
for i in range(3):
@@ -443,7 +443,7 @@ async def test_webhook_multiple_executions_create_multiple_builds(client, added_
await asyncio.sleep(5)
# Check vertex builds
builds_endpoint = f"api/v1/monitor/builds?flow_id={flow_id}"
builds_endpoint = f"v1/monitor/builds?flow_id={flow_id}"
builds_response = await client.get(builds_endpoint, headers={"x-api-key": created_api_key.api_key})
assert builds_response.status_code == 200
@@ -465,13 +465,13 @@ async def test_vertex_builds_endpoint_returns_empty_for_new_flow(client, logged_
graph_dict = graph.dump(name="Empty Test Flow")
flow = FlowCreate(**graph_dict)
response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
response = await client.post("v1/flows/", json=flow.model_dump(), headers=logged_in_headers)
assert response.status_code == 201
flow_id = response.json()["id"]
try:
# Check vertex builds - should be empty
builds_endpoint = f"api/v1/monitor/builds?flow_id={flow_id}"
builds_endpoint = f"v1/monitor/builds?flow_id={flow_id}"
builds_response = await client.get(builds_endpoint, headers=logged_in_headers)
assert builds_response.status_code == 200
@@ -479,7 +479,7 @@ async def test_vertex_builds_endpoint_returns_empty_for_new_flow(client, logged_
assert builds_data["vertex_builds"] == {}
finally:
# Cleanup
await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers)
await client.delete(f"v1/flows/{flow_id}", headers=logged_in_headers)
# =============================================================================
@@ -1,7 +1,7 @@
"""Regression test for webhook SSE real-time event delivery.
Bug: After POST /api/v1/webhook/{flow_id}, the UI's Inspect Output panel never shows
the Webhook build because the SSE endpoint `/api/v1/webhook-events/{flow_id}` was
Bug: After POST /v1/webhook/{flow_id}, the UI's Inspect Output panel never shows
the Webhook build because the SSE endpoint `/v1/webhook-events/{flow_id}` was
returning 403 to the frontend subscriber. As a result the frontend `flowPool` stayed
empty and the Inspect Output panel showed "Please build the component first".
@@ -52,7 +52,7 @@ async def test_should_not_return_403_when_accessing_webhook_sse_endpoint_with_va
):
"""Direct reproduction: SSE must accept `x-api-key` query param."""
flow_id = added_webhook_test["id"]
sse_url = f"api/v1/webhook-events/{flow_id}?x-api-key={created_api_key.api_key}"
sse_url = f"v1/webhook-events/{flow_id}?x-api-key={created_api_key.api_key}"
await _assert_sse_not_forbidden(client, sse_url)
@@ -69,7 +69,7 @@ async def test_should_not_return_403_when_accessing_webhook_sse_endpoint_with_va
the real end-user scenario.
"""
login_response = await client.post(
"api/v1/login",
"v1/login",
data={"username": active_user.username, "password": "testpassword"},
)
assert login_response.status_code == 200
@@ -78,7 +78,7 @@ async def test_should_not_return_403_when_accessing_webhook_sse_endpoint_with_va
assert "access_token_lf" in client.cookies
flow_id = added_webhook_test["id"]
sse_url = f"api/v1/webhook-events/{flow_id}"
sse_url = f"v1/webhook-events/{flow_id}"
await _assert_sse_not_forbidden(client, sse_url)
@@ -103,7 +103,7 @@ async def test_should_not_return_403_on_sse_endpoint_when_webhook_auth_enable_is
settings_service.auth_settings.WEBHOOK_AUTH_ENABLE = True
flow_id = added_webhook_test["id"]
sse_url = f"api/v1/webhook-events/{flow_id}?x-api-key={created_api_key.api_key}"
sse_url = f"v1/webhook-events/{flow_id}?x-api-key={created_api_key.api_key}"
await _assert_sse_not_forbidden(client, sse_url)
finally:
@@ -295,19 +295,19 @@ class TestClientHeaders:
class TestClientUrl:
def test_basic_path(self):
client = FlowClient(server_url="http://localhost:7860")
assert client._url("/flows/") == "http://localhost:7860/api/v1/flows/"
assert client._url("/flows/") == "http://localhost:7860/v1/flows/"
def test_trailing_slash_stripped(self):
client = FlowClient(server_url="http://localhost:7860/")
assert client._url("/flows/") == "http://localhost:7860/api/v1/flows/"
assert client._url("/flows/") == "http://localhost:7860/v1/flows/"
def test_nested_path(self):
client = FlowClient(server_url="http://host:8000")
assert client._url("/flows/abc-123") == "http://host:8000/api/v1/flows/abc-123"
assert client._url("/flows/abc-123") == "http://host:8000/v1/flows/abc-123"
def test_default_server_url(self):
client = FlowClient()
assert client._url("/test") == f"{client.server_url}/api/v1/test"
assert client._url("/test") == f"{client.server_url}/v1/test"
class TestClientInit:
@@ -471,7 +471,7 @@ class TestValidateFlowExecution:
assert "Flow execution validation failed: Build error" in errors[0]
# Verify cleanup was called
mock_client.delete.assert_called_once_with("api/v1/flows/flow123", headers=headers, timeout=10)
mock_client.delete.assert_called_once_with("v1/flows/flow123", headers=headers, timeout=10)
class TestValidateEventStream:
@@ -113,7 +113,7 @@ test(
const searchResponse = page.waitForResponse(
(response) =>
response.url().includes("/api/v1/users") && response.status() === 200,
response.url().includes("/v1/users") && response.status() === 200,
);
await page.getByPlaceholder("Username").last().fill(randomName);
await searchResponse;
@@ -50,7 +50,7 @@ test.describe("Bulk Delete Sessions", () => {
async function setupApiInterceptor(page: Page) {
const apiCalls: string[] = [];
await page.route("**/api/v1/monitor/messages/sessions*", (route) => {
await page.route("**/v1/monitor/messages/sessions*", (route) => {
const method = route.request().method();
const url = route.request().url();
apiCalls.push(`${method} ${url}`);
@@ -21,7 +21,7 @@ async function setupDeploymentMocks(
snapshotsMock: object = SNAPSHOTS_EMPTY_MOCK,
) {
// Broad catch-all registered FIRST so specific routes (registered after) take priority via LIFO
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -30,7 +30,7 @@ async function setupDeploymentMocks(
});
// Snapshots (used for duplicate tool name check on review step)
await page.route("**/api/v1/deployments/snapshots**", (route) => {
await page.route("**/v1/deployments/snapshots**", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -39,7 +39,7 @@ async function setupDeploymentMocks(
});
// Provider accounts
await page.route("**/api/v1/deployments/providers**", (route) => {
await page.route("**/v1/deployments/providers**", (route) => {
if (route.request().method() === "GET") {
route.fulfill({
status: 200,
@@ -52,7 +52,7 @@ async function setupDeploymentMocks(
});
// LLMs
await page.route("**/api/v1/deployments/llms**", (route) => {
await page.route("**/v1/deployments/llms**", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -61,7 +61,7 @@ async function setupDeploymentMocks(
});
// Deployment configs (connections)
await page.route("**/api/v1/deployments/configs**", (route) => {
await page.route("**/v1/deployments/configs**", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -70,7 +70,7 @@ async function setupDeploymentMocks(
});
// Flows list — inject the captured folderId so the component's folder filter passes
await page.route("**/api/v1/flows/**", (route) => {
await page.route("**/v1/flows/**", (route) => {
const url = route.request().url();
if (url.includes("/versions/")) {
route.fulfill({
@@ -89,7 +89,7 @@ async function setupDeploymentMocks(
});
// Global variables (used in attach-flows step)
await page.route("**/api/v1/variables**", (route) => {
await page.route("**/v1/variables**", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -109,7 +109,7 @@ async function openDeploymentStepper(
// the real myCollectionId. The step-attach-flows component filters flows by
// folder_id === myCollectionId, so mock flows must carry the same id.
const projectsResponsePromise = page.waitForResponse(
(resp) => resp.url().includes("/api/v1/projects") && resp.status() === 200,
(resp) => resp.url().includes("/v1/projects") && resp.status() === 200,
{ timeout: 30000 },
);
@@ -371,7 +371,7 @@ test(
await openDeploymentStepper(page);
// Set up POST deployments mock (after bootstrap, before deploy click)
await page.route("**/api/v1/deployments", (route) => {
await page.route("**/v1/deployments", (route) => {
if (route.request().method() === "POST") {
route.fulfill({
status: 201,
@@ -387,7 +387,7 @@ test(
// Watch for the POST request
const postRequestPromise = page.waitForRequest(
(req) =>
req.url().includes("/api/v1/deployments") &&
req.url().includes("/v1/deployments") &&
req.method() === "POST" &&
!req.url().includes("/providers") &&
!req.url().includes("/llms") &&
@@ -518,7 +518,7 @@ test(
await expect(page.getByTestId("deployment-stepper-next")).toBeDisabled();
// Override the snapshots mock to return empty (unique name) for the next check
await page.route("**/api/v1/deployments/snapshots**", (route) => {
await page.route("**/v1/deployments/snapshots**", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -14,7 +14,7 @@ import {
async function setupRoutes(page: Parameters<typeof test>[2]["page"]) {
// Register broad catch-all FIRST so specific routes (registered after) take priority via LIFO
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -22,7 +22,7 @@ async function setupRoutes(page: Parameters<typeof test>[2]["page"]) {
});
});
await page.route("**/api/v1/deployments/configs*", (route) => {
await page.route("**/v1/deployments/configs*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -30,7 +30,7 @@ async function setupRoutes(page: Parameters<typeof test>[2]["page"]) {
});
});
await page.route("**/api/v1/deployments/llms*", (route) => {
await page.route("**/v1/deployments/llms*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -38,7 +38,7 @@ async function setupRoutes(page: Parameters<typeof test>[2]["page"]) {
});
});
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -46,7 +46,7 @@ async function setupRoutes(page: Parameters<typeof test>[2]["page"]) {
});
});
await page.route("**/api/v1/deployments/dep-1", (route) => {
await page.route("**/v1/deployments/dep-1", (route) => {
if (route.request().method() === "PATCH") {
route.fulfill({
status: 200,
@@ -62,7 +62,7 @@ async function setupRoutes(page: Parameters<typeof test>[2]["page"]) {
}
});
await page.route("**/api/v1/deployments/dep-1/flows*", (route) => {
await page.route("**/v1/deployments/dep-1/flows*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -194,7 +194,7 @@ test(
// Step: Review → click Update (final step)
const patchRequestPromise = page.waitForRequest(
(req) =>
req.url().includes("/api/v1/deployments/dep-1") &&
req.url().includes("/v1/deployments/dep-1") &&
req.method() === "PATCH",
);
@@ -230,7 +230,7 @@ test(
let patchCalled = false;
page.on("request", (req) => {
if (
req.url().includes("/api/v1/deployments/dep-1") &&
req.url().includes("/v1/deployments/dep-1") &&
req.method() === "PATCH"
) {
patchCalled = true;
@@ -256,7 +256,7 @@ async function setupRoutesWithConnections(
folderId: string,
) {
// Broad catch-all FIRST
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -264,7 +264,7 @@ async function setupRoutesWithConnections(
});
});
await page.route("**/api/v1/deployments/configs*", (route) => {
await page.route("**/v1/deployments/configs*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -272,7 +272,7 @@ async function setupRoutesWithConnections(
});
});
await page.route("**/api/v1/deployments/llms*", (route) => {
await page.route("**/v1/deployments/llms*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -280,7 +280,7 @@ async function setupRoutesWithConnections(
});
});
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -288,7 +288,7 @@ async function setupRoutesWithConnections(
});
});
await page.route("**/api/v1/deployments/dep-1", (route) => {
await page.route("**/v1/deployments/dep-1", (route) => {
if (route.request().method() === "PATCH") {
route.fulfill({
status: 200,
@@ -305,7 +305,7 @@ async function setupRoutesWithConnections(
});
// Attachments with provider_data containing app_ids
await page.route("**/api/v1/deployments/dep-1/flows*", (route) => {
await page.route("**/v1/deployments/dep-1/flows*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -314,7 +314,7 @@ async function setupRoutesWithConnections(
});
// Flows list
await page.route("**/api/v1/flows/**", (route) => {
await page.route("**/v1/flows/**", (route) => {
const url = route.request().url();
if (url.includes("/versions/")) {
route.fulfill({
@@ -333,7 +333,7 @@ async function setupRoutesWithConnections(
});
// Global variables
await page.route("**/api/v1/variables**", (route) => {
await page.route("**/v1/variables**", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -342,7 +342,7 @@ async function setupRoutesWithConnections(
});
// Env var detection
await page.route("**/api/v1/variables/detections**", (route) => {
await page.route("**/v1/variables/detections**", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -366,7 +366,7 @@ test(
// Capture folder ID from the projects API before bootstrap
const projectsResponsePromise = page.waitForResponse(
(resp) =>
resp.url().includes("/api/v1/projects") && resp.status() === 200,
resp.url().includes("/v1/projects") && resp.status() === 200,
{ timeout: 30000 },
);
@@ -427,7 +427,7 @@ test(
// Intercept the PATCH request and verify its body
const patchRequestPromise = page.waitForRequest(
(req) =>
req.url().includes("/api/v1/deployments/dep-1") &&
req.url().includes("/v1/deployments/dep-1") &&
req.method() === "PATCH",
);
@@ -464,7 +464,7 @@ test(
const projectsResponsePromise = page.waitForResponse(
(resp) =>
resp.url().includes("/api/v1/projects") && resp.status() === 200,
resp.url().includes("/v1/projects") && resp.status() === 200,
{ timeout: 30000 },
);
@@ -522,7 +522,7 @@ test(
const patchRequestPromise = page.waitForRequest(
(req) =>
req.url().includes("/api/v1/deployments/dep-1") &&
req.url().includes("/v1/deployments/dep-1") &&
req.method() === "PATCH",
);
@@ -24,7 +24,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -32,7 +32,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -55,7 +55,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -63,7 +63,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -89,7 +89,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -97,7 +97,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -134,7 +134,7 @@ test(
let postCalled = false;
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
const method = route.request().method();
if (method === "POST") {
@@ -156,7 +156,7 @@ test(
}
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -178,7 +178,7 @@ test(
const postRequest = page.waitForRequest(
(req) =>
req.url().includes("/api/v1/deployments/providers") &&
req.url().includes("/v1/deployments/providers") &&
req.method() === "POST",
);
@@ -201,7 +201,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -209,7 +209,7 @@ test(
});
});
await page.route("**/api/v1/deployments/providers/prov-1", (route) => {
await page.route("**/v1/deployments/providers/prov-1", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -217,7 +217,7 @@ test(
});
});
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
const method = route.request().method();
if (method === "GET") {
route.fulfill({
@@ -243,7 +243,7 @@ test(
const deleteRequest = page.waitForRequest(
(req) =>
req.url().includes("/api/v1/deployments/providers/prov-1") &&
req.url().includes("/v1/deployments/providers/prov-1") &&
req.method() === "DELETE",
);
@@ -264,7 +264,7 @@ test(
let deleteRequestCount = 0;
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -272,7 +272,7 @@ test(
});
});
await page.route("**/api/v1/deployments/providers/prov-1", (route) => {
await page.route("**/v1/deployments/providers/prov-1", (route) => {
deleteRequestCount++;
route.fulfill({
status: 200,
@@ -281,7 +281,7 @@ test(
});
});
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -11,7 +11,7 @@ import {
async function setupBaseRoutes(page: Page) {
// Register broad catch-all FIRST so specific routes (registered after) take priority via LIFO
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
const url = route.request().url();
// Run routes are handled per-test; fall through for those
if (url.includes("/dep-1/runs") || url.includes("/runs")) {
@@ -25,7 +25,7 @@ async function setupBaseRoutes(page: Page) {
});
});
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -73,7 +73,7 @@ test(
await setupBaseRoutes(page);
await page.route("**/api/v1/deployments/dep-1/runs/exec-1", (route) => {
await page.route("**/v1/deployments/dep-1/runs/exec-1", (route) => {
runCallCount++;
const isCompleted = runCallCount > 1;
route.fulfill({
@@ -85,7 +85,7 @@ test(
});
});
await page.route("**/api/v1/deployments/dep-1/runs", async (route) => {
await page.route("**/v1/deployments/dep-1/runs", async (route) => {
if (route.request().method() === "POST") {
const body = route.request().postDataJSON() as Record<string, unknown>;
capturedRequestBody = body;
@@ -128,7 +128,7 @@ test(
await setupBaseRoutes(page);
await page.route("**/api/v1/deployments/dep-1/runs/exec-1", (route) => {
await page.route("**/v1/deployments/dep-1/runs/exec-1", (route) => {
runCallCount++;
const isCompleted = runCallCount > 1;
route.fulfill({
@@ -140,7 +140,7 @@ test(
});
});
await page.route("**/api/v1/deployments/dep-1/runs", async (route) => {
await page.route("**/v1/deployments/dep-1/runs", async (route) => {
if (route.request().method() === "POST") {
route.fulfill({
status: 200,
@@ -180,7 +180,7 @@ test(
await setupBaseRoutes(page);
await page.route("**/api/v1/deployments/dep-1/runs/exec-1", (route) => {
await page.route("**/v1/deployments/dep-1/runs/exec-1", (route) => {
runCallCount++;
const isCompleted = runCallCount > 1;
route.fulfill({
@@ -192,7 +192,7 @@ test(
});
});
await page.route("**/api/v1/deployments/dep-1/runs", async (route) => {
await page.route("**/v1/deployments/dep-1/runs", async (route) => {
if (route.request().method() === "POST") {
route.fulfill({
status: 200,
@@ -237,7 +237,7 @@ test(
await setupBaseRoutes(page);
await page.route("**/api/v1/deployments/dep-1/runs/exec-1", (route) => {
await page.route("**/v1/deployments/dep-1/runs/exec-1", (route) => {
runCallCount++;
const isCompleted = runCallCount % 2 === 0;
route.fulfill({
@@ -249,7 +249,7 @@ test(
});
});
await page.route("**/api/v1/deployments/dep-1/runs", async (route) => {
await page.route("**/v1/deployments/dep-1/runs", async (route) => {
if (route.request().method() === "POST") {
const body = route.request().postDataJSON() as Record<string, unknown>;
capturedBodies.push(body);
@@ -312,7 +312,7 @@ test(
await setupBaseRoutes(page);
await page.route("**/api/v1/deployments/dep-1/runs/exec-1", (route) => {
await page.route("**/v1/deployments/dep-1/runs/exec-1", (route) => {
runCallCount++;
const isCompleted = runCallCount > 1;
route.fulfill({
@@ -324,7 +324,7 @@ test(
});
});
await page.route("**/api/v1/deployments/dep-1/runs", async (route) => {
await page.route("**/v1/deployments/dep-1/runs", async (route) => {
if (route.request().method() === "POST") {
route.fulfill({
status: 200,
@@ -23,7 +23,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -31,7 +31,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -57,7 +57,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -65,7 +65,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -90,7 +90,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -98,7 +98,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -125,7 +125,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -133,7 +133,7 @@ test(
});
});
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -158,7 +158,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -166,7 +166,7 @@ test(
});
});
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -193,7 +193,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -201,7 +201,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -230,7 +230,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -238,7 +238,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -272,7 +272,7 @@ test(
"Requires FLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -280,7 +280,7 @@ test(
});
});
await page.route("**/api/v1/deployments/dep-1", (route) => {
await page.route("**/v1/deployments/dep-1", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -288,7 +288,7 @@ test(
});
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -306,7 +306,7 @@ test(
const deleteRequest = page.waitForRequest(
(req) =>
req.url().includes("/api/v1/deployments/dep-1") &&
req.url().includes("/v1/deployments/dep-1") &&
req.method() === "DELETE",
);
@@ -327,7 +327,7 @@ test(
let deleteRequestCount = 0;
await page.route("**/api/v1/deployments/providers*", (route) => {
await page.route("**/v1/deployments/providers*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -335,7 +335,7 @@ test(
});
});
await page.route("**/api/v1/deployments/dep-1", (route) => {
await page.route("**/v1/deployments/dep-1", (route) => {
if (route.request().method() === "DELETE") {
deleteRequestCount++;
route.fulfill({
@@ -348,7 +348,7 @@ test(
}
});
await page.route("**/api/v1/deployments*", (route) => {
await page.route("**/v1/deployments*", (route) => {
route.fulfill({
status: 200,
contentType: "application/json",
@@ -24,7 +24,7 @@ test.describe("Flow Agent Events", () => {
// Post an event via the page's fetch (uses the page's auth cookies/headers)
await page.evaluate(async (fid: string) => {
await fetch(`/api/v1/flows/${fid}/events`, {
await fetch(`/v1/flows/${fid}/events`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -41,7 +41,7 @@ test.describe("Flow Agent Events", () => {
// Post a flow_settled event
await page.evaluate(async (fid: string) => {
await fetch(`/api/v1/flows/${fid}/events`, {
await fetch(`/v1/flows/${fid}/events`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
@@ -15,7 +15,7 @@ import { initialGPTsetup } from "../../utils/initialGPTsetup";
* Helper: mock auto-login as disabled and log in manually.
*/
async function setupAutoLoginOff(page: any) {
await page.route("**/api/v1/auto_login", (route: any) => {
await page.route("**/v1/auto_login", (route: any) => {
route.fulfill({
status: 500,
contentType: "application/json",
+1 -1
View File
@@ -69,7 +69,7 @@ export const ATTACHMENTS_MOCK = {
total: 1,
};
// Shape returned by GET /api/v1/deployments/llms — used in create stepper
// Shape returned by GET /v1/deployments/llms — used in create stepper
export const LLMS_MOCK = {
provider_data: {
models: [{ model_name: "ibm/granite-13b-chat" }],
+2 -2
View File
@@ -1,10 +1,10 @@
"""Regression tests for the SDK upload helper after the auth change.
After PR #12831, the server's ``/api/v1/upload/{flow_id}`` endpoint requires
After PR #12831, the server's ``/v1/upload/{flow_id}`` endpoint requires
authentication. The SDK helpers in :mod:`lfx.load.utils` were updated to
forward an optional ``api_key`` (or ``FLOW_API_KEY`` env var) as the
``x-api-key`` header so existing callers can pass credentials without
rewriting against the non-deprecated ``/api/v1/files/upload/{flow_id}``
rewriting against the non-deprecated ``/v1/files/upload/{flow_id}``
route.
"""
+9 -9
View File
@@ -50,7 +50,7 @@ def _client() -> FlowClient:
@respx.mock
def test_upsert_flow_create():
respx.put(f"{_BASE}/api/v1/flows/{_FLOW_ID}").mock(return_value=httpx.Response(201, json=_FLOW_PAYLOAD))
respx.put(f"{_BASE}/v1/flows/{_FLOW_ID}").mock(return_value=httpx.Response(201, json=_FLOW_PAYLOAD))
from flow_sdk.models import FlowCreate
client = _client()
@@ -67,7 +67,7 @@ def test_upsert_flow_create():
@respx.mock
def test_upsert_flow_update():
respx.put(f"{_BASE}/api/v1/flows/{_FLOW_ID}").mock(return_value=httpx.Response(200, json=_FLOW_PAYLOAD))
respx.put(f"{_BASE}/v1/flows/{_FLOW_ID}").mock(return_value=httpx.Response(200, json=_FLOW_PAYLOAD))
from flow_sdk.models import FlowCreate
client = _client()
@@ -83,7 +83,7 @@ def test_upsert_flow_update():
@respx.mock
def test_upsert_flow_not_found_raises():
respx.put(f"{_BASE}/api/v1/flows/{_FLOW_ID}").mock(
respx.put(f"{_BASE}/v1/flows/{_FLOW_ID}").mock(
return_value=httpx.Response(404, json={"detail": "Flow not found"})
)
from flow_sdk.models import FlowCreate
@@ -100,7 +100,7 @@ def test_upsert_flow_not_found_raises():
@respx.mock
def test_upsert_flow_conflict_raises():
respx.put(f"{_BASE}/api/v1/flows/{_FLOW_ID}").mock(
respx.put(f"{_BASE}/v1/flows/{_FLOW_ID}").mock(
return_value=httpx.Response(409, json={"detail": "Name must be unique"})
)
from flow_sdk.models import FlowCreate
@@ -133,9 +133,9 @@ def test_push_command_creates_flow(tmp_path: Path):
encoding="utf-8",
)
respx.put(f"{_BASE}/api/v1/flows/{_FLOW_ID}").mock(return_value=httpx.Response(201, json=_FLOW_PAYLOAD))
respx.put(f"{_BASE}/v1/flows/{_FLOW_ID}").mock(return_value=httpx.Response(201, json=_FLOW_PAYLOAD))
# projects list (empty -- no project targeting in this test)
respx.get(f"{_BASE}/api/v1/projects/").mock(return_value=httpx.Response(200, json=[]))
respx.get(f"{_BASE}/v1/projects/").mock(return_value=httpx.Response(200, json=[]))
# Write an environments config
env_file = tmp_path / "flow-environments.toml"
@@ -211,12 +211,12 @@ def test_push_command_project_dir(tmp_path: Path):
project_id = UUID("cccccccc-0000-0000-0000-000000000001")
project_payload = {"id": str(project_id), "name": "My Project", "description": None, "parent_id": None}
respx.get(f"{_BASE}/api/v1/projects/").mock(return_value=httpx.Response(200, json=[]))
respx.post(f"{_BASE}/api/v1/projects/").mock(return_value=httpx.Response(201, json=project_payload))
respx.get(f"{_BASE}/v1/projects/").mock(return_value=httpx.Response(200, json=[]))
respx.post(f"{_BASE}/v1/projects/").mock(return_value=httpx.Response(201, json=project_payload))
for fid in flow_ids:
payload = {**_FLOW_PAYLOAD, "id": str(fid)}
respx.put(f"{_BASE}/api/v1/flows/{fid}").mock(return_value=httpx.Response(201, json=payload))
respx.put(f"{_BASE}/v1/flows/{fid}").mock(return_value=httpx.Response(201, json=payload))
env_file = tmp_path / "flow-environments.toml"
env_file.write_text(
+1 -1
View File
@@ -17,7 +17,7 @@ from flow_sdk.models import RunResponse
_BASE_URL = "http://flow.test"
_FLOW_ID = "00000000-0000-0000-0000-000000000001"
_RUN_ENDPOINT = f"/api/v1/run/{_FLOW_ID}"
_RUN_ENDPOINT = f"/v1/run/{_FLOW_ID}"
def _sse_body(*events: dict[str, Any]) -> bytes:
+8 -8
View File
@@ -201,8 +201,8 @@ def test_run_response_all_text_outputs_skips_none():
@respx.mock
def test_flow_runner_calls_run_flow():
"""FlowRunner.__call__ hits POST /api/v1/run/<endpoint>."""
respx.post(f"{_BASE}/api/v1/run/my-endpoint").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
"""FlowRunner.__call__ hits POST /v1/run/<endpoint>."""
respx.post(f"{_BASE}/v1/run/my-endpoint").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
runner = FlowRunner(_sync_client())
response = runner("my-endpoint", "Hello!")
@@ -214,7 +214,7 @@ def test_flow_runner_calls_run_flow():
@respx.mock
def test_flow_runner_passes_tweaks():
"""FlowRunner forwards tweaks inside the JSON body."""
route = respx.post(f"{_BASE}/api/v1/run/ep").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
route = respx.post(f"{_BASE}/v1/run/ep").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
runner = FlowRunner(_sync_client())
runner("ep", "Q", tweaks={"MyComponent": {"param": "value"}})
@@ -228,7 +228,7 @@ def test_flow_runner_passes_tweaks():
@respx.mock
def test_flow_runner_default_input_type():
"""FlowRunner defaults to input_type='chat' and output_type='chat'."""
route = respx.post(f"{_BASE}/api/v1/run/ep").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
route = respx.post(f"{_BASE}/v1/run/ep").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
runner = FlowRunner(_sync_client())
runner("ep")
@@ -246,8 +246,8 @@ def test_flow_runner_default_input_type():
@respx.mock
async def test_async_flow_runner_calls_run_flow():
"""AsyncFlowRunner.__call__ hits POST /api/v1/run/<endpoint> asynchronously."""
respx.post(f"{_BASE}/api/v1/run/async-ep").mock(return_value=httpx.Response(200, json=_TEXT_RUN_RESPONSE))
"""AsyncFlowRunner.__call__ hits POST /v1/run/<endpoint> asynchronously."""
respx.post(f"{_BASE}/v1/run/async-ep").mock(return_value=httpx.Response(200, json=_TEXT_RUN_RESPONSE))
runner = AsyncFlowRunner(_async_client())
response = await runner("async-ep", "Hi async!")
@@ -258,7 +258,7 @@ async def test_async_flow_runner_calls_run_flow():
@respx.mock
async def test_async_flow_runner_multi_output():
"""AsyncFlowRunner returns all outputs correctly."""
respx.post(f"{_BASE}/api/v1/run/multi-ep").mock(return_value=httpx.Response(200, json=_MULTI_RUN_RESPONSE))
respx.post(f"{_BASE}/v1/run/multi-ep").mock(return_value=httpx.Response(200, json=_MULTI_RUN_RESPONSE))
runner = AsyncFlowRunner(_async_client())
response = await runner("multi-ep", "Query")
@@ -276,7 +276,7 @@ def test_flow_runner_accepts_uuid():
from uuid import UUID
flow_id = UUID("dddddddd-0000-0000-0000-000000000001")
respx.post(f"{_BASE}/api/v1/run/{flow_id}").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
respx.post(f"{_BASE}/v1/run/{flow_id}").mock(return_value=httpx.Response(200, json=_CHAT_RUN_RESPONSE))
runner = FlowRunner(_sync_client())
response = runner(flow_id, "Hello by UUID")