SurfSense/surfsense_backend/tests/integration/test_trello_integration.py
Mandar77 e20e9fa57a feat: add comprehensive Trello connector with full test suite
- Add TrelloConnector class with API integration for boards, cards, and comments
- Implement Trello indexing task for background document processing
- Add TRELLO_CONNECTOR enum to DocumentType and SearchSourceConnectorType
- Create /trello/boards/ API endpoint for board fetching
- Add TrelloCredentialsRequest Pydantic model for API validation
- Implement Trello search functionality in ConnectorService
- Add comprehensive test suite with 80+ test cases covering:
  * Unit tests for TrelloConnector class with error handling
  * Integration tests for API endpoints and database operations
  * Frontend component tests for configuration and creation pages
  * End-to-end workflow testing
- Add test configuration with shared fixtures and mock data
- Create test runner script and comprehensive documentation
- Fix missing TRELLO_CONNECTOR in SearchSourceConnectorType enum
- Add frontend TrelloBoard interface and connector page
- Implement complete connector creation flow with board selection
- Add robust error handling for API failures, timeouts, and malformed data
- Include security considerations for credential handling and input validation
2025-09-19 12:12:51 -07:00

345 lines
14 KiB
Python

"""
Integration tests for Trello connector functionality.
Tests the complete flow from API endpoints to database operations.
"""
import pytest
import asyncio
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import AsyncSession
from app.main import app
from app.db import SearchSourceConnector, Document, DocumentType, SearchSourceConnectorType
from app.connectors.trello_connector import TrelloConnector
class TestTrelloIntegration:
"""Integration tests for Trello connector."""
@pytest.fixture
def client(self):
"""Create test client."""
return TestClient(app)
@pytest.fixture
def mock_user(self):
"""Create mock user."""
user = MagicMock()
user.id = "test_user_id"
return user
@pytest.fixture
def sample_trello_boards(self):
"""Sample Trello boards data."""
return [
{"id": "board1", "name": "Project Board"},
{"id": "board2", "name": "Personal Tasks"},
]
@pytest.fixture
def sample_trello_cards(self):
"""Sample Trello cards data."""
return [
{
"id": "card1",
"name": "Task 1",
"desc": "Description of task 1",
"url": "https://trello.com/c/card1",
},
{
"id": "card2",
"name": "Task 2",
"desc": "Description of task 2",
"url": "https://trello.com/c/card2",
},
]
@pytest.fixture
def sample_card_details(self):
"""Sample card details with comments."""
return {
"id": "card1",
"name": "Task 1",
"desc": "Description of task 1",
"url": "https://trello.com/c/card1",
"comments": ["This is a comment", "Another comment"],
}
def test_trello_boards_endpoint_success(self, client, mock_user, sample_trello_boards):
"""Test successful fetching of Trello boards via API."""
with patch("app.routes.search_source_connectors_routes.current_active_user", return_value=mock_user):
with patch("app.routes.search_source_connectors_routes.TrelloConnector") as mock_trello_class:
mock_trello_instance = MagicMock()
mock_trello_instance.get_user_boards.return_value = sample_trello_boards
mock_trello_class.return_value = mock_trello_instance
response = client.post(
"/trello/boards/",
json={
"trello_api_key": "test_key",
"trello_api_token": "test_token",
}
)
assert response.status_code == 200
data = response.json()
assert len(data) == 2
assert data[0]["name"] == "Project Board"
assert data[1]["name"] == "Personal Tasks"
def test_trello_boards_endpoint_invalid_credentials(self, client, mock_user):
"""Test Trello boards endpoint with invalid credentials."""
with patch("app.routes.search_source_connectors_routes.current_active_user", return_value=mock_user):
with patch("app.routes.search_source_connectors_routes.TrelloConnector") as mock_trello_class:
mock_trello_class.side_effect = ValueError("Invalid credentials")
response = client.post(
"/trello/boards/",
json={
"trello_api_key": "invalid_key",
"trello_api_token": "invalid_token",
}
)
assert response.status_code == 400
assert "Invalid Trello credentials" in response.json()["detail"]
def test_trello_connector_creation_flow(self, client, mock_user, sample_trello_boards):
"""Test complete flow of creating a Trello connector."""
with patch("app.routes.search_source_connectors_routes.current_active_user", return_value=mock_user):
with patch("app.routes.search_source_connectors_routes.TrelloConnector") as mock_trello_class:
mock_trello_instance = MagicMock()
mock_trello_instance.get_user_boards.return_value = sample_trello_boards
mock_trello_class.return_value = mock_trello_instance
# First, fetch boards
boards_response = client.post(
"/trello/boards/",
json={
"trello_api_key": "test_key",
"trello_api_token": "test_token",
}
)
assert boards_response.status_code == 200
# Then create connector (this would be done by the frontend)
# We're testing the integration, so we'll mock the connector creation
with patch("app.routes.search_source_connectors_routes.create_search_source_connector") as mock_create:
mock_create.return_value = {"id": 1, "name": "Test Trello Connector"}
connector_response = client.post(
"/search-source-connectors/",
json={
"name": "Test Trello Connector",
"connector_type": "TRELLO_CONNECTOR",
"config": {
"TRELLO_API_KEY": "test_key",
"TRELLO_API_TOKEN": "test_token",
"board_ids": ["board1", "board2"],
},
"is_indexable": True,
}
)
# This would depend on the actual implementation
# For now, we're just testing that the flow works
assert mock_trello_class.called
assert mock_trello_instance.get_user_boards.called
@pytest.mark.asyncio
async def test_trello_indexing_integration(self, sample_trello_cards, sample_card_details):
"""Test the complete indexing flow."""
from app.tasks.connector_indexers.trello_indexer import index_trello_boards
# Mock database session
mock_session = MagicMock(spec=AsyncSession)
mock_connector = MagicMock()
mock_connector.id = 1
mock_connector.config = {
"TRELLO_API_KEY": "test_key",
"TRELLO_API_TOKEN": "test_token",
"board_ids": ["board1"],
}
mock_session.execute.return_value.scalars.return_value.first.return_value = mock_connector
# Mock TrelloConnector
with patch("app.tasks.connector_indexers.trello_indexer.TrelloConnector") as mock_trello_class:
mock_trello_instance = MagicMock()
mock_trello_instance.get_board_data.return_value = sample_trello_cards
mock_trello_instance.get_card_details.return_value = sample_card_details
mock_trello_class.return_value = mock_trello_instance
# Run indexing
result = await index_trello_boards(
session=mock_session,
connector_id=1,
search_space_id=1,
user_id="test_user",
start_date="2023-01-01",
end_date="2023-12-31",
)
# Verify results
assert result[0] == 2 # 2 documents processed
assert result[1] is None # No error
# Verify database operations
assert mock_session.add.call_count == 2
assert mock_session.commit.called
# Verify Trello API calls
mock_trello_instance.get_board_data.assert_called_once_with("board1")
assert mock_trello_instance.get_card_details.call_count == 2
def test_trello_connector_enum_values(self):
"""Test that Trello connector enum values are properly defined."""
from app.db import DocumentType, SearchSourceConnectorType
assert DocumentType.TRELLO_CONNECTOR == "TRELLO_CONNECTOR"
assert SearchSourceConnectorType.TRELLO_CONNECTOR == "TRELLO_CONNECTOR"
def test_trello_connector_initialization(self):
"""Test TrelloConnector initialization with valid credentials."""
connector = TrelloConnector(api_key="test_key", token="test_token")
assert connector.api_key == "test_key"
assert connector.token == "test_token"
assert connector.auth_params == {"key": "test_key", "token": "test_token"}
def test_trello_connector_initialization_invalid(self):
"""Test TrelloConnector initialization with invalid credentials."""
with pytest.raises(ValueError, match="Trello API key and token cannot be empty"):
TrelloConnector(api_key="", token="test_token")
with pytest.raises(ValueError, match="Trello API key and token cannot be empty"):
TrelloConnector(api_key="test_key", token="")
@patch("requests.get")
def test_trello_connector_get_user_boards(self, mock_get, sample_trello_boards):
"""Test TrelloConnector.get_user_boards method."""
mock_response = MagicMock()
mock_response.json.return_value = sample_trello_boards
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
connector = TrelloConnector(api_key="test_key", token="test_token")
boards = connector.get_user_boards()
assert len(boards) == 2
assert boards[0]["name"] == "Project Board"
assert boards[1]["name"] == "Personal Tasks"
@patch("requests.get")
def test_trello_connector_get_board_data(self, mock_get, sample_trello_cards):
"""Test TrelloConnector.get_board_data method."""
mock_response = MagicMock()
mock_response.json.return_value = sample_trello_cards
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
connector = TrelloConnector(api_key="test_key", token="test_token")
cards = connector.get_board_data("board1")
assert len(cards) == 2
assert cards[0]["name"] == "Task 1"
assert cards[1]["name"] == "Task 2"
@patch("requests.get")
def test_trello_connector_get_card_details(self, mock_get, sample_card_details):
"""Test TrelloConnector.get_card_details method."""
mock_card_response = MagicMock()
mock_card_response.json.return_value = {
"id": "card1",
"name": "Task 1",
"desc": "Description of task 1",
"url": "https://trello.com/c/card1",
}
mock_card_response.raise_for_status.return_value = None
mock_comments_response = MagicMock()
mock_comments_response.json.return_value = [
{"data": {"text": "This is a comment"}},
{"data": {"text": "Another comment"}},
]
mock_comments_response.raise_for_status.return_value = None
mock_get.side_effect = [mock_card_response, mock_comments_response]
connector = TrelloConnector(api_key="test_key", token="test_token")
card_details = connector.get_card_details("card1")
assert card_details["name"] == "Task 1"
assert card_details["desc"] == "Description of task 1"
assert len(card_details["comments"]) == 2
assert card_details["comments"][0] == "This is a comment"
def test_trello_credentials_request_model(self):
"""Test TrelloCredentialsRequest Pydantic model."""
from app.routes.search_source_connectors_routes import TrelloCredentialsRequest
# Valid request
request = TrelloCredentialsRequest(
trello_api_key="test_key",
trello_api_token="test_token"
)
assert request.trello_api_key == "test_key"
assert request.trello_api_token == "test_token"
# Invalid request - missing fields
with pytest.raises(ValueError):
TrelloCredentialsRequest(trello_api_key="test_key")
with pytest.raises(ValueError):
TrelloCredentialsRequest(trello_api_token="test_token")
@pytest.mark.asyncio
async def test_trello_indexing_error_handling(self):
"""Test error handling in Trello indexing."""
from app.tasks.connector_indexers.trello_indexer import index_trello_boards
# Mock database session with no connector
mock_session = MagicMock(spec=AsyncSession)
mock_session.execute.return_value.scalars.return_value.first.return_value = None
result = await index_trello_boards(
session=mock_session,
connector_id=999, # Non-existent connector
search_space_id=1,
user_id="test_user",
start_date="2023-01-01",
end_date="2023-12-31",
)
assert result[0] == 0 # No documents processed
assert result[1] == "Connector not found."
@pytest.mark.asyncio
async def test_trello_indexing_invalid_config(self):
"""Test Trello indexing with invalid configuration."""
from app.tasks.connector_indexers.trello_indexer import index_trello_boards
# Mock database session with invalid config
mock_session = MagicMock(spec=AsyncSession)
mock_connector = MagicMock()
mock_connector.id = 1
mock_connector.config = {"TRELLO_API_KEY": "test_key"} # Missing token and board_ids
mock_session.execute.return_value.scalars.return_value.first.return_value = mock_connector
result = await index_trello_boards(
session=mock_session,
connector_id=1,
search_space_id=1,
user_id="test_user",
start_date="2023-01-01",
end_date="2023-12-31",
)
assert result[0] == 0 # No documents processed
assert result[1] == "Invalid Trello connector configuration."
if __name__ == "__main__":
pytest.main([__file__])