diff --git a/pyproject.toml b/pyproject.toml index d180bc4c3..df6c71793 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -301,8 +301,8 @@ dev-dependencies = [ "pytest-split>=0.9.0", "pytest-flakefinder>=1.1.0", "types-markdown>=3.7.0.20240822", - "packaging>=23.2,<24.0" - + "packaging>=23.2,<24.0", + "asgi-lifespan>=2.1.0", ] @@ -328,7 +328,8 @@ log_cli = true log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)" log_cli_date_format = "%Y-%m-%d %H:%M:%S" markers = ["async_test", "api_key_required"] - +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" [tool.coverage.run] command_line = """ @@ -365,4 +366,4 @@ ignore_missing_imports = true [build-system] requires = ["hatchling"] -build-backend = "hatchling.build" \ No newline at end of file +build-backend = "hatchling.build" diff --git a/src/backend/base/langflow/utils/validate.py b/src/backend/base/langflow/utils/validate.py index 17dc41965..6798da087 100644 --- a/src/backend/base/langflow/utils/validate.py +++ b/src/backend/base/langflow/utils/validate.py @@ -124,10 +124,17 @@ def create_function(code, function_name): exec_globals = globals().copy() for node in module.body: - if isinstance(node, ast.Import): + if isinstance(node, ast.Import | ast.ImportFrom): for alias in node.names: try: - exec_globals[alias.asname or alias.name] = importlib.import_module(alias.name) + if isinstance(node, ast.ImportFrom): + module_name = node.module + exec_globals[alias.asname or alias.name] = getattr( + importlib.import_module(module_name), alias.name + ) + else: + module_name = alias.name + exec_globals[alias.asname or alias.name] = importlib.import_module(module_name) except ModuleNotFoundError as e: msg = f"Module {alias.name} not found. Please install it and try again." raise ModuleNotFoundError(msg) from e diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index d6c4106ba..5f2088729 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -134,6 +134,8 @@ console_output_style = "progress" filterwarnings = ["ignore::DeprecationWarning"] log_cli = true markers = ["async_test"] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" [tool.mypy] plugins = ["pydantic.mypy"] @@ -189,6 +191,11 @@ ignore = [ "COM812", # Messes with the formatter ] +[tool.uv] +dev-dependencies = [ + "asgi-lifespan>=2.1.0", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/src/backend/tests/conftest.py b/src/backend/tests/conftest.py index 8bcfae4ea..eeaf62488 100644 --- a/src/backend/tests/conftest.py +++ b/src/backend/tests/conftest.py @@ -11,13 +11,14 @@ from typing import TYPE_CHECKING import orjson import pytest +from asgi_lifespan import LifespanManager from loguru import logger from pytest import LogCaptureFixture from base.langflow.components.inputs.ChatInput import ChatInput from dotenv import load_dotenv from fastapi.testclient import TestClient -from httpx import AsyncClient +from httpx import ASGITransport, AsyncClient from sqlmodel import Session, SQLModel, create_engine, select from sqlmodel.pool import StaticPool from tests.api_keys import get_openai_api_key @@ -260,7 +261,7 @@ def json_memory_chatbot_no_llm(): @pytest.fixture(name="client", autouse=True) -def client_fixture(session: Session, monkeypatch, request, load_flows_dir): +async def client_fixture(session: Session, monkeypatch, request, load_flows_dir): # Set the database url to a test database if "noclient" in request.keywords: yield @@ -281,8 +282,9 @@ def client_fixture(session: Session, monkeypatch, request, load_flows_dir): app = create_app() # app.dependency_overrides[get_session] = get_session_override - with TestClient(app) as client: - yield client + async with LifespanManager(app, startup_timeout=None, shutdown_timeout=None) as manager: + async with AsyncClient(transport=ASGITransport(app=manager.app), base_url="http://testserver/") as client: + yield client # app.dependency_overrides.clear() monkeypatch.undo() # clear the temp db @@ -307,12 +309,12 @@ def runner(): @pytest.fixture -def test_user(client): +async def test_user(client): user_data = UserCreate( username="testuser", password="testpassword", ) - response = client.post("/api/v1/users", json=user_data.model_dump()) + response = await client.post("api/v1/users/", json=user_data.model_dump()) assert response.status_code == 201 return response.json() @@ -337,9 +339,9 @@ def active_user(client): @pytest.fixture -def logged_in_headers(client, active_user): +async def logged_in_headers(client, active_user): login_data = {"username": active_user.username, "password": "testpassword"} - response = client.post("/api/v1/login", data=login_data) + response = await client.post("api/v1/login", data=login_data) assert response.status_code == 200 tokens = response.json() a_token = tokens["access_token"] @@ -375,11 +377,11 @@ def json_two_outputs(): @pytest.fixture -def added_flow_with_prompt_and_history(client, json_flow_with_prompt_and_history, logged_in_headers): +async def added_flow_with_prompt_and_history(client, json_flow_with_prompt_and_history, logged_in_headers): flow = orjson.loads(json_flow_with_prompt_and_history) data = flow["data"] flow = FlowCreate(name="Basic Chat", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 @@ -388,11 +390,11 @@ def added_flow_with_prompt_and_history(client, json_flow_with_prompt_and_history @pytest.fixture -def added_flow_chat_input(client, json_chat_input, logged_in_headers): +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 = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 @@ -401,11 +403,11 @@ def added_flow_chat_input(client, json_chat_input, logged_in_headers): @pytest.fixture -def added_flow_two_outputs(client, json_two_outputs, logged_in_headers): +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 = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 @@ -414,11 +416,11 @@ def added_flow_two_outputs(client, json_two_outputs, logged_in_headers): @pytest.fixture -def added_vector_store(client, json_vector_store, logged_in_headers): +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 = client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 @@ -427,13 +429,13 @@ def added_vector_store(client, json_vector_store, logged_in_headers): @pytest.fixture -def added_webhook_test(client, json_webhook_test, logged_in_headers): +async def added_webhook_test(client, json_webhook_test, logged_in_headers): webhook_test = orjson.loads(json_webhook_test) data = webhook_test["data"] webhook_test = FlowCreate( name="Webhook Test", description="description", data=data, endpoint_name=webhook_test["endpoint_name"] ) - response = client.post("api/v1/flows/", json=webhook_test.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 @@ -442,12 +444,12 @@ def added_webhook_test(client, json_webhook_test, logged_in_headers): @pytest.fixture -def flow_component(client: TestClient, logged_in_headers): +async def flow_component(client: TestClient, logged_in_headers): chat_input = ChatInput() graph = Graph(start=chat_input, end=chat_input) graph_dict = graph.dump(name="Chat Input Component") flow = FlowCreate(**graph_dict) - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 yield response.json() client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers) @@ -473,13 +475,13 @@ def created_api_key(active_user): @pytest.fixture(name="simple_api_test") -def get_simple_api_test(client, logged_in_headers, json_simple_api_test): +async def get_simple_api_test(client, logged_in_headers, json_simple_api_test): # Once the client is created, we can get the starter project # Just create a new flow with the 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 = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 yield response.json() client.delete(f"api/v1/flows/{response.json()['id']}", headers=logged_in_headers) diff --git a/src/backend/tests/integration/components/assistants/test_assistants_components.py b/src/backend/tests/integration/components/assistants/test_assistants_components.py index 90e36d4e8..a7b9f3b8a 100644 --- a/src/backend/tests/integration/components/assistants/test_assistants_components.py +++ b/src/backend/tests/integration/components/assistants/test_assistants_components.py @@ -1,8 +1,8 @@ import pytest - from tests.integration.utils import run_single_component +@pytest.mark.api_key_required async def test_list_assistants(): from langflow.components.astra_assistants import AssistantsListAssistants @@ -34,6 +34,7 @@ async def test_create_assistants(): await run_assistant(assistant_id, thread_id) +@pytest.mark.api_key_required async def test_create_thread(): from langflow.components.astra_assistants import AssistantsCreateThread diff --git a/src/backend/tests/integration/test_misc.py b/src/backend/tests/integration/test_misc.py index 8b83bc1ca..e15cd26d8 100644 --- a/src/backend/tests/integration/test_misc.py +++ b/src/backend/tests/integration/test_misc.py @@ -10,7 +10,7 @@ from langflow.load import run_flow_from_json @pytest.mark.api_key_required -def test_run_flow_with_caching_success(client: TestClient, starter_project, created_api_key): +async def test_run_flow_with_caching_success(client: TestClient, starter_project, created_api_key): flow_id = starter_project["id"] headers = {"x-api-key": created_api_key.api_key} payload = { @@ -20,7 +20,7 @@ def test_run_flow_with_caching_success(client: TestClient, starter_project, crea "tweaks": {"parameter_name": "value"}, "stream": False, } - response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) + response = await client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) assert response.status_code == status.HTTP_200_OK data = response.json() assert "outputs" in data @@ -28,11 +28,11 @@ def test_run_flow_with_caching_success(client: TestClient, starter_project, crea @pytest.mark.api_key_required -def test_run_flow_with_caching_invalid_flow_id(client: TestClient, created_api_key): +async def test_run_flow_with_caching_invalid_flow_id(client: TestClient, created_api_key): invalid_flow_id = uuid4() headers = {"x-api-key": created_api_key.api_key} payload = {"input_value": "", "input_type": "text", "output_type": "text", "tweaks": {}, "stream": False} - response = client.post(f"/api/v1/run/{invalid_flow_id}", json=payload, headers=headers) + response = await client.post(f"/api/v1/run/{invalid_flow_id}", json=payload, headers=headers) assert response.status_code == status.HTTP_404_NOT_FOUND data = response.json() assert "detail" in data @@ -40,16 +40,16 @@ def test_run_flow_with_caching_invalid_flow_id(client: TestClient, created_api_k @pytest.mark.api_key_required -def test_run_flow_with_caching_invalid_input_format(client: TestClient, starter_project, created_api_key): +async def test_run_flow_with_caching_invalid_input_format(client: TestClient, starter_project, created_api_key): flow_id = starter_project["id"] headers = {"x-api-key": created_api_key.api_key} payload = {"input_value": {"key": "value"}, "input_type": "text", "output_type": "text", "tweaks": {}} - response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) + response = await client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY @pytest.mark.api_key_required -def test_run_flow_with_invalid_tweaks(client, starter_project, created_api_key): +async def test_run_flow_with_invalid_tweaks(client, starter_project, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = starter_project["id"] payload = { @@ -58,12 +58,12 @@ def test_run_flow_with_invalid_tweaks(client, starter_project, created_api_key): "output_type": "text", "tweaks": {"invalid_tweak": "value"}, } - response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) + response = await client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) assert response.status_code == status.HTTP_200_OK @pytest.mark.api_key_required -def test_run_with_inputs_and_outputs(client, starter_project, created_api_key): +async def test_run_with_inputs_and_outputs(client, starter_project, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = starter_project["id"] payload = { @@ -73,7 +73,7 @@ def test_run_with_inputs_and_outputs(client, starter_project, created_api_key): "tweaks": {"parameter_name": "value"}, "stream": False, } - response = client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) + response = await client.post(f"/api/v1/run/{flow_id}", json=payload, headers=headers) assert response.status_code == status.HTTP_200_OK, response.text diff --git a/src/backend/tests/unit/api/v1/test_variable.py b/src/backend/tests/unit/api/v1/test_variable.py index 3b983fd2a..27a33e91e 100644 --- a/src/backend/tests/unit/api/v1/test_variable.py +++ b/src/backend/tests/unit/api/v1/test_variable.py @@ -1,12 +1,13 @@ -import pytest -from uuid import uuid4 from unittest import mock +from uuid import uuid4 -from fastapi import status, HTTPException +import pytest +from fastapi import HTTPException, status +from httpx import AsyncClient @pytest.fixture -def body(): +async def body(): return { "name": "test_variable", "value": "test_value", @@ -15,8 +16,8 @@ def body(): } -def test_create_variable(client, body, active_user, logged_in_headers): - response = client.post("api/v1/variables", json=body, headers=logged_in_headers) +async def test_create_variable(client: AsyncClient, body, active_user, logged_in_headers): + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_201_CREATED == response.status_code @@ -27,105 +28,112 @@ def test_create_variable(client, body, active_user, logged_in_headers): assert body["value"] != result["value"] -def test_create_variable__variable_name_alread_exists(client, body, active_user, logged_in_headers): - client.post("api/v1/variables", json=body, headers=logged_in_headers) +async def test_create_variable__variable_name_already_exists(client: AsyncClient, body, active_user, logged_in_headers): + await client.post("api/v1/variables/", json=body, headers=logged_in_headers) - response = client.post("api/v1/variables", json=body, headers=logged_in_headers) + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_400_BAD_REQUEST == response.status_code assert "Variable name already exists" in result["detail"] -def test_create_variable__variable_name_and_value_cannot_be_empty(client, body, active_user, logged_in_headers): +async def test_create_variable__variable_name_and_value_cannot_be_empty( + client: AsyncClient, body, active_user, logged_in_headers +): body["name"] = "" body["value"] = "" - response = client.post("api/v1/variables", json=body, headers=logged_in_headers) + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_400_BAD_REQUEST == response.status_code assert "Variable name and value cannot be empty" in result["detail"] -def test_create_variable__variable_name_cannot_be_empty(client, body, active_user, logged_in_headers): +async def test_create_variable__variable_name_cannot_be_empty( + client: AsyncClient, body, active_user, logged_in_headers +): body["name"] = "" - response = client.post("api/v1/variables", json=body, headers=logged_in_headers) + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_400_BAD_REQUEST == response.status_code assert "Variable name cannot be empty" in result["detail"] -def test_create_variable__variable_value_cannot_be_empty(client, body, active_user, logged_in_headers): +async def test_create_variable__variable_value_cannot_be_empty( + client: AsyncClient, body, active_user, logged_in_headers +): body["value"] = "" - response = client.post("api/v1/variables", json=body, headers=logged_in_headers) + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_400_BAD_REQUEST == response.status_code assert "Variable value cannot be empty" in result["detail"] -def test_create_variable__HTTPException(client, body, active_user, logged_in_headers): +async def test_create_variable__HTTPException(client: AsyncClient, body, active_user, logged_in_headers): status_code = 418 generic_message = "I'm a teapot" with mock.patch("langflow.services.auth.utils.encrypt_api_key") as m: m.side_effect = HTTPException(status_code=status_code, detail=generic_message) - response = client.post("api/v1/variables", json=body, headers=logged_in_headers) + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_418_IM_A_TEAPOT == response.status_code assert generic_message in result["detail"] -def test_create_variable__Exception(client, body, active_user, logged_in_headers): +async def test_create_variable__Exception(client: AsyncClient, body, active_user, logged_in_headers): generic_message = "Generic error message" with mock.patch("langflow.services.auth.utils.encrypt_api_key") as m: m.side_effect = Exception(generic_message) - response = client.post("api/v1/variables", json=body, headers=logged_in_headers) + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_500_INTERNAL_SERVER_ERROR == response.status_code assert generic_message in result["detail"] -def test_read_variables(client, body, active_user, logged_in_headers): +async def test_read_variables(client: AsyncClient, body, active_user, logged_in_headers): names = ["test_variable1", "test_variable2", "test_variable3"] for name in names: body["name"] = name - client.post("api/v1/variables", json=body, headers=logged_in_headers) + await client.post("api/v1/variables/", json=body, headers=logged_in_headers) - response = client.get("api/v1/variables", headers=logged_in_headers) + response = await client.get("api/v1/variables/", headers=logged_in_headers) result = response.json() assert status.HTTP_200_OK == response.status_code assert all(name in [r["name"] for r in result] for name in names) -def test_read_variables__empty(client, active_user, logged_in_headers): - all_variables = client.get("api/v1/variables", headers=logged_in_headers).json() +async def test_read_variables__empty(client: AsyncClient, active_user, logged_in_headers): + all_variables = await client.get("api/v1/variables/", headers=logged_in_headers) + all_variables = all_variables.json() for variable in all_variables: - client.delete(f"api/v1/variables/{variable.get('id')}", headers=logged_in_headers) + await client.delete(f"api/v1/variables/{variable.get('id')}", headers=logged_in_headers) - response = client.get("api/v1/variables", headers=logged_in_headers) + response = await client.get("api/v1/variables/", headers=logged_in_headers) result = response.json() assert status.HTTP_200_OK == response.status_code assert [] == result -def test_read_variables__(client, active_user, logged_in_headers): # TODO check if this is correct +async def test_read_variables__(client: AsyncClient, active_user, logged_in_headers): generic_message = "Generic error message" with pytest.raises(Exception) as exc: with mock.patch("sqlmodel.Session.exec") as m: m.side_effect = Exception(generic_message) - response = client.get("api/v1/variables", headers=logged_in_headers) + response = await client.get("api/v1/variables/", headers=logged_in_headers) result = response.json() assert status.HTTP_500_INTERNAL_SERVER_ERROR == response.status_code @@ -134,47 +142,46 @@ def test_read_variables__(client, active_user, logged_in_headers): # TODO check assert generic_message in str(exc.value) -def test_update_variable(client, body, active_user, logged_in_headers): - saved = client.post("api/v1/variables", json=body, headers=logged_in_headers).json() +async def test_update_variable(client: AsyncClient, body, active_user, logged_in_headers): + saved = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) + saved = saved.json() body["id"] = saved.get("id") body["name"] = "new_name" body["value"] = "new_value" body["type"] = "new_type" body["default_fields"] = ["new_field"] - response = client.patch(f"api/v1/variables/{saved.get('id')}", json=body, headers=logged_in_headers) + response = await client.patch(f"api/v1/variables/{saved.get('id')}", json=body, headers=logged_in_headers) result = response.json() assert status.HTTP_200_OK == response.status_code assert saved["id"] == result["id"] assert saved["name"] != result["name"] - # assert saved["type"] != result["type"] # TODO check if this is correct assert saved["default_fields"] != result["default_fields"] -def test_update_variable__Exception(client, body, active_user, logged_in_headers): +async def test_update_variable__Exception(client: AsyncClient, body, active_user, logged_in_headers): wrong_id = uuid4() body["id"] = str(wrong_id) - response = client.patch(f"api/v1/variables/{wrong_id}", json=body, headers=logged_in_headers) + response = await client.patch(f"api/v1/variables/{wrong_id}", json=body, headers=logged_in_headers) result = response.json() - # assert status.HTTP_404_NOT_FOUND == response.status_code # TODO check if this is correct + assert status.HTTP_404_NOT_FOUND == response.status_code assert "Variable not found" in result["detail"] -def test_delete_variable(client, body, active_user, logged_in_headers): - saved = client.post("api/v1/variables", json=body, headers=logged_in_headers).json() - - response = client.delete(f"api/v1/variables/{saved.get('id')}", headers=logged_in_headers) +async def test_delete_variable(client: AsyncClient, body, active_user, logged_in_headers): + response = await client.post("api/v1/variables/", json=body, headers=logged_in_headers) + saved = response.json() + response = await client.delete(f"api/v1/variables/{saved.get('id')}", headers=logged_in_headers) assert status.HTTP_204_NO_CONTENT == response.status_code -def test_delete_variable__Exception(client, active_user, logged_in_headers): +async def test_delete_variable__Exception(client: AsyncClient, active_user, logged_in_headers): wrong_id = uuid4() - response = client.delete(f"api/v1/variables/{wrong_id}", headers=logged_in_headers) + response = await client.delete(f"api/v1/variables/{wrong_id}", headers=logged_in_headers) - # assert status.HTTP_404_NOT_FOUND == response.status_code # TODO check if this is correct assert status.HTTP_500_INTERNAL_SERVER_ERROR == response.status_code diff --git a/src/backend/tests/unit/test_api_key.py b/src/backend/tests/unit/test_api_key.py index 92d649cae..5deaa3436 100644 --- a/src/backend/tests/unit/test_api_key.py +++ b/src/backend/tests/unit/test_api_key.py @@ -1,44 +1,42 @@ import pytest +from httpx import AsyncClient + from langflow.services.database.models.api_key import ApiKeyCreate @pytest.fixture -def api_key(client, logged_in_headers, active_user): +async def api_key(client, logged_in_headers, active_user): api_key = ApiKeyCreate(name="test-api-key") - response = client.post("api/v1/api_key", data=api_key.model_dump_json(), headers=logged_in_headers) + response = await client.post("api/v1/api_key/", data=api_key.model_dump_json(), headers=logged_in_headers) assert response.status_code == 200, response.text return response.json() -def test_get_api_keys(client, logged_in_headers, api_key): - response = client.get("api/v1/api_key", headers=logged_in_headers) +async def test_get_api_keys(client: AsyncClient, logged_in_headers, api_key): + response = await client.get("api/v1/api_key/", headers=logged_in_headers) assert response.status_code == 200, response.text data = response.json() assert "total_count" in data assert "user_id" in data assert "api_keys" in data assert any("test-api-key" in api_key["name"] for api_key in data["api_keys"]) - # assert all api keys in data["api_keys"] are masked assert all("**" in api_key["api_key"] for api_key in data["api_keys"]) -def test_create_api_key(client, logged_in_headers): +async def test_create_api_key(client: AsyncClient, logged_in_headers): api_key_name = "test-api-key" - response = client.post("api/v1/api_key", json={"name": api_key_name}, headers=logged_in_headers) + response = await client.post("api/v1/api_key/", json={"name": api_key_name}, headers=logged_in_headers) assert response.status_code == 200 data = response.json() assert "name" in data and data["name"] == api_key_name assert "api_key" in data - # When creating the API key is returned which is - # the only time the API key is unmasked assert "**" not in data["api_key"] -def test_delete_api_key(client, logged_in_headers, active_user, api_key): - # Assuming a function to create a test API key, returning the key ID +async def test_delete_api_key(client, logged_in_headers, active_user, api_key): api_key_id = api_key["id"] - response = client.delete(f"api/v1/api_key/{api_key_id}", headers=logged_in_headers) + response = await client.delete(f"api/v1/api_key/{api_key_id}", headers=logged_in_headers) assert response.status_code == 200 data = response.json() assert data["detail"] == "API Key deleted" diff --git a/src/backend/tests/unit/test_chat_endpoint.py b/src/backend/tests/unit/test_chat_endpoint.py index 81d0016ec..5140cc3f0 100644 --- a/src/backend/tests/unit/test_chat_endpoint.py +++ b/src/backend/tests/unit/test_chat_endpoint.py @@ -1,46 +1,49 @@ import json from uuid import UUID + from orjson import orjson from langflow.memory import get_messages from langflow.services.database.models.flow import FlowCreate, FlowUpdate -def test_build_flow(client, json_memory_chatbot_no_llm, logged_in_headers): - flow_id = _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) +async def test_build_flow(client, json_memory_chatbot_no_llm, logged_in_headers): + flow_id = await _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) - with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r: - consume_and_assert_stream(r) + async with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r: + await consume_and_assert_stream(r) check_messages(flow_id) -def test_build_flow_from_request_data(client, json_memory_chatbot_no_llm, logged_in_headers): - flow_id = _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) - flow_data = client.get("api/v1/flows/" + str(flow_id), headers=logged_in_headers).json() +async def test_build_flow_from_request_data(client, json_memory_chatbot_no_llm, logged_in_headers): + flow_id = await _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) + response = await client.get("api/v1/flows/" + str(flow_id), headers=logged_in_headers) + flow_data = response.json() - with client.stream( + async with client.stream( "POST", f"api/v1/build/{flow_id}/flow", json={"data": flow_data["data"]}, headers=logged_in_headers ) as r: - consume_and_assert_stream(r) + await consume_and_assert_stream(r) check_messages(flow_id) -def test_build_flow_with_frozen_path(client, json_memory_chatbot_no_llm, logged_in_headers): - flow_id = _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) +async def test_build_flow_with_frozen_path(client, json_memory_chatbot_no_llm, logged_in_headers): + flow_id = await _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) - flow_data = client.get("api/v1/flows/" + str(flow_id), headers=logged_in_headers).json() + response = await client.get("api/v1/flows/" + str(flow_id), headers=logged_in_headers) + flow_data = response.json() flow_data["data"]["nodes"][0]["data"]["node"]["frozen"] = True - response = client.patch( - "api/v1/flows/" + str(flow_id), + response = await client.patch( + f"api/v1/flows/{flow_id}", json=FlowUpdate(name="Flow", description="description", data=flow_data["data"]).model_dump(), headers=logged_in_headers, ) response.raise_for_status() - with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r: - consume_and_assert_stream(r) + async with client.stream("POST", f"api/v1/build/{flow_id}/flow", json={}, headers=logged_in_headers) as r: + await consume_and_assert_stream(r) check_messages(flow_id) @@ -57,9 +60,9 @@ def check_messages(flow_id): assert messages[1].sender_name == "AI" -def consume_and_assert_stream(r): +async def consume_and_assert_stream(r): count = 0 - for line in r.iter_lines(): + async for line in r.aiter_lines(): # httpx split by \n, but ndjson sends two \n for each line if not line: continue @@ -83,11 +86,11 @@ def consume_and_assert_stream(r): count += 1 -def _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers): +async def _create_flow(client, json_memory_chatbot_no_llm, logged_in_headers): vector_store = orjson.loads(json_memory_chatbot_no_llm) data = vector_store["data"] vector_store = FlowCreate(name="Flow", description="description", data=data, endpoint_name="f") - response = client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=vector_store.model_dump(), headers=logged_in_headers) response.raise_for_status() flow_id = response.json()["id"] return flow_id diff --git a/src/backend/tests/unit/test_cli.py b/src/backend/tests/unit/test_cli.py index 75614054f..275fee279 100644 --- a/src/backend/tests/unit/test_cli.py +++ b/src/backend/tests/unit/test_cli.py @@ -2,6 +2,7 @@ from pathlib import Path from tempfile import tempdir import pytest + from langflow.__main__ import app from langflow.services import deps diff --git a/src/backend/tests/unit/test_database.py b/src/backend/tests/unit/test_database.py index c59762417..9705df15e 100644 --- a/src/backend/tests/unit/test_database.py +++ b/src/backend/tests/unit/test_database.py @@ -31,72 +31,72 @@ def json_style(): ) -def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): +async def test_create_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] flow = FlowCreate(name=str(uuid4()), description="description", data=data) - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 = client.post("api/v1/flows/", json=flow.model_dump(exclude_unset=True), headers=logged_in_headers) + response = await client.post("api/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 -def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_headers): +async def test_read_flows(client: TestClient, json_flow: str, active_user, logged_in_headers): flow_data = orjson.loads(json_flow) data = flow_data["data"] flow = FlowCreate(name=str(uuid4()), description="description", data=data) - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 = client.get("api/v1/flows/", headers=logged_in_headers) + response = await client.get("api/v1/flows/", headers=logged_in_headers) assert response.status_code == 200 assert len(response.json()) > 0 -def test_read_flows_components_only(client: TestClient, flow_component: dict, logged_in_headers): - response = client.get("api/v1/flows/", headers=logged_in_headers, params={"components_only": True}) +async def test_read_flows_components_only(client: TestClient, flow_component: dict, logged_in_headers): + response = await client.get("api/v1/flows/", headers=logged_in_headers, params={"components_only": True}) assert response.status_code == 200 names = [flow["name"] for flow in response.json()] assert any("Chat Input Component" in name for name in names) assert all(flow["is_component"] is True for flow in response.json()), [flow["name"] for flow in response.json()] -def test_read_flow(client: TestClient, json_flow: str, logged_in_headers): +async def test_read_flow(client: TestClient, json_flow: str, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] unique_name = str(uuid4()) flow = FlowCreate(name=unique_name, description="description", data=data) - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/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 = client.get(f"api/v1/flows/{flow_id}", headers=logged_in_headers) + response = await client.get(f"api/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 -def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): +async def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) flow_id = response.json()["id"] updated_flow = FlowUpdate( @@ -104,7 +104,7 @@ def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_ description="updated description", data=data, ) - response = client.patch(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers) + response = await client.patch(f"api/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 @@ -112,28 +112,28 @@ def test_update_flow(client: TestClient, json_flow: str, active_user, logged_in_ # assert response.json()["data"] == updated_flow.data -def test_delete_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): +async def test_delete_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] flow = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) flow_id = response.json()["id"] - response = client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers) + response = await client.delete(f"api/v1/flows/{flow_id}", headers=logged_in_headers) assert response.status_code == 200 assert response.json()["message"] == "Flow deleted successfully" -def test_delete_flows(client: TestClient, json_flow: str, active_user, logged_in_headers): +async def test_delete_flows(client: TestClient, json_flow: str, active_user, logged_in_headers): # Create ten flows number_of_flows = 10 flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)] flow_ids = [] for flow in flows: - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 flow_ids.append(response.json()["id"]) - response = client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids) + response = await client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids) assert response.status_code == 200, response.content assert response.json().get("deleted") == number_of_flows @@ -147,7 +147,7 @@ async def test_delete_flows_with_transaction_and_build( flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)] flow_ids = [] for flow in flows: - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 flow_ids.append(response.json()["id"]) @@ -179,19 +179,19 @@ async def test_delete_flows_with_transaction_and_build( artifacts=build.get("artifacts"), ) - response = client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids) + response = await client.request("DELETE", "api/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 = client.request( + response = await client.request( "GET", "api/v1/monitor/transactions", params={"flow_id": flow_id}, headers=logged_in_headers ) assert response.status_code == 200 assert response.json() == [] for flow_id in flow_ids: - response = client.request( + response = await client.request( "GET", "api/v1/monitor/builds", params={"flow_id": flow_id}, headers=logged_in_headers ) assert response.status_code == 200 @@ -206,7 +206,7 @@ async def test_delete_folder_with_flows_with_transaction_and_build( folder_name = f"Test Folder {uuid4()}" folder = FolderCreate(name=folder_name, description="Test folder description", components_list=[], flows_list=[]) - response = client.post("/api/v1/folders/", json=folder.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/folders/", json=folder.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() @@ -218,7 +218,7 @@ async def test_delete_folder_with_flows_with_transaction_and_build( flow_ids = [] for flow in flows: flow.folder_id = folder_id - response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) assert response.status_code == 201 flow_ids.append(response.json()["id"]) @@ -249,25 +249,25 @@ async def test_delete_folder_with_flows_with_transaction_and_build( artifacts=build.get("artifacts"), ) - response = client.request("DELETE", f"api/v1/folders/{folder_id}", headers=logged_in_headers) + response = await client.request("DELETE", f"api/v1/folders/{folder_id}", headers=logged_in_headers) assert response.status_code == 204 for flow_id in flow_ids: - response = client.request( + response = await client.request( "GET", "api/v1/monitor/transactions", params={"flow_id": flow_id}, headers=logged_in_headers ) assert response.status_code == 200 assert response.json() == [] for flow_id in flow_ids: - response = client.request( + response = await client.request( "GET", "api/v1/monitor/builds", params={"flow_id": flow_id}, headers=logged_in_headers ) assert response.status_code == 200 assert response.json() == {"vertex_builds": {}} -def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers): +async def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] # Create test data @@ -280,7 +280,7 @@ def test_create_flows(client: TestClient, session: Session, json_flow: str, logg ] ) # Make request to endpoint - response = client.post("api/v1/flows/batch/", json=flow_list.dict(), headers=logged_in_headers) + response = await client.post("api/v1/flows/batch/", json=flow_list.dict(), headers=logged_in_headers) # Check response status code assert response.status_code == 201 # Check response data @@ -294,7 +294,7 @@ def test_create_flows(client: TestClient, session: Session, json_flow: str, logg assert response_data[1]["data"] == data -def test_upload_file(client: TestClient, session: Session, json_flow: str, logged_in_headers): +async def test_upload_file(client: TestClient, session: Session, json_flow: str, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] # Create test data @@ -307,7 +307,7 @@ def test_upload_file(client: TestClient, session: Session, json_flow: str, logge ] ) file_contents = orjson_dumps(flow_list.dict()) - response = client.post( + response = await client.post( "api/v1/flows/upload/", files={"file": ("examples.json", file_contents, "application/json")}, headers=logged_in_headers, @@ -325,7 +325,7 @@ def test_upload_file(client: TestClient, session: Session, json_flow: str, logge assert response_data[1]["data"] == data -def test_download_file( +async def test_download_file( client: TestClient, session: Session, json_flow, @@ -355,7 +355,7 @@ def test_download_file( # Make request to endpoint inside the session context flow_ids = [str(db_flow.id) for db_flow in saved_flows] # Convert UUIDs to strings flow_ids_json = json.dumps(flow_ids) - response = client.post( + response = await client.post( "api/v1/flows/download/", data=flow_ids_json, headers={**logged_in_headers, "Content-Type": "application/json"}, @@ -368,31 +368,31 @@ def test_download_file( assert "attachment; filename=" in response.headers["Content-Disposition"] -def test_create_flow_with_invalid_data(client: TestClient, active_user, logged_in_headers): +async def test_create_flow_with_invalid_data(client: TestClient, active_user, logged_in_headers): flow = {"name": "a" * 256, "data": "Invalid flow data"} - response = client.post("api/v1/flows/", json=flow, headers=logged_in_headers) + response = await client.post("api/v1/flows/", json=flow, headers=logged_in_headers) assert response.status_code == 422 -def test_get_nonexistent_flow(client: TestClient, active_user, logged_in_headers): +async def test_get_nonexistent_flow(client: TestClient, active_user, logged_in_headers): uuid = uuid4() - response = client.get(f"api/v1/flows/{uuid}", headers=logged_in_headers) + response = await client.get(f"api/v1/flows/{uuid}", headers=logged_in_headers) assert response.status_code == 404 -def test_update_flow_idempotency(client: TestClient, json_flow: str, active_user, logged_in_headers): +async def test_update_flow_idempotency(client: TestClient, json_flow: str, active_user, logged_in_headers): flow_data = orjson.loads(json_flow) data = flow_data["data"] flow_data = FlowCreate(name="Test Flow", description="description", data=data) - response = client.post("api/v1/flows/", json=flow_data.dict(), headers=logged_in_headers) + response = await client.post("api/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 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers) - response2 = client.put(f"api/v1/flows/{flow_id}", json=updated_flow.model_dump(), headers=logged_in_headers) + 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) assert response1.json() == response2.json() -def test_update_nonexistent_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): +async def test_update_nonexistent_flow(client: TestClient, json_flow: str, active_user, logged_in_headers): flow_data = orjson.loads(json_flow) data = flow_data["data"] uuid = uuid4() @@ -401,31 +401,31 @@ def test_update_nonexistent_flow(client: TestClient, json_flow: str, active_user description="description", data=data, ) - response = client.patch(f"api/v1/flows/{uuid}", json=updated_flow.model_dump(), headers=logged_in_headers) + response = await client.patch(f"api/v1/flows/{uuid}", json=updated_flow.model_dump(), headers=logged_in_headers) assert response.status_code == 404, response.text -def test_delete_nonexistent_flow(client: TestClient, active_user, logged_in_headers): +async def test_delete_nonexistent_flow(client: TestClient, active_user, logged_in_headers): uuid = uuid4() - response = client.delete(f"api/v1/flows/{uuid}", headers=logged_in_headers) + response = await client.delete(f"api/v1/flows/{uuid}", headers=logged_in_headers) assert response.status_code == 404 -def test_read_only_starter_projects(client: TestClient, active_user, logged_in_headers): - response = client.get("api/v1/flows/", headers=logged_in_headers) +async def test_read_only_starter_projects(client: TestClient, active_user, logged_in_headers): + response = await client.get("api/v1/flows/", headers=logged_in_headers) starter_projects = load_starter_projects() assert response.status_code == 200 assert len(response.json()) == len(starter_projects) @pytest.mark.load_flows -def test_load_flows(client: TestClient, load_flows_dir): - response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b") +async def test_load_flows(client: TestClient, load_flows_dir): + response = await client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b") assert response.status_code == 200 assert response.json()["name"] == "BasicExample" # re-run to ensure updates work well load_flows_from_directory() - response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b") + response = await client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b") assert response.status_code == 200 assert response.json()["name"] == "BasicExample" @@ -436,5 +436,5 @@ def test_sqlite_pragmas(): with db_service.with_session() as session: from sqlalchemy import text - assert "wal" == session.execute(text("PRAGMA journal_mode;")).fetchone()[0] - assert 1 == session.execute(text("PRAGMA synchronous;")).fetchone()[0] + assert "wal" == session.exec(text("PRAGMA journal_mode;")).scalar() + assert 1 == session.exec(text("PRAGMA synchronous;")).scalar() diff --git a/src/backend/tests/unit/test_endpoints.py b/src/backend/tests/unit/test_endpoints.py index f238a4a6a..324a530ca 100644 --- a/src/backend/tests/unit/test_endpoints.py +++ b/src/backend/tests/unit/test_endpoints.py @@ -3,14 +3,14 @@ from uuid import UUID, uuid4 import pytest from fastapi import status -from fastapi.testclient import TestClient +from httpx import AsyncClient from langflow.custom.directory_reader.directory_reader import DirectoryReader from langflow.services.deps import get_settings_service -def run_post(client, flow_id, headers, post_data): - response = client.post( +async def run_post(client, flow_id, headers, post_data): + response = await client.post( f"api/v1/process/{flow_id}", headers=headers, json=post_data, @@ -20,9 +20,9 @@ def run_post(client, flow_id, headers, post_data): # Helper function to poll task status -def poll_task_status(client, headers, href, max_attempts=20, sleep_time=1): +async def poll_task_status(client, headers, href, max_attempts=20, sleep_time=1): for _ in range(max_attempts): - task_status_response = client.get( + task_status_response = await client.get( href, headers=headers, ) @@ -135,7 +135,7 @@ PROMPT_REQUEST = { # "session_id": None, # } -# response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data) +# response = await client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data) # assert response.status_code == 403 # assert response.json() == {"detail": "Invalid or missing API key"} @@ -160,7 +160,7 @@ PROMPT_REQUEST = { # } # invalid_id = uuid.uuid4() -# response = client.post(f"api/v1/process/{invalid_id}", headers=headers, json=post_data) +# response = await client.post(f"api/v1/process/{invalid_id}", headers=headers, json=post_data) # assert response.status_code == 404 # assert f"Flow {invalid_id} not found" in response.json()["detail"] @@ -216,7 +216,7 @@ PROMPT_REQUEST = { # # Make the request to the FastAPI TestClient -# response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data) +# response = await client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data) # # Check the response # assert response.status_code == 200, response.json() @@ -253,15 +253,15 @@ PROMPT_REQUEST = { # # Make the request to the FastAPI TestClient -# response = client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data) +# response = await client.post(f"api/v1/process/{flow.id}", headers=headers, json=post_data) # # Check the response # assert response.status_code == 403, response.json() # assert response.json() == {"detail": "Invalid or missing API key"} -def test_get_all(client: TestClient, logged_in_headers): - response = client.get("api/v1/all", headers=logged_in_headers) +async def test_get_all(client: AsyncClient, logged_in_headers): + response = await client.get("api/v1/all", headers=logged_in_headers) assert response.status_code == 200 settings = get_settings_service().settings dir_reader = DirectoryReader(settings.components_path[0]) @@ -278,7 +278,7 @@ def test_get_all(client: TestClient, logged_in_headers): assert "ChatOutput" in json_response["outputs"] -def test_post_validate_code(client: TestClient): +async def test_post_validate_code(client: AsyncClient): # Test case with a valid import and function code1 = """ import math @@ -286,7 +286,7 @@ import math def square(x): return x ** 2 """ - response1 = client.post("api/v1/validate/code", json={"code": code1}) + response1 = await client.post("api/v1/validate/code", json={"code": code1}) assert response1.status_code == 200 assert response1.json() == {"imports": {"errors": []}, "function": {"errors": []}} @@ -297,7 +297,7 @@ import non_existent_module def square(x): return x ** 2 """ - response2 = client.post("api/v1/validate/code", json={"code": code2}) + response2 = await client.post("api/v1/validate/code", json={"code": code2}) assert response2.status_code == 200 assert response2.json() == { "imports": {"errors": ["No module named 'non_existent_module'"]}, @@ -311,7 +311,7 @@ import math def square(x) return x ** 2 """ - response3 = client.post("api/v1/validate/code", json={"code": code3}) + response3 = await client.post("api/v1/validate/code", json={"code": code3}) assert response3.status_code == 200 assert response3.json() == { "imports": {"errors": []}, @@ -319,11 +319,11 @@ def square(x) } # Test case with invalid JSON payload - response4 = client.post("api/v1/validate/code", json={"invalid_key": code1}) + response4 = await client.post("api/v1/validate/code", json={"invalid_key": code1}) assert response4.status_code == 422 # Test case with an empty code string - response5 = client.post("api/v1/validate/code", json={"code": ""}) + response5 = await client.post("api/v1/validate/code", json={"code": ""}) assert response5.status_code == 200 assert response5.json() == {"imports": {"errors": []}, "function": {"errors": []}} @@ -334,7 +334,7 @@ import math def square(x) return x ** 2 """ - response6 = client.post("api/v1/validate/code", json={"code": code6}) + response6 = await client.post("api/v1/validate/code", json={"code": code6}) assert response6.status_code == 200 assert response6.json() == { "imports": {"errors": []}, @@ -359,16 +359,16 @@ What is a good name for a company that makes {product}? INVALID_PROMPT = "This is an invalid prompt without any input variable." -def test_valid_prompt(client: TestClient): +async def test_valid_prompt(client: AsyncClient): PROMPT_REQUEST["template"] = VALID_PROMPT - response = client.post("api/v1/validate/prompt", json=PROMPT_REQUEST) + response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST) assert response.status_code == 200 assert response.json()["input_variables"] == ["product"] -def test_invalid_prompt(client: TestClient): +async def test_invalid_prompt(client: AsyncClient): PROMPT_REQUEST["template"] = INVALID_PROMPT - response = client.post( + response = await client.post( "api/v1/validate/prompt", json=PROMPT_REQUEST, ) @@ -385,22 +385,22 @@ def test_invalid_prompt(client: TestClient): ("{a}, {b}, and {c} are variables.", ["a", "b", "c"]), ], ) -def test_various_prompts(client, prompt, expected_input_variables): +async def test_various_prompts(client, prompt, expected_input_variables): PROMPT_REQUEST["template"] = prompt - response = client.post("api/v1/validate/prompt", json=PROMPT_REQUEST) + response = await client.post("api/v1/validate/prompt", json=PROMPT_REQUEST) assert response.status_code == 200 assert response.json()["input_variables"] == expected_input_variables -def test_get_vertices_flow_not_found(client, logged_in_headers): +async def test_get_vertices_flow_not_found(client, logged_in_headers): uuid = uuid4() - response = client.post(f"/api/v1/build/{uuid}/vertices", headers=logged_in_headers) + response = await client.post(f"/api/v1/build/{uuid}/vertices", headers=logged_in_headers) assert response.status_code == 500 -def test_get_vertices(client, added_flow_with_prompt_and_history, logged_in_headers): +async def test_get_vertices(client, added_flow_with_prompt_and_history, logged_in_headers): flow_id = added_flow_with_prompt_and_history["id"] - response = client.post(f"/api/v1/build/{flow_id}/vertices", headers=logged_in_headers) + response = await client.post(f"/api/v1/build/{flow_id}/vertices", headers=logged_in_headers) assert response.status_code == 200 assert "ids" in response.json() # The response should contain the list in this order @@ -415,22 +415,22 @@ def test_get_vertices(client, added_flow_with_prompt_and_history, logged_in_head } -def test_build_vertex_invalid_flow_id(client, logged_in_headers): +async def test_build_vertex_invalid_flow_id(client, logged_in_headers): uuid = uuid4() - response = client.post(f"/api/v1/build/{uuid}/vertices/vertex_id", headers=logged_in_headers) + response = await client.post(f"/api/v1/build/{uuid}/vertices/vertex_id", headers=logged_in_headers) assert response.status_code == 500 -def test_build_vertex_invalid_vertex_id(client, added_flow_with_prompt_and_history, logged_in_headers): +async def test_build_vertex_invalid_vertex_id(client, added_flow_with_prompt_and_history, logged_in_headers): flow_id = added_flow_with_prompt_and_history["id"] - response = client.post(f"/api/v1/build/{flow_id}/vertices/invalid_vertex_id", headers=logged_in_headers) + response = await client.post(f"/api/v1/build/{flow_id}/vertices/invalid_vertex_id", headers=logged_in_headers) assert response.status_code == 500 -def test_successful_run_no_payload(client, simple_api_test, created_api_key): +async def test_successful_run_no_payload(client, simple_api_test, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = simple_api_test["id"] - response = client.post(f"/api/v1/run/{flow_id}", headers=headers) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers) assert response.status_code == status.HTTP_200_OK, response.text # Add more assertions here to validate the response content json_response = response.json() @@ -455,13 +455,13 @@ def test_successful_run_no_payload(client, simple_api_test, created_api_key): assert all([result is not None for result in inner_results]), (outputs_dict, output_results_has_results) -def test_successful_run_with_output_type_text(client, simple_api_test, created_api_key): +async def test_successful_run_with_output_type_text(client, simple_api_test, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = simple_api_test["id"] payload = { "output_type": "text", } - response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) assert response.status_code == status.HTTP_200_OK, response.text # Add more assertions here to validate the response content json_response = response.json() @@ -485,14 +485,14 @@ def test_successful_run_with_output_type_text(client, simple_api_test, created_a assert all([key in result for result in inner_results for key in expected_keys]), outputs_dict -def test_successful_run_with_output_type_any(client, simple_api_test, created_api_key): +async def test_successful_run_with_output_type_any(client, simple_api_test, created_api_key): # This one should have both the ChatOutput and TextOutput components headers = {"x-api-key": created_api_key.api_key} flow_id = simple_api_test["id"] payload = { "output_type": "any", } - response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) assert response.status_code == status.HTTP_200_OK, response.text # Add more assertions here to validate the response content json_response = response.json() @@ -516,7 +516,7 @@ def test_successful_run_with_output_type_any(client, simple_api_test, created_ap assert all([key in result for result in inner_results for key in expected_keys]), outputs_dict -def test_successful_run_with_output_type_debug(client, simple_api_test, created_api_key): +async def test_successful_run_with_output_type_debug(client, simple_api_test, created_api_key): # This one should return outputs for all components # Let's just check the amount of outputs(there should be 7) headers = {"x-api-key": created_api_key.api_key} @@ -524,7 +524,7 @@ def test_successful_run_with_output_type_debug(client, simple_api_test, created_ payload = { "output_type": "debug", } - response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) assert response.status_code == status.HTTP_200_OK, response.text # Add more assertions here to validate the response content json_response = response.json() @@ -541,7 +541,7 @@ def test_successful_run_with_output_type_debug(client, simple_api_test, created_ assert len(outputs_dict.get("outputs")) == 3 -def test_successful_run_with_input_type_text(client, simple_api_test, created_api_key): +async def test_successful_run_with_input_type_text(client, simple_api_test, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = simple_api_test["id"] payload = { @@ -549,7 +549,7 @@ def test_successful_run_with_input_type_text(client, simple_api_test, created_ap "output_type": "debug", "input_value": "value1", } - response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) assert response.status_code == status.HTTP_200_OK, response.text # Add more assertions here to validate the response content json_response = response.json() @@ -574,7 +574,8 @@ def test_successful_run_with_input_type_text(client, simple_api_test, created_ap ), text_input_outputs -def test_successful_run_with_input_type_chat(client, simple_api_test, created_api_key): +@pytest.mark.api_key_required +async def test_successful_run_with_input_type_chat(client: AsyncClient, simple_api_test, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = simple_api_test["id"] payload = { @@ -582,7 +583,7 @@ def test_successful_run_with_input_type_chat(client, simple_api_test, created_ap "output_type": "debug", "input_value": "value1", } - response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) assert response.status_code == status.HTTP_200_OK, response.text # Add more assertions here to validate the response content json_response = response.json() @@ -606,7 +607,7 @@ def test_successful_run_with_input_type_chat(client, simple_api_test, created_ap ), chat_input_outputs -def test_invalid_run_with_input_type_chat(client, simple_api_test, created_api_key): +async def test_invalid_run_with_input_type_chat(client, simple_api_test, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = simple_api_test["id"] payload = { @@ -615,12 +616,12 @@ def test_invalid_run_with_input_type_chat(client, simple_api_test, created_api_k "input_value": "value1", "tweaks": {"Chat Input": {"input_value": "value2"}}, } - response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text assert "If you pass an input_value to the chat input, you cannot pass a tweak with the same name." in response.text -def test_successful_run_with_input_type_any(client, simple_api_test, created_api_key): +async def test_successful_run_with_input_type_any(client, simple_api_test, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = simple_api_test["id"] payload = { @@ -628,7 +629,7 @@ def test_successful_run_with_input_type_any(client, simple_api_test, created_api "output_type": "debug", "input_value": "value1", } - response = client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers, json=payload) assert response.status_code == status.HTTP_200_OK, response.text # Add more assertions here to validate the response content json_response = response.json() @@ -660,19 +661,19 @@ def test_successful_run_with_input_type_any(client, simple_api_test, created_api ), any_input_outputs -def test_invalid_flow_id(client, created_api_key): +async def test_invalid_flow_id(client, created_api_key): headers = {"x-api-key": created_api_key.api_key} flow_id = "invalid-flow-id" - response = client.post(f"/api/v1/run/{flow_id}", headers=headers) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers) assert response.status_code == status.HTTP_404_NOT_FOUND, response.text headers = {"x-api-key": created_api_key.api_key} flow_id = UUID(int=0) - response = client.post(f"/api/v1/run/{flow_id}", headers=headers) + response = await client.post(f"/api/v1/run/{flow_id}", headers=headers) assert response.status_code == status.HTTP_404_NOT_FOUND, response.text # Check if the error detail is as expected -def test_starter_projects(client, created_api_key): +async def test_starter_projects(client, created_api_key): headers = {"x-api-key": created_api_key.api_key} - response = client.get("/api/v1/starter-projects/", headers=headers) + response = await client.get("api/v1/starter-projects/", headers=headers) assert response.status_code == status.HTTP_200_OK, response.text diff --git a/src/backend/tests/unit/test_files.py b/src/backend/tests/unit/test_files.py index 4c4d9c73e..33a5f5b70 100644 --- a/src/backend/tests/unit/test_files.py +++ b/src/backend/tests/unit/test_files.py @@ -1,7 +1,15 @@ +import os import re +import shutil +import tempfile +from contextlib import suppress +from pathlib import Path from unittest.mock import MagicMock import pytest +from asgi_lifespan import LifespanManager +from httpx import ASGITransport, AsyncClient +from sqlmodel import Session from langflow.services.deps import get_storage_service from langflow.services.storage.service import StorageService @@ -19,12 +27,42 @@ def mock_storage_service(): return service -def test_upload_file(client, mock_storage_service, created_api_key, flow): - headers = {"x-api-key": created_api_key.api_key} - # Replace the actual storage service with the mock - client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service +@pytest.fixture(name="files_client", scope="function") +async def files_client_fixture(session: Session, monkeypatch, request, load_flows_dir, mock_storage_service): + # Set the database url to a test database + if "noclient" in request.keywords: + yield + else: + db_dir = tempfile.mkdtemp() + db_path = Path(db_dir) / "test.db" + monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}") + monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false") + if "load_flows" in request.keywords: + shutil.copyfile( + pytest.BASIC_EXAMPLE_PATH, os.path.join(load_flows_dir, "c54f9130-f2fa-4a3e-b22a-3856d946351b.json") + ) + monkeypatch.setenv("LANGFLOW_LOAD_FLOWS_PATH", load_flows_dir) + monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "true") - response = client.post( + from langflow.main import create_app + + app = create_app() + + app.dependency_overrides[get_storage_service] = lambda: mock_storage_service + async with LifespanManager(app, startup_timeout=None, shutdown_timeout=None) as manager: + async with AsyncClient(transport=ASGITransport(app=manager.app), base_url="http://testserver/") as client: + yield client + # app.dependency_overrides.clear() + monkeypatch.undo() + # clear the temp db + with suppress(FileNotFoundError): + db_path.unlink() + + +async def test_upload_file(files_client, mock_storage_service, created_api_key, flow): + headers = {"x-api-key": created_api_key.api_key} + + response = await files_client.post( f"api/v1/files/upload/{flow.id}", files={"file": ("test.txt", b"test content")}, headers=headers, @@ -39,41 +77,36 @@ def test_upload_file(client, mock_storage_service, created_api_key, flow): assert file_path_pattern.match(response_json["file_path"]) -def test_download_file(client, mock_storage_service, created_api_key, flow): +async def test_download_file(files_client, mock_storage_service, created_api_key, flow): headers = {"x-api-key": created_api_key.api_key} - client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service - - response = client.get(f"api/v1/files/download/{flow.id}/test.txt", headers=headers) + response = await files_client.get(f"api/v1/files/download/{flow.id}/test.txt", headers=headers) assert response.status_code == 200 assert response.content == b"file content" -def test_list_files(client, mock_storage_service, created_api_key, flow): +async def test_list_files(files_client, mock_storage_service, created_api_key, flow): headers = {"x-api-key": created_api_key.api_key} - client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service - - response = client.get(f"api/v1/files/list/{flow.id}", headers=headers) + response = await files_client.get(f"api/v1/files/list/{flow.id}", headers=headers) assert response.status_code == 200 assert response.json() == {"files": ["file1.txt", "file2.jpg"]} -def test_delete_file(client, mock_storage_service, created_api_key, flow): +async def test_delete_file(files_client, mock_storage_service, created_api_key, flow): headers = {"x-api-key": created_api_key.api_key} - client.app.dependency_overrides[get_storage_service] = lambda: mock_storage_service - response = client.delete(f"api/v1/files/delete/{flow.id}/test.txt", headers=headers) + response = await files_client.delete(f"api/v1/files/delete/{flow.id}/test.txt", headers=headers) assert response.status_code == 200 assert response.json() == {"message": "File test.txt deleted successfully"} -def test_file_operations(client, created_api_key, flow): +async def test_file_operations(client, created_api_key, flow): headers = {"x-api-key": created_api_key.api_key} flow_id = flow.id file_name = "test.txt" file_content = b"Hello, world!" # Step 1: Upload the file - response = client.post( + response = await client.post( f"api/v1/files/upload/{flow_id}", files={"file": (file_name, file_content)}, headers=headers, @@ -91,21 +124,21 @@ def test_file_operations(client, created_api_key, flow): full_file_name = response_json["file_path"].split("/")[-1] # Step 2: List files in the folder - response = client.get(f"api/v1/files/list/{flow_id}", headers=headers) + response = await client.get(f"api/v1/files/list/{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 = client.get(f"api/v1/files/download/{flow_id}/{full_file_name}", headers=headers) + response = await client.get(f"api/v1/files/download/{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 = client.delete(f"api/v1/files/delete/{flow_id}/{full_file_name}", headers=headers) + response = await client.delete(f"api/v1/files/delete/{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 = client.get(f"api/v1/files/list/{flow_id}", headers=headers) + response = await client.get(f"api/v1/files/list/{flow_id}", headers=headers) assert full_file_name not in response.json()["files"] diff --git a/src/backend/tests/unit/test_login.py b/src/backend/tests/unit/test_login.py index f4bc05bd5..b8e945ae1 100644 --- a/src/backend/tests/unit/test_login.py +++ b/src/backend/tests/unit/test_login.py @@ -1,8 +1,9 @@ import pytest +from sqlalchemy.exc import IntegrityError + from langflow.services.auth.utils import get_password_hash from langflow.services.database.models.user import User from langflow.services.deps import session_scope -from sqlalchemy.exc import IntegrityError @pytest.fixture @@ -15,7 +16,7 @@ def test_user(): ) -def test_login_successful(client, test_user): +async def test_login_successful(client, test_user): # Adding the test user to the database try: with session_scope() as session: @@ -24,22 +25,22 @@ def test_login_successful(client, test_user): except IntegrityError: pass - response = client.post("api/v1/login", data={"username": "testuser", "password": "testpassword"}) + response = await client.post("api/v1/login", data={"username": "testuser", "password": "testpassword"}) assert response.status_code == 200 assert "access_token" in response.json() -def test_login_unsuccessful_wrong_username(client): - response = client.post("api/v1/login", data={"username": "wrongusername", "password": "testpassword"}) +async def test_login_unsuccessful_wrong_username(client): + response = await client.post("api/v1/login", data={"username": "wrongusername", "password": "testpassword"}) assert response.status_code == 401 assert response.json()["detail"] == "Incorrect username or password" -def test_login_unsuccessful_wrong_password(client, test_user, session): +async def test_login_unsuccessful_wrong_password(client, test_user, session): # Adding the test user to the database session.add(test_user) session.commit() - response = client.post("api/v1/login", data={"username": "testuser", "password": "wrongpassword"}) + response = await client.post("api/v1/login", data={"username": "testuser", "password": "wrongpassword"}) assert response.status_code == 401 assert response.json()["detail"] == "Incorrect username or password" diff --git a/src/backend/tests/unit/test_messages_endpoints.py b/src/backend/tests/unit/test_messages_endpoints.py index 74d9badc1..78c15970c 100644 --- a/src/backend/tests/unit/test_messages_endpoints.py +++ b/src/backend/tests/unit/test_messages_endpoints.py @@ -1,7 +1,7 @@ from uuid import UUID import pytest -from fastapi.testclient import TestClient +from httpx import AsyncClient from langflow.memory import add_messagetables @@ -12,7 +12,7 @@ from langflow.services.deps import session_scope @pytest.fixture() -def created_message(): +async def created_message(): with session_scope() as session: message = MessageCreate(text="Test message", sender="User", sender_name="User", session_id="session_id") messagetable = MessageTable.model_validate(message, from_attributes=True) @@ -35,18 +35,20 @@ def created_messages(session): return message_list -def test_delete_messages(client: TestClient, created_messages, logged_in_headers): - response = client.request( +@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 ) assert response.status_code == 204, response.text assert response.reason_phrase == "No Content" -def test_update_message(client: TestClient, logged_in_headers, created_message): +@pytest.mark.api_key_required +async def test_update_message(client: AsyncClient, logged_in_headers, created_message): message_id = created_message.id message_update = MessageUpdate(text="Updated content") - response = client.put( + response = await client.put( f"api/v1/monitor/messages/{message_id}", json=message_update.model_dump(), headers=logged_in_headers ) assert response.status_code == 200, response.text @@ -54,34 +56,36 @@ def test_update_message(client: TestClient, logged_in_headers, created_message): assert updated_message.text == "Updated content" -def test_update_message_not_found(client: TestClient, logged_in_headers): +@pytest.mark.api_key_required +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 = client.put( + response = await client.put( f"api/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" -def test_delete_messages_session(client: TestClient, created_messages, logged_in_headers): +@pytest.mark.api_key_required +async def test_delete_messages_session(client: AsyncClient, created_messages, logged_in_headers): session_id = "session_id2" - response = client.delete(f"api/v1/monitor/messages/session/{session_id}", headers=logged_in_headers) + response = await client.delete(f"api/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 = client.get("api/v1/monitor/messages", headers=logged_in_headers) + response = await client.get("api/v1/monitor/messages", headers=logged_in_headers) assert response.status_code == 200 assert len(response.json()) == 0 # Successfully update session ID for all messages with the old session ID -def test_successfully_update_session_id(client, session, logged_in_headers, created_messages): +async def test_successfully_update_session_id(client, session, logged_in_headers, created_messages): old_session_id = "session_id2" new_session_id = "new_session_id" - response = client.patch( + response = await client.patch( f"api/v1/monitor/messages/session/{old_session_id}", params={"new_session_id": new_session_id}, headers=logged_in_headers, @@ -93,7 +97,9 @@ def test_successfully_update_session_id(client, session, logged_in_headers, crea for message in updated_messages: assert message["session_id"] == new_session_id - response = client.get("api/v1/monitor/messages", headers=logged_in_headers, params={"session_id": new_session_id}) + response = await client.get( + "api/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) for message in response.json(): @@ -101,11 +107,11 @@ def test_successfully_update_session_id(client, session, logged_in_headers, crea # No messages found with the given session ID -def test_no_messages_found_with_given_session_id(client, session, logged_in_headers): +async def test_no_messages_found_with_given_session_id(client, session, logged_in_headers): old_session_id = "non_existent_session_id" new_session_id = "new_session_id" - response = client.patch( + response = await client.patch( f"/messages/session/{old_session_id}", params={"new_session_id": new_session_id}, headers=logged_in_headers ) diff --git a/src/backend/tests/unit/test_process.py b/src/backend/tests/unit/test_process.py index 5d0f80d90..1f6ea5bce 100644 --- a/src/backend/tests/unit/test_process.py +++ b/src/backend/tests/unit/test_process.py @@ -1,4 +1,5 @@ import pytest + from langflow.processing.process import process_tweaks from langflow.services.deps import get_session_service diff --git a/src/backend/tests/unit/test_user.py b/src/backend/tests/unit/test_user.py index 86d4b1865..461ebd7ea 100644 --- a/src/backend/tests/unit/test_user.py +++ b/src/backend/tests/unit/test_user.py @@ -1,6 +1,7 @@ from datetime import datetime import pytest +from httpx import AsyncClient from langflow.services.auth.utils import create_super_user, get_password_hash from langflow.services.database.models.user import UserUpdate @@ -22,14 +23,14 @@ def super_user(client): @pytest.fixture -def super_user_headers(client, super_user): +async def super_user_headers(client: AsyncClient, super_user): settings_service = get_settings_service() auth_settings = settings_service.auth_settings login_data = { "username": auth_settings.SUPERUSER, "password": auth_settings.SUPERUSER_PASSWORD, } - response = client.post("/api/v1/login", data=login_data) + response = await client.post("api/v1/login", data=login_data) assert response.status_code == 200 tokens = response.json() a_token = tokens["access_token"] @@ -52,9 +53,8 @@ def deactivated_user(): return user -def test_user_waiting_for_approval( - client, -): +@pytest.mark.api_key_required +async def test_user_waiting_for_approval(client: AsyncClient): # Create a user that is not active and has never logged in with session_getter(get_db_service()) as session: user = User( @@ -67,50 +67,54 @@ def test_user_waiting_for_approval( session.commit() login_data = {"username": "waitingforapproval", "password": "testpassword"} - response = client.post("/api/v1/login", data=login_data) + response = await client.post("api/v1/login", data=login_data) assert response.status_code == 400 assert response.json()["detail"] == "Waiting for approval" -def test_deactivated_user_cannot_login(client, deactivated_user): +@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 = client.post("/api/v1/login", data=login_data) + response = await client.post("api/v1/login", data=login_data) assert response.status_code == 401, response.json() assert response.json()["detail"] == "Inactive user", response.text -def test_deactivated_user_cannot_access(client, deactivated_user, logged_in_headers): +async def test_deactivated_user_cannot_access(client: AsyncClient, deactivated_user, logged_in_headers): # Assuming the headers for deactivated_user - response = client.get("/api/v1/users", headers=logged_in_headers) - assert response.status_code == 403, response.json() + response = await client.get("api/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 -def test_data_consistency_after_update(client, active_user, logged_in_headers, super_user_headers): +@pytest.mark.api_key_required +async def test_data_consistency_after_update(client: AsyncClient, active_user, logged_in_headers, super_user_headers): user_id = active_user.id update_data = UserUpdate(is_active=False) - response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=super_user_headers) + response = await client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=super_user_headers) assert response.status_code == 200, response.json() # Fetch the updated user from the database - response = client.get("/api/v1/users/whoami", headers=logged_in_headers) + response = await client.get("api/v1/users/whoami", headers=logged_in_headers) assert response.status_code == 401, response.json() assert response.json()["detail"] == "User not found or is inactive." -def test_data_consistency_after_delete(client, test_user, super_user_headers): +@pytest.mark.api_key_required +async def test_data_consistency_after_delete(client: AsyncClient, test_user, super_user_headers): user_id = test_user.get("id") - response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers) + response = await client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers) assert response.status_code == 200, response.json() # Attempt to fetch the deleted user from the database - response = client.get("/api/v1/users", headers=super_user_headers) + response = await client.get("api/v1/users/", headers=super_user_headers) assert response.status_code == 200 assert all(user["id"] != user_id for user in response.json()["users"]) -def test_inactive_user(client): +@pytest.mark.api_key_required +async def test_inactive_user(client: AsyncClient): # Create a user that is not active and has a last_login_at value with session_getter(get_db_service()) as session: user = User( @@ -123,12 +127,13 @@ def test_inactive_user(client): session.commit() login_data = {"username": "inactiveuser", "password": "testpassword"} - response = client.post("/api/v1/login", data=login_data) + response = await client.post("api/v1/login", data=login_data) assert response.status_code == 401 assert response.json()["detail"] == "Inactive user" -def test_add_user(client, test_user): +@pytest.mark.api_key_required +async def test_add_user(client: AsyncClient, test_user): assert test_user["username"] == "testuser" @@ -136,51 +141,55 @@ def test_add_user(client, test_user): # def test_read_current_user(client: TestClient, active_user): # # First we need to login to get the access token # login_data = {"username": "testuser", "password": "testpassword"} -# response = client.post("/api/v1/login", data=login_data) +# response = await client.post("api/v1/login", data=login_data) # assert response.status_code == 200 # headers = {"Authorization": f"Bearer {response.json()['access_token']}"} -# response = client.get("/api/v1/user", headers=headers) +# response = await client.get("api/v1/user", headers=headers) # assert response.status_code == 200, response.json() # assert response.json()["username"] == "testuser" -def test_read_all_users(client, super_user_headers): - response = client.get("/api/v1/users", headers=super_user_headers) +@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) assert response.status_code == 200, response.json() assert isinstance(response.json()["users"], list) -def test_normal_user_cant_read_all_users(client, logged_in_headers): - response = client.get("/api/v1/users", headers=logged_in_headers) +@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) assert response.status_code == 403, response.json() assert response.json() == {"detail": "The user doesn't have enough privileges"} -def test_patch_user(client, active_user, logged_in_headers): +@pytest.mark.api_key_required +async def test_patch_user(client: AsyncClient, active_user, logged_in_headers): user_id = active_user.id update_data = UserUpdate( username="newname", ) - response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers) + response = await client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers) assert response.status_code == 200, response.json() update_data = UserUpdate( profile_image="new_image", ) - response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers) + response = await client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers) assert response.status_code == 200, response.json() -def test_patch_reset_password(client, active_user, logged_in_headers): +@pytest.mark.api_key_required +async def test_patch_reset_password(client: AsyncClient, active_user, logged_in_headers): user_id = active_user.id update_data = UserUpdate( password="newpassword", ) - response = client.patch( + response = await client.patch( f"/api/v1/users/{user_id}/reset-password", json=update_data.model_dump(), headers=logged_in_headers, @@ -188,17 +197,18 @@ def test_patch_reset_password(client, active_user, logged_in_headers): 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 = client.post("/api/v1/login", data=login_data) + response = await client.post("api/v1/login", data=login_data) assert response.status_code == 200 -def test_patch_user_wrong_id(client, active_user, logged_in_headers): +@pytest.mark.api_key_required +async def test_patch_user_wrong_id(client: AsyncClient, active_user, logged_in_headers): user_id = "wrong_id" update_data = UserUpdate( username="newname", ) - response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers) + response = await client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers) assert response.status_code == 422, response.json() json_response = response.json() detail = json_response["detail"] @@ -207,16 +217,18 @@ def test_patch_user_wrong_id(client, active_user, logged_in_headers): assert error["type"] == "uuid_parsing" -def test_delete_user(client, test_user, super_user_headers): +@pytest.mark.api_key_required +async def test_delete_user(client: AsyncClient, test_user, super_user_headers): user_id = test_user["id"] - response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers) + response = await client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers) assert response.status_code == 200 assert response.json() == {"detail": "User deleted"} -def test_delete_user_wrong_id(client, test_user, super_user_headers): +@pytest.mark.api_key_required +async def test_delete_user_wrong_id(client: AsyncClient, test_user, super_user_headers): user_id = "wrong_id" - response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers) + response = await client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers) assert response.status_code == 422 json_response = response.json() detail = json_response["detail"] @@ -225,8 +237,9 @@ def test_delete_user_wrong_id(client, test_user, super_user_headers): assert error["type"] == "uuid_parsing" -def test_normal_user_cant_delete_user(client, test_user, logged_in_headers): +@pytest.mark.api_key_required +async def test_normal_user_cant_delete_user(client: AsyncClient, test_user, logged_in_headers): user_id = test_user["id"] - response = client.delete(f"/api/v1/users/{user_id}", headers=logged_in_headers) + response = await client.delete(f"/api/v1/users/{user_id}", headers=logged_in_headers) assert response.status_code == 403 assert response.json() == {"detail": "The user doesn't have enough privileges"} diff --git a/src/backend/tests/unit/test_webhook.py b/src/backend/tests/unit/test_webhook.py index ad1286ac3..89a0e427e 100644 --- a/src/backend/tests/unit/test_webhook.py +++ b/src/backend/tests/unit/test_webhook.py @@ -9,7 +9,7 @@ def check_openai_api_key_in_environment_variables(): pass -def test_webhook_endpoint(client, added_webhook_test): +async def test_webhook_endpoint(client, added_webhook_test): # The test is as follows: # 1. The flow when run will get a "path" from the payload and save a file with the path as the name. # We will create a temporary file path and send it to the webhook endpoint, then check if the file exists. @@ -22,7 +22,7 @@ def test_webhook_endpoint(client, added_webhook_test): payload = {"path": str(file_path)} - response = client.post(endpoint, json=payload) + response = await client.post(endpoint, json=payload) assert response.status_code == 202 assert file_path.exists() @@ -30,12 +30,12 @@ def test_webhook_endpoint(client, added_webhook_test): # Send an invalid payload payload = {"invalid_key": "invalid_value"} - response = client.post(endpoint, json=payload) + response = await client.post(endpoint, json=payload) assert response.status_code == 202 assert not file_path.exists() -def test_webhook_flow_on_run_endpoint(client, added_webhook_test, created_api_key): +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" # Just test that "Random Payload" returns 202 @@ -43,16 +43,16 @@ def test_webhook_flow_on_run_endpoint(client, added_webhook_test, created_api_ke payload = { "output_type": "any", } - response = client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=payload) + response = await client.post(endpoint, headers={"x-api-key": created_api_key.api_key}, json=payload) assert response.status_code == 200, response.json() -def test_webhook_with_random_payload(client, added_webhook_test): +async def test_webhook_with_random_payload(client, added_webhook_test): endpoint_name = added_webhook_test["endpoint_name"] endpoint = f"api/v1/webhook/{endpoint_name}" # Just test that "Random Payload" returns 202 # returns 202 - response = client.post( + response = await client.post( endpoint, json="Random Payload", ) diff --git a/uv.lock b/uv.lock index 1b13b19c7..88d2d41ff 100644 --- a/uv.lock +++ b/uv.lock @@ -225,6 +225,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, ] +[[package]] +name = "asgi-lifespan" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/da/e7908b54e0f8043725a990bf625f2041ecf6bfe8eb7b19407f1c00b630f7/asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308", size = 15627 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895 }, +] + [[package]] name = "asgiref" version = "3.8.1" @@ -335,14 +347,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.3.2" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/75/47dbab150ef6f9298e227a40c93c7fed5f3ffb67c9fb62cd49f66285e46e/authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2", size = 147313 } +sdist = { url = "https://files.pythonhosted.org/packages/09/47/df70ecd34fbf86d69833fe4e25bb9ecbaab995c8e49df726dd416f6bb822/authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", size = 146074 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/4c/9aa0416a403d5cc80292cb030bcd2c918cce2755e314d8c1aa18656e1e12/Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc", size = 225111 }, + { url = "https://files.pythonhosted.org/packages/87/1f/bc95e43ffb57c05b8efcc376dd55a0240bf58f47ddf5a0f92452b6457b75/Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377", size = 223827 }, ] [[package]] @@ -2321,6 +2333,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/2b/a6e68d7ea6f4fbc31cce20e354d6cef484da0a9891ee6a3eaf3aa9659d01/grpcio-1.66.1-cp312-cp312-win_amd64.whl", hash = "sha256:b0aa03d240b5539648d996cc60438f128c7f46050989e35b25f5c18286c86734", size = 4275565 }, ] +[[package]] +name = "grpcio-health-checking" +version = "1.62.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/9f/09df9b02fc8eafa3031d878c8a4674a0311293c8c6f1c942cdaeec204126/grpcio-health-checking-1.62.3.tar.gz", hash = "sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93", size = 15640 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4c/ee3173906196b741ac6ba55a9788ba9ebf2cd05f91715a49b6c3bfbb9d73/grpcio_health_checking-1.62.3-py3-none-any.whl", hash = "sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d", size = 18547 }, +] + [[package]] name = "grpcio-status" version = "1.62.3" @@ -2484,7 +2509,7 @@ wheels = [ [[package]] name = "httpx" -version = "0.27.2" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2493,9 +2518,9 @@ dependencies = [ { name = "idna" }, { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, + { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, ] [package.optional-dependencies] @@ -3486,6 +3511,7 @@ local = [ [package.dev-dependencies] dev = [ + { name = "asgi-lifespan" }, { name = "dictdiffer" }, { name = "httpx" }, { name = "ipykernel" }, @@ -3607,6 +3633,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "asgi-lifespan", specifier = ">=2.1.0" }, { name = "dictdiffer", specifier = ">=0.9.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "ipykernel", specifier = ">=6.29.0" }, @@ -3752,6 +3779,11 @@ local = [ { name = "sentence-transformers" }, ] +[package.dev-dependencies] +dev = [ + { name = "asgi-lifespan" }, +] + [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, @@ -3852,6 +3884,9 @@ requires-dist = [ { name = "vulture", marker = "extra == 'dev'", specifier = ">=2.11" }, ] +[package.metadata.requires-dev] +dev = [{ name = "asgi-lifespan", specifier = ">=2.1.0" }] + [[package]] name = "langfuse" version = "2.51.0" @@ -7876,16 +7911,21 @@ wheels = [ [[package]] name = "weaviate-client" -version = "3.26.7" +version = "4.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, + { name = "grpcio" }, + { name = "grpcio-health-checking" }, + { name = "grpcio-tools" }, + { name = "httpx" }, + { name = "pydantic" }, { name = "requests" }, { name = "validators" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/2e/9588bae34c1d67d05ccc07d74a4f5d73cce342b916f79ab3a9114c6607bb/weaviate_client-3.26.7.tar.gz", hash = "sha256:ea538437800abc6edba21acf213accaf8a82065584ee8b914bae4a4ad4ef6b70", size = 210480 } +sdist = { url = "https://files.pythonhosted.org/packages/4f/4d/650831937f25b8e788870b46a693a6e141d9d3d72bfd708ce88b0b01d69f/weaviate_client-4.8.1.tar.gz", hash = "sha256:2756996a2205bb991f258c064fc502011fc78a40e8786cb072208b1d3d7c9932", size = 681877 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/95/fb326052bc1d73cb3c19fcfaf6ebb477f896af68de07eaa1337e27ee57fa/weaviate_client-3.26.7-py3-none-any.whl", hash = "sha256:48b8d4b71df881b4e5e15964d7ac339434338ccee73779e3af7eab698a92083b", size = 120051 }, + { url = "https://files.pythonhosted.org/packages/c8/d8/88610f5aaaffd3d2447fe755b86a8bb06b79472e45ec999baa5040dea9a3/weaviate_client-4.8.1-py3-none-any.whl", hash = "sha256:c16453ebfd9bd4045675f8e50841d1af21aa9af1332f379d0418c4531c03bd44", size = 374526 }, ] [[package]]