tests: update tests to use httpx.AsyncClient (#3984)

* Add async support and dependencies to pyproject.toml files

- Added `asgi-lifespan>=2.1.0` to dependencies.
- Configured `asyncio_mode` and `asyncio_default_fixture_loop_scope` for pytest.
- Updated `tool.uv` section with `asgi-lifespan` in dev-dependencies.

* Convert test fixtures to async and use AsyncClient for HTTP requests

* Handle 'ImportFrom' nodes in AST validation to support module attribute imports

* Convert test cases to use async HTTP client

- Updated test cases in `test_database.py`, `test_endpoints.py`, `test_user.py`, `test_variable.py`, `test_files.py`, `test_chat_endpoint.py`, `test_misc.py`, `test_messages_endpoints.py`, `test_api_key.py`, `test_webhook.py`, and `test_login.py` to use `httpx.AsyncClient` instead of `fastapi.TestClient`.
- Modified test functions to be asynchronous and use `await` for HTTP requests.
- Adjusted fixtures and helper functions to support asynchronous operations.
- Ensured consistency in endpoint paths and request methods across all test cases.

* Refactor string concatenation to f-string in test_chat_endpoint.py

* [autofix.ci] apply automated fixes

* Refactor import validation to use pattern matching for AST nodes

* Set `startup_timeout` and `shutdown_timeout` to `None` in `LifespanManager` for test files.

* Convert test functions to async in `test_messages_endpoints.py`

* Add `api_key_required` marker to assistant component tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-10-02 14:21:52 -03:00 committed by GitHub
commit 6febae599b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 444 additions and 322 deletions

View file

@ -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"
build-backend = "hatchling.build"

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -2,6 +2,7 @@ from pathlib import Path
from tempfile import tempdir
import pytest
from langflow.__main__ import app
from langflow.services import deps

View file

@ -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()

View file

@ -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

View file

@ -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"]

View file

@ -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"

View file

@ -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
)

View file

@ -1,4 +1,5 @@
import pytest
from langflow.processing.process import process_tweaks
from langflow.services.deps import get_session_service

View file

@ -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"}

View file

@ -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",
)

58
uv.lock generated
View file

@ -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]]