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:
parent
7e3d470845
commit
6febae599b
19 changed files with 444 additions and 322 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from pathlib import Path
|
|||
from tempfile import tempdir
|
||||
|
||||
import pytest
|
||||
|
||||
from langflow.__main__ import app
|
||||
from langflow.services import deps
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from langflow.processing.process import process_tweaks
|
||||
from langflow.services.deps import get_session_service
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
58
uv.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue