Files

547 lines
21 KiB
Python
Raw Permalink Normal View History

"""
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import pytest
from datetime import datetime
from uuid import UUID, uuid4
from unittest.mock import patch, MagicMock
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
from app.models.user import User
from app.models.role import Role
from app.models.permission import Permission, role_permissions
from app.models.agent import Agent, AgentType
from app.models.jira import AgentJiraConfig, JiraToken
from app.models.user import UserGroup
from app.core.security import get_password_hash
from app.repositories.jira import JiraRepository
from app.models.schemas.jira import AgentWithJiraConfig
@pytest.fixture
def test_role(db: Session, test_organization_id: UUID) -> Role:
"""Create a test role with required permissions"""
role = Role(
name="Test Role",
organization_id=test_organization_id
)
db.add(role)
db.commit()
# Add required permissions
permission = Permission(
name="manage_jira",
description="Can manage Jira integration"
)
db.add(permission)
db.commit()
# Associate permission with role
db.execute(
role_permissions.insert().values(
role_id=role.id,
permission_id=permission.id
)
)
db.commit()
return role
@pytest.fixture
def test_user(db: Session, test_organization_id: UUID, test_role: Role) -> User:
"""Create a test user with required permissions"""
user = User(
id=uuid4(),
email="test@test.com",
hashed_password=get_password_hash("testpassword"),
organization_id=test_organization_id,
role_id=test_role.id,
is_active=True
)
db.add(user)
db.commit()
db.refresh(user)
return user
@pytest.fixture
def test_user_group(db: Session, test_organization_id: UUID) -> UserGroup:
"""Create a test user group"""
group = UserGroup(
id=uuid4(),
name="Test Group",
organization_id=test_organization_id,
description="Test group for Jira tests"
)
db.add(group)
db.commit()
db.refresh(group)
return group
@pytest.fixture
def test_agent(db: Session, test_organization_id: UUID, test_organization, test_user_group: UserGroup) -> Agent:
"""Create a test agent"""
agent = Agent(
id=uuid4(),
organization_id=test_organization_id,
name="Test Agent",
display_name="Test Agent Display",
description="Test agent description",
instructions="Test instructions",
tools='["jira"]',
agent_type=AgentType.CUSTOMER_SUPPORT,
is_default=False,
is_active=True,
transfer_to_human=False,
ask_for_rating=True
)
db.add(agent)
db.commit()
db.refresh(agent)
# Set up relationships manually since they might not be auto-loaded
agent.organization = test_organization
agent.groups = [test_user_group]
return agent
@pytest.fixture
def test_agent_jira_config(db: Session, test_agent: Agent) -> AgentJiraConfig:
"""Create a test agent Jira configuration"""
config = AgentJiraConfig(
agent_id=str(test_agent.id),
enabled=True,
project_key="TEST",
issue_type_id="10001"
)
db.add(config)
db.commit()
db.refresh(config)
return config
@pytest.fixture
def test_jira_token(db: Session, test_organization_id: UUID) -> JiraToken:
"""Create a test Jira token"""
token = JiraToken(
organization_id=test_organization_id,
access_token="test_access_token",
refresh_token="test_refresh_token",
token_type="Bearer",
expires_at=datetime.utcnow(),
cloud_id="test_cloud_id",
site_url="https://test.atlassian.net"
)
db.add(token)
db.commit()
db.refresh(token)
return token
@pytest.fixture
def jira_repo(db):
"""Create a Jira repository instance"""
return JiraRepository(db)
class TestJiraRepository:
"""Test Jira repository functionality"""
def test_get_agent_with_jira_config_success(self, jira_repo, test_agent, test_agent_jira_config):
"""Test successfully retrieving agent with Jira configuration"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert isinstance(result, AgentWithJiraConfig)
assert result.id == test_agent.id
assert result.name == test_agent.name
assert result.display_name == test_agent.display_name
assert result.description == test_agent.description
assert result.instructions == test_agent.instructions
assert result.tools == ["jira"] # Parsed from JSON string
assert result.agent_type == test_agent.agent_type
assert result.is_default == test_agent.is_default
assert result.is_active == test_agent.is_active
assert result.organization_id == test_agent.organization_id
assert result.transfer_to_human == test_agent.transfer_to_human
assert result.ask_for_rating == test_agent.ask_for_rating
# Check Jira-specific fields
assert result.jira_enabled is True
assert result.jira_project_key == "TEST"
assert result.jira_issue_type_id == "10001"
def test_get_agent_with_jira_config_uuid_input(self, jira_repo, test_agent, test_agent_jira_config):
"""Test retrieving agent with UUID input"""
result = jira_repo.get_agent_with_jira_config(test_agent.id)
assert result is not None
assert result.id == test_agent.id
assert result.jira_enabled is True
def test_get_agent_with_jira_config_no_jira_config(self, jira_repo, test_agent):
"""Test retrieving agent without Jira configuration"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert isinstance(result, AgentWithJiraConfig)
assert result.id == test_agent.id
assert result.name == test_agent.name
# Check default Jira fields
assert result.jira_enabled is False
assert result.jira_project_key is None
assert result.jira_issue_type_id is None
def test_get_agent_with_jira_config_nonexistent_agent(self, jira_repo):
"""Test retrieving nonexistent agent"""
result = jira_repo.get_agent_with_jira_config(str(uuid4()))
assert result is None
def test_get_agent_with_jira_config_invalid_uuid(self, jira_repo):
"""Test retrieving agent with invalid UUID string"""
result = jira_repo.get_agent_with_jira_config("invalid-uuid")
assert result is None
def test_get_agent_with_jira_config_empty_string(self, jira_repo):
"""Test retrieving agent with empty string"""
result = jira_repo.get_agent_with_jira_config("")
assert result is None
def test_get_agent_with_jira_config_none_input(self, jira_repo):
"""Test retrieving agent with None input"""
result = jira_repo.get_agent_with_jira_config(None)
assert result is None
@patch('app.repositories.jira.logger')
def test_get_agent_with_jira_config_database_error(self, mock_logger, jira_repo):
"""Test agent retrieval with database error"""
with patch.object(jira_repo.db, 'query', side_effect=SQLAlchemyError("DB Error")):
result = jira_repo.get_agent_with_jira_config(str(uuid4()))
assert result is None
mock_logger.error.assert_called()
@patch('app.repositories.jira.logger')
def test_get_agent_with_jira_config_uuid_conversion_error(self, mock_logger, jira_repo):
"""Test agent retrieval with UUID conversion error"""
result = jira_repo.get_agent_with_jira_config("not-a-valid-uuid")
assert result is None
mock_logger.error.assert_called()
def test_get_agent_with_jira_config_disabled_jira(self, jira_repo, test_agent, db):
"""Test retrieving agent with disabled Jira configuration"""
# Create disabled Jira config
config = AgentJiraConfig(
agent_id=str(test_agent.id),
enabled=False,
project_key="TEST",
issue_type_id="10001"
)
db.add(config)
db.commit()
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert result.jira_enabled is False
assert result.jira_project_key == "TEST" # Still populated even if disabled
assert result.jira_issue_type_id == "10001"
def test_get_agent_with_jira_config_partial_jira_config(self, jira_repo, test_agent, db):
"""Test retrieving agent with partial Jira configuration"""
# Create config with only some fields
config = AgentJiraConfig(
agent_id=str(test_agent.id),
enabled=True,
project_key="TEST",
issue_type_id=None # Missing issue type
)
db.add(config)
db.commit()
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert result.jira_enabled is True
assert result.jira_project_key == "TEST"
assert result.jira_issue_type_id is None
def test_get_agent_with_jira_config_relationships_loaded(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that agent relationships are properly loaded"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
# Check that relationships are included (even if empty lists)
assert hasattr(result, 'groups')
assert hasattr(result, 'organization')
assert hasattr(result, 'knowledge')
assert result.knowledge == [] # Default empty list
def test_get_agent_with_jira_config_multiple_configs(self, jira_repo, test_agent, db):
"""Test retrieving agent when multiple Jira configs exist (should get first)"""
# Create multiple configs (this shouldn't happen in practice, but test edge case)
config1 = AgentJiraConfig(
agent_id=str(test_agent.id),
enabled=True,
project_key="TEST1",
issue_type_id="10001"
)
config2 = AgentJiraConfig(
agent_id=str(test_agent.id),
enabled=False,
project_key="TEST2",
issue_type_id="10002"
)
db.add(config1)
db.add(config2)
db.commit()
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
# Should get the first config found
assert result.jira_enabled is True
assert result.jira_project_key == "TEST1"
assert result.jira_issue_type_id == "10001"
def test_get_agent_with_jira_config_string_agent_id_in_config(self, jira_repo, test_agent, db):
"""Test that agent_id is properly converted to string for Jira config lookup"""
# Create config with string agent_id
config = AgentJiraConfig(
agent_id=str(test_agent.id), # Explicitly string
enabled=True,
project_key="TEST",
issue_type_id="10001"
)
db.add(config)
db.commit()
# Test with both string and UUID inputs
result1 = jira_repo.get_agent_with_jira_config(str(test_agent.id))
result2 = jira_repo.get_agent_with_jira_config(test_agent.id)
assert result1 is not None
assert result2 is not None
assert result1.jira_enabled is True
assert result2.jira_enabled is True
@patch('app.repositories.jira.logger')
def test_get_agent_with_jira_config_exception_in_agent_data_creation(self, mock_logger, jira_repo, test_agent, db):
"""Test exception handling during AgentWithJiraConfig creation"""
# Create a mock agent that will cause an exception during AgentWithJiraConfig creation
with patch('app.repositories.jira.AgentWithJiraConfig', side_effect=Exception("Schema Error")):
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is None
mock_logger.error.assert_called()
def test_get_agent_with_jira_config_all_agent_fields_mapped(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that all agent fields are properly mapped to AgentWithJiraConfig"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
# Verify all agent fields are mapped
agent_fields = [
'id', 'name', 'display_name', 'description', 'instructions',
'agent_type', 'is_default', 'is_active',
'organization_id', 'transfer_to_human', 'ask_for_rating'
]
for field in agent_fields:
assert hasattr(result, field)
assert getattr(result, field) == getattr(test_agent, field)
# Special check for tools field (parsed from JSON)
assert hasattr(result, 'tools')
assert result.tools == ["jira"] # Parsed from JSON string
def test_get_agent_with_jira_config_jira_fields_default_values(self, jira_repo, test_agent):
"""Test that Jira fields have correct default values when no config exists"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert result.jira_enabled is False
assert result.jira_project_key is None
assert result.jira_issue_type_id is None
def test_get_agent_with_jira_config_knowledge_field_default(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that knowledge field is set to empty list by default"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert result.knowledge == []
assert isinstance(result.knowledge, list)
class TestJiraRepositoryErrorHandling:
"""Test error handling and edge cases"""
@patch('app.repositories.jira.logger')
def test_database_connection_error(self, mock_logger, jira_repo):
"""Test handling of database connection errors"""
with patch.object(jira_repo.db, 'query', side_effect=SQLAlchemyError("Connection lost")):
result = jira_repo.get_agent_with_jira_config(str(uuid4()))
assert result is None
mock_logger.error.assert_called_with("Error getting agent with Jira config: Connection lost")
@patch('app.repositories.jira.logger')
def test_invalid_uuid_format(self, mock_logger, jira_repo):
"""Test handling of various invalid UUID formats"""
invalid_uuids = [
"not-a-uuid",
"12345",
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"123e4567-e89b-12d3-a456-42661417400", # Too short
"123e4567-e89b-12d3-a456-426614174000x", # Too long
]
for invalid_uuid in invalid_uuids:
result = jira_repo.get_agent_with_jira_config(invalid_uuid)
assert result is None
# Should have logged errors for each invalid UUID
assert mock_logger.error.call_count >= len(invalid_uuids)
def test_empty_and_whitespace_inputs(self, jira_repo):
"""Test handling of empty and whitespace-only inputs"""
inputs = ["", " ", "\t", "\n", None]
for input_val in inputs:
result = jira_repo.get_agent_with_jira_config(input_val)
assert result is None
@patch('app.repositories.jira.logger')
def test_query_timeout_simulation(self, mock_logger, jira_repo):
"""Test handling of query timeouts"""
with patch.object(jira_repo.db, 'query', side_effect=SQLAlchemyError("Query timeout")):
result = jira_repo.get_agent_with_jira_config(str(uuid4()))
assert result is None
mock_logger.error.assert_called()
class TestJiraRepositoryPerformance:
"""Test performance-related aspects"""
def test_single_query_optimization(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that the method uses optimized queries with proper joins"""
with patch.object(jira_repo.db, 'query') as mock_query:
# Set up the mock chain
mock_query_result = MagicMock()
mock_query_result.options.return_value.filter.return_value.first.return_value = test_agent
mock_query.return_value = mock_query_result
# Also mock the Jira config query
mock_jira_query = MagicMock()
mock_jira_query.filter.return_value.first.return_value = test_agent_jira_config
# Set up side effect to return different mocks for different queries
def query_side_effect(model):
if model == Agent:
return mock_query_result
elif model == AgentJiraConfig:
return mock_jira_query
return MagicMock()
mock_query.side_effect = query_side_effect
jira_repo.get_agent_with_jira_config(str(test_agent.id))
# Verify that query was called (optimization check)
assert mock_query.called
# Verify that joinedload was used for relationships
mock_query_result.options.assert_called()
def test_minimal_data_transfer(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that only necessary data is loaded and transferred"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
# Verify that the result contains only the expected fields
expected_fields = {
'id', 'name', 'display_name', 'description', 'instructions',
'tools', 'agent_type', 'is_default', 'is_active', 'organization_id',
'transfer_to_human', 'ask_for_rating', 'groups', 'organization',
'knowledge', 'jira_enabled', 'jira_project_key', 'jira_issue_type_id'
}
result_dict = result.dict()
result_fields = set(result_dict.keys())
# All expected fields should be present
assert expected_fields.issubset(result_fields)
class TestJiraRepositoryDataIntegrity:
"""Test data integrity and consistency"""
def test_agent_data_consistency(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that agent data remains consistent between source and result"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
# Test all scalar fields for exact match
scalar_fields = [
'id', 'name', 'display_name', 'description', 'instructions',
'agent_type', 'is_default', 'is_active', 'organization_id',
'transfer_to_human', 'ask_for_rating'
]
for field in scalar_fields:
assert getattr(result, field) == getattr(test_agent, field), f"Field {field} mismatch"
def test_jira_config_data_consistency(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that Jira config data is consistently mapped"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert result.jira_enabled == test_agent_jira_config.enabled
assert result.jira_project_key == test_agent_jira_config.project_key
assert result.jira_issue_type_id == test_agent_jira_config.issue_type_id
def test_tools_list_integrity(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that tools list is properly handled"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert result.tools == ["jira"] # Parsed from JSON string
assert isinstance(result.tools, list)
def test_boolean_field_integrity(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that boolean fields maintain their types and values"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
# Test boolean fields
boolean_fields = ['is_default', 'is_active', 'transfer_to_human', 'ask_for_rating', 'jira_enabled']
for field in boolean_fields:
value = getattr(result, field)
assert isinstance(value, bool), f"Field {field} should be boolean, got {type(value)}"
def test_uuid_field_integrity(self, jira_repo, test_agent, test_agent_jira_config):
"""Test that UUID fields maintain their types"""
result = jira_repo.get_agent_with_jira_config(str(test_agent.id))
assert result is not None
assert isinstance(result.id, UUID)
assert isinstance(result.organization_id, UUID)
assert result.id == test_agent.id
assert result.organization_id == test_agent.organization_id