""" 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