feat(api): add special handling for _mcp_servers file uploads and improve tests (#8847)
* update * [autofix.ci] apply automated fixes * refactor: update file query logic and enhance error logging in file deletion * refactor: enhance error handling and improve code clarity in MCP server list retrieval * refactor: improve type hinting and code clarity in MCP servers file tests * fix: handle potential None value for server configuration file in async server list retrieval * Update test_files.py * fix: autofix.ci / Update Starter Projects (pull_request) * chore: Add API key environment variables to CI workflows * Updated .github/workflows/ci.yml and .github/workflows/python_test.yml to include OPENAI_API_KEY, ANTHROPIC_API_KEY, and other relevant API keys as environment variables for CI jobs. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Co-authored-by: Carlos Coelho <80289056+carlosrcoelho@users.noreply.github.com> Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com>
This commit is contained in:
parent
557f7b31aa
commit
a88829a9d6
6 changed files with 239 additions and 47 deletions
|
|
@ -187,7 +187,7 @@ async def test_delete_file(files_client, files_created_api_key):
|
|||
|
||||
response = await files_client.delete(f"api/v2/files/{upload_response['id']}", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "File deleted successfully"}
|
||||
assert response.json() == {"detail": "File test deleted successfully"}
|
||||
|
||||
|
||||
async def test_edit_file(files_client, files_created_api_key):
|
||||
|
|
|
|||
164
src/backend/tests/unit/api/v2/test_mcp_servers_file.py
Normal file
164
src/backend/tests/unit/api/v2/test_mcp_servers_file.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import io
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from fastapi import UploadFile
|
||||
|
||||
# Module under test
|
||||
from langflow.api.v2.files import MCP_SERVERS_FILE, upload_user_file
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langflow.services.database.models.file.model import File as UserFile
|
||||
|
||||
|
||||
class FakeStorageService: # Minimal stub for storage interactions
|
||||
def __init__(self):
|
||||
# key -> bytes
|
||||
self._store: dict[str, bytes] = {}
|
||||
|
||||
async def save_file(self, flow_id: str, file_name: str, data: bytes):
|
||||
self._store[f"{flow_id}/{file_name}"] = data
|
||||
|
||||
async def get_file_size(self, flow_id: str, file_name: str):
|
||||
return len(self._store.get(f"{flow_id}/{file_name}", b""))
|
||||
|
||||
async def delete_file(self, flow_id: str, file_name: str):
|
||||
self._store.pop(f"{flow_id}/{file_name}", None)
|
||||
|
||||
|
||||
class FakeResult: # Helper for Session.exec return
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
|
||||
def first(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
def all(self):
|
||||
return list(self._rows)
|
||||
|
||||
|
||||
class FakeSession: # Minimal async session stub
|
||||
def __init__(self):
|
||||
self._db: dict[str, UserFile] = {}
|
||||
|
||||
async def exec(self, stmt):
|
||||
# Extremely simplified: detect by LIKE pattern or equality against name/id
|
||||
# We only support SELECT UserFile WHERE name LIKE pattern or id equality
|
||||
stmt_str = str(stmt)
|
||||
if "user_file.name" in stmt_str:
|
||||
# LIKE pattern extraction
|
||||
pattern = stmt_str.split("like(")[-1].split(")")[0].strip('"%')
|
||||
rows = [f for name, f in self._db.items() if name.startswith(pattern)]
|
||||
return FakeResult(rows)
|
||||
if "user_file.id" in stmt_str:
|
||||
uid = stmt_str.split("=")[-1].strip().strip("'")
|
||||
rows = [f for f in self._db.values() if str(f.id) == uid]
|
||||
return FakeResult(rows)
|
||||
return FakeResult([])
|
||||
|
||||
def add(self, obj):
|
||||
self._db[obj.name] = obj
|
||||
|
||||
async def commit(self):
|
||||
return
|
||||
|
||||
async def refresh(self, obj): # noqa: ARG002
|
||||
return
|
||||
|
||||
async def delete(self, obj):
|
||||
self._db.pop(obj.name, None)
|
||||
|
||||
async def flush(self):
|
||||
return
|
||||
|
||||
|
||||
class FakeSettings:
|
||||
max_file_size_upload: int = 10 # MB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def current_user():
|
||||
class User(SimpleNamespace):
|
||||
id: str
|
||||
|
||||
return User(id=str(uuid.uuid4()))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def storage_service():
|
||||
return FakeStorageService()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings_service():
|
||||
return SimpleNamespace(settings=FakeSettings())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
return FakeSession()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_servers_upload_replace(session, storage_service, settings_service, current_user):
|
||||
"""Uploading _mcp_servers.json twice should keep single DB record and no rename."""
|
||||
content1 = b'{"mcpServers": {}}'
|
||||
file1 = UploadFile(filename=f"{MCP_SERVERS_FILE}.json", file=io.BytesIO(content1))
|
||||
file1.size = len(content1)
|
||||
|
||||
# First upload
|
||||
await upload_user_file(
|
||||
file=file1,
|
||||
session=session,
|
||||
current_user=current_user,
|
||||
storage_service=storage_service,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
|
||||
# DB should contain single entry named _mcp_servers
|
||||
assert list(session._db.keys()) == [MCP_SERVERS_FILE]
|
||||
|
||||
# Upload again with different content
|
||||
content2 = b'{"mcpServers": {"everything": {}}}'
|
||||
file2 = UploadFile(filename=f"{MCP_SERVERS_FILE}.json", file=io.BytesIO(content2))
|
||||
file2.size = len(content2)
|
||||
|
||||
await upload_user_file(
|
||||
file=file2,
|
||||
session=session,
|
||||
current_user=current_user,
|
||||
storage_service=storage_service,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
|
||||
# Still single record, same name
|
||||
assert list(session._db.keys()) == [MCP_SERVERS_FILE]
|
||||
|
||||
record = session._db[MCP_SERVERS_FILE]
|
||||
# Storage path should match user_id/_mcp_servers.json
|
||||
expected_path = f"{current_user.id}/{MCP_SERVERS_FILE}.json"
|
||||
assert record.path == expected_path
|
||||
|
||||
# Storage should have updated content
|
||||
stored_bytes = storage_service._store[expected_path]
|
||||
assert stored_bytes == content2
|
||||
|
||||
# Third upload with server config provided by user
|
||||
content3 = (
|
||||
b'{"mcpServers": {"everything": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"]}}}'
|
||||
)
|
||||
file3 = UploadFile(filename=f"{MCP_SERVERS_FILE}.json", file=io.BytesIO(content3))
|
||||
file3.size = len(content3)
|
||||
|
||||
await upload_user_file(
|
||||
file=file3,
|
||||
session=session,
|
||||
current_user=current_user,
|
||||
storage_service=storage_service,
|
||||
settings_service=settings_service,
|
||||
)
|
||||
|
||||
stored_bytes = storage_service._store[expected_path]
|
||||
assert stored_bytes == content3
|
||||
Loading…
Add table
Add a link
Reference in a new issue