mirror of
https://github.com/hanzoai/flow.git
synced 2026-10-17 01:29:18 +00:00
Point flow's tests and scripts at /v1
Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
This commit is contained in:
@@ -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,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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user