langflow/src/backend/tests/unit/test_messages_endpoints.py
Cristhian Zanforlin Lousa 043ba55718
feat: add sessions endpoint with session management enhancements (#8596)
* 📝 (monitor.py): Add endpoint to get sessions and handle session_id encoding for API requests
📝 (use-get-messages-mutation.ts): Implement a mutation function to fetch messages with query parameters and handle session_id encoding for API requests
📝 (use-get-messages-polling.ts): Ensure proper encoding of session_id for API requests in polling mutation
📝 (use-get-messages.ts): Handle session_id encoding for API requests in messages query
📝 (new-modal.tsx): Implement functions to handle session deletion and proper encoding of session_id for API requests
📝 (utils.ts): Add functions to encode, decode, validate, format, and prepare session IDs for API requests

* 📝 (constants.ts): Add SESSIONS constant to API URLs for monitoring sessions
🔧 (use-delete-messages.ts): Add queryClient to UseRequestProcessor to invalidate sessions query
 (use-get-sessions-from-flow.ts): Introduce useGetSessionsFromFlowQuery to fetch sessions from flow
🔧 (use-rename-session.ts): Change refetchQueries to invalidateQueries for useGetSessionsFromFlowQuery
🔧 (custom-new-modal.tsx): Update import path for IOModal to playground-modal
🔧 (session-selector.tsx): Add setActiveSession function to handle setting active session
🔧 (sidebar-open-view.tsx): Add setActiveSession function to handle setting active session
♻️ (new-modal.tsx): Refactor IOModal into playground-modal and update functionality
♻️ (playground-modal.tsx): Refactor IOModal to handle playground-specific functionality
⬆️ (flowStore.ts): Add newChatOnPlayground state and setNewChatOnPlayground function
⬆️ (index.ts): Update FlowStoreType to include newChatOnPlayground and setNewChatOnPlayground

* 🔧 (pyproject.toml): update testpaths to point to the correct directory for tests
 (test_session_endpoint.py): add unit tests for sessions endpoint with flow_id filtering
♻️ (session-selector.tsx): refactor to trim editedSession before setting it
♻️ (sidebar-open-view.tsx): refactor to set visibleSession instead of activeSession

*  (use-get-sessions-from-flow.ts): Always include the flow ID as the default session if it's not already present
♻️ (playground-modal.tsx): Refactor setting sessions to include currentFlowId as the default session if not present, and handle visibility of sessions more efficiently

* ♻️ (use-get-messages-mutation.ts): remove unused imports and refactor code for better readability and maintainability

*  (test_session_endpoint.py): refactor test function names for better clarity and consistency

*  (create-new-session-name.ts): add function to generate a new session name based on the current date and time
🔧 (playground-modal.tsx): import createNewSessionName function to dynamically set a new session name when no session is visible

* [autofix.ci] apply automated fixes

*  (monitor.py): rename get_sessions endpoint to get_message_sessions for clarity and consistency
🔧 (constants.ts): remove unused SESSIONS constant from API URLs
🔧 (use-delete-messages.ts): remove commented out code and unnecessary comments
 (use-delete-sessions.ts): add functionality to delete sessions in frontend
🔧 (use-get-sessions-from-flow.ts): update API endpoint for getting sessions to match backend changes
🔧 (playground-modal.tsx): add functionality to delete sessions and associated messages in the UI, update UI optimistically, and handle errors appropriately

* [autofix.ci] apply automated fixes

* 🐛 (monitor.py): Fix type hinting issue in delete_messages function
📝 (monitor.py): Add comments and improve readability in test_messages_endpoints.py
📝 (session_endpoint.py): Update endpoint paths for consistency and clarity in test_session_endpoint.py

* [autofix.ci] apply automated fixes

* fix: update SQL statement to use col() for session_id filtering in get_message_sessions function

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
2025-06-23 14:11:07 +00:00

237 lines
9.9 KiB
Python

from datetime import datetime, timezone
from urllib.parse import quote
from uuid import UUID
import pytest
from httpx import AsyncClient
from langflow.memory import aadd_messagetables
# Assuming you have these imports available
from langflow.services.database.models.message import MessageCreate, MessageRead, MessageUpdate
from langflow.services.database.models.message.model import MessageTable
from langflow.services.deps import session_scope
@pytest.fixture
async def created_message():
async 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)
messagetables = await aadd_messagetables([messagetable], session)
return MessageRead.model_validate(messagetables[0], from_attributes=True)
@pytest.fixture
async def created_messages(session): # noqa: ARG001
async with session_scope() as _session:
messages = [
MessageCreate(text="Test message 1", sender="User", sender_name="User", session_id="session_id2"),
MessageCreate(text="Test message 2", sender="User", sender_name="User", session_id="session_id2"),
MessageCreate(text="Test message 3", sender="AI", sender_name="AI", session_id="session_id2"),
]
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
return await aadd_messagetables(messagetables, _session)
@pytest.fixture
async def messages_with_datetime_session_id(session): # noqa: ARG001
"""Create messages with datetime-like session IDs that contain characters requiring URL encoding."""
datetime_session_id = "2024-01-15 10:30:45 UTC" # Contains spaces and colons
async with session_scope() as _session:
messages = [
MessageCreate(text="Datetime message 1", sender="User", sender_name="User", session_id=datetime_session_id),
MessageCreate(text="Datetime message 2", sender="AI", sender_name="AI", session_id=datetime_session_id),
]
messagetables = [MessageTable.model_validate(message, from_attributes=True) for message in messages]
created_messages = await aadd_messagetables(messagetables, _session)
return created_messages, datetime_session_id
@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"
@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 = 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
updated_message = MessageRead(**response.json())
assert updated_message.text == "Updated content"
@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 = 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"
@pytest.mark.api_key_required
async def test_delete_messages_session(client: AsyncClient, created_messages, logged_in_headers):
session_id = "session_id2"
response = await client.delete(f"api/v1/monitor/messages/session/{session_id}", headers=logged_in_headers)
assert response.status_code == 204
assert response.reason_phrase == "No Content"
assert len(created_messages) == 3
response = await client.get("api/v1/monitor/messages", headers=logged_in_headers)
assert response.status_code == 200
assert len(response.json()) == 0
# Successfully update session ID for all messages with the old session ID
@pytest.mark.usefixtures("session")
async def test_successfully_update_session_id(client, logged_in_headers, created_messages):
old_session_id = "session_id2"
new_session_id = "new_session_id"
response = await client.patch(
f"api/v1/monitor/messages/session/{old_session_id}",
params={"new_session_id": new_session_id},
headers=logged_in_headers,
)
assert response.status_code == 200, response.text
updated_messages = response.json()
assert len(updated_messages) == len(created_messages)
for message in updated_messages:
assert message["session_id"] == new_session_id
response = await client.get(
"api/v1/monitor/messages", headers=logged_in_headers, params={"session_id": new_session_id}
)
assert response.status_code == 200
assert len(response.json()) == len(created_messages)
messages = response.json()
for message in messages:
assert message["session_id"] == new_session_id
response_timestamp = message["timestamp"]
timestamp = datetime.strptime(response_timestamp, "%Y-%m-%d %H:%M:%S %Z").replace(tzinfo=timezone.utc)
timestamp_str = timestamp.strftime("%Y-%m-%d %H:%M:%S %Z")
assert timestamp_str == response_timestamp
# Check if the messages ordered by timestamp are in the correct order
# User, User, AI
assert messages[0]["sender"] == "User"
assert messages[1]["sender"] == "User"
assert messages[2]["sender"] == "AI"
# No messages found with the given session ID
@pytest.mark.usefixtures("session")
async def test_no_messages_found_with_given_session_id(client, logged_in_headers):
old_session_id = "non_existent_session_id"
new_session_id = "new_session_id"
response = await client.patch(
f"/messages/session/{old_session_id}", params={"new_session_id": new_session_id}, headers=logged_in_headers
)
assert response.status_code == 404, response.text
assert response.json()["detail"] == "Not Found"
# Test for URL-encoded datetime session ID
@pytest.mark.api_key_required
async def test_get_messages_with_url_encoded_datetime_session_id(
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
):
"""Test that URL-encoded datetime session IDs are properly decoded and matched."""
created_messages, datetime_session_id = messages_with_datetime_session_id
# URL encode the datetime session ID (spaces become %20, colons become %3A)
encoded_session_id = quote(datetime_session_id)
# Test with URL-encoded session ID
response = await client.get(
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
messages = response.json()
assert len(messages) == 2
# Verify all messages have the correct (decoded) session ID
for message in messages:
assert message["session_id"] == datetime_session_id
# Verify message content
assert messages[0]["text"] == "Datetime message 1"
assert messages[1]["text"] == "Datetime message 2"
@pytest.mark.api_key_required
async def test_get_messages_with_non_encoded_datetime_session_id(
client: AsyncClient, messages_with_datetime_session_id, logged_in_headers
):
"""Test that non-URL-encoded datetime session IDs also work correctly."""
created_messages, datetime_session_id = messages_with_datetime_session_id
# Test with non-encoded session ID (should still work due to unquote being safe for non-encoded strings)
response = await client.get(
"api/v1/monitor/messages", params={"session_id": datetime_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
messages = response.json()
assert len(messages) == 2
# Verify all messages have the correct session ID
for message in messages:
assert message["session_id"] == datetime_session_id
@pytest.mark.api_key_required
async def test_get_messages_with_various_encoded_characters(client: AsyncClient, logged_in_headers):
"""Test various URL-encoded characters in session IDs."""
# Create a session ID with various special characters
special_session_id = "test+session:2024@domain.com"
async with session_scope() as session:
message = MessageCreate(
text="Special chars message", sender="User", sender_name="User", session_id=special_session_id
)
messagetable = MessageTable.model_validate(message, from_attributes=True)
await aadd_messagetables([messagetable], session)
# URL encode the session ID
encoded_session_id = quote(special_session_id)
# Test with URL-encoded session ID
response = await client.get(
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
messages = response.json()
assert len(messages) == 1
assert messages[0]["session_id"] == special_session_id
assert messages[0]["text"] == "Special chars message"
@pytest.mark.api_key_required
async def test_get_messages_empty_result_with_encoded_nonexistent_session(client: AsyncClient, logged_in_headers):
"""Test that URL-encoded non-existent session IDs return empty results."""
nonexistent_session_id = "2024-12-31 23:59:59 UTC"
encoded_session_id = quote(nonexistent_session_id)
response = await client.get(
"api/v1/monitor/messages", params={"session_id": encoded_session_id}, headers=logged_in_headers
)
assert response.status_code == 200, response.text
messages = response.json()
assert len(messages) == 0