""" Test Suite for Phase 2: Helper Functions Tests the helper functions in app/bot/helpers.py: - get_active_company_or_prompt() - search_companies_by_name() - create_company_selection_keyboard() - format_company_context_footer() """ import pytest from unittest.mock import AsyncMock, MagicMock, patch from telegram import Update, Message, Chat, User, InlineKeyboardMarkup from app.bot.helpers import ( get_active_company_or_prompt, search_companies_by_name, create_company_selection_keyboard, format_company_context_footer ) from app.agent.session import ConversationSession, SessionManager class TestGetActiveCompanyOrPrompt: """Test get_active_company_or_prompt function.""" @pytest.mark.asyncio async def test_returns_company_when_set(self): """Test that function returns company dict when active company is set.""" # Mock Update mock_update = MagicMock(spec=Update) mock_update.message = MagicMock(spec=Message) # Mock SessionManager with active company mock_session_manager = MagicMock(spec=SessionManager) mock_session = MagicMock(spec=ConversationSession) mock_session.get_active_company.return_value = { "id": 42, "name": "ACME SRL", "cui": "RO12345678" } mock_session_manager.get_or_create_session = AsyncMock(return_value=mock_session) # Call function result = await get_active_company_or_prompt( update=mock_update, session_manager=mock_session_manager, telegram_user_id=123456 ) # Verify assert result is not None assert result["id"] == 42 assert result["name"] == "ACME SRL" assert result["cui"] == "RO12345678" # Verify no message was sent mock_update.message.reply_text.assert_not_called() @pytest.mark.asyncio async def test_returns_none_when_no_company(self): """Test that function returns None when no company set.""" # Mock Update with reply_text capability mock_update = MagicMock(spec=Update) mock_update.message = MagicMock(spec=Message) mock_update.message.reply_text = AsyncMock() # Mock SessionManager with NO active company mock_session_manager = MagicMock(spec=SessionManager) mock_session = MagicMock(spec=ConversationSession) mock_session.get_active_company.return_value = None mock_session_manager.get_or_create_session = AsyncMock(return_value=mock_session) # Need to mock the auth and client calls too with patch('app.auth.linking.get_user_auth_data', new_callable=AsyncMock) as mock_auth, \ patch('app.bot.helpers.get_backend_client') as mock_get_client: mock_auth.return_value = {'jwt_token': 'fake-token'} mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_user_companies = AsyncMock(return_value=[ {"id": 1, "nume_firma": "Test Co", "cui": "RO123"} ]) mock_get_client.return_value = mock_client # Call function result = await get_active_company_or_prompt( update=mock_update, session_manager=mock_session_manager, telegram_user_id=123456 ) # Verify returns None (user needs to select company) assert result is None class TestSearchCompaniesByName: """Test search_companies_by_name function.""" @pytest.mark.asyncio async def test_case_insensitive_search(self): """Test that search is case-insensitive.""" mock_companies = [ {"id": 1, "nume_firma": "ACME Corporation SRL", "cui": "RO123"}, {"id": 2, "nume_firma": "Beta Industries", "cui": "RO456"}, {"id": 3, "nume_firma": "Gamma Solutions SRL", "cui": "RO789"} ] with patch('app.bot.helpers.get_backend_client') as mock_get_client: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_user_companies = AsyncMock(return_value=mock_companies) mock_get_client.return_value = mock_client # Search with lowercase results = await search_companies_by_name("acme", "fake-token") assert len(results) == 1 assert results[0]["nume_firma"] == "ACME Corporation SRL" @pytest.mark.asyncio async def test_partial_match_search(self): """Test that search finds partial matches.""" mock_companies = [ {"id": 1, "nume_firma": "ACME Corporation SRL", "cui": "RO123"}, {"id": 2, "nume_firma": "Beta ACME Industries", "cui": "RO456"}, {"id": 3, "nume_firma": "Gamma Solutions SRL", "cui": "RO789"} ] with patch('app.bot.helpers.get_backend_client') as mock_get_client: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_user_companies = AsyncMock(return_value=mock_companies) mock_get_client.return_value = mock_client # Search for partial string results = await search_companies_by_name("acme", "fake-token") assert len(results) == 2 assert any("ACME Corporation" in c["nume_firma"] for c in results) assert any("Beta ACME" in c["nume_firma"] for c in results) @pytest.mark.asyncio async def test_no_matches_returns_empty_list(self): """Test that no matches returns empty list.""" mock_companies = [ {"id": 1, "nume_firma": "ACME Corporation SRL", "cui": "RO123"}, {"id": 2, "nume_firma": "Beta Industries", "cui": "RO456"} ] with patch('app.bot.helpers.get_backend_client') as mock_get_client: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_user_companies = AsyncMock(return_value=mock_companies) mock_get_client.return_value = mock_client # Search for non-existent company results = await search_companies_by_name("xyz", "fake-token") assert len(results) == 0 assert results == [] @pytest.mark.asyncio async def test_search_with_special_characters(self): """Test search handles company names with special characters.""" mock_companies = [ {"id": 1, "nume_firma": "ACME & Partners SRL", "cui": "RO123"}, {"id": 2, "nume_firma": "Beta-Gamma Industries", "cui": "RO456"} ] with patch('app.bot.helpers.get_backend_client') as mock_get_client: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_user_companies = AsyncMock(return_value=mock_companies) mock_get_client.return_value = mock_client # Search with special char results = await search_companies_by_name("beta-gamma", "fake-token") assert len(results) == 1 assert results[0]["nume_firma"] == "Beta-Gamma Industries" class TestCreateCompanySelectionKeyboard: """Test create_company_selection_keyboard function.""" def test_creates_keyboard_with_all_companies(self): """Test keyboard creation with company list.""" companies = [ {"id": 1, "nume_firma": "ACME SRL", "cui": "RO123"}, {"id": 2, "nume_firma": "Beta Industries", "cui": "RO456"} ] keyboard = create_company_selection_keyboard(companies) assert isinstance(keyboard, InlineKeyboardMarkup) assert len(keyboard.inline_keyboard) == 2 # Check first button first_button = keyboard.inline_keyboard[0][0] assert "ACME SRL" in first_button.text assert "RO123" in first_button.text assert first_button.callback_data == "select_company:1" # Check second button second_button = keyboard.inline_keyboard[1][0] assert "Beta Industries" in second_button.text assert "RO456" in second_button.text assert second_button.callback_data == "select_company:2" def test_handles_company_without_cui(self): """Test keyboard handles companies without CUI.""" companies = [ {"id": 1, "nume_firma": "ACME SRL", "cui": ""}, {"id": 2, "nume_firma": "Beta Industries"} # No cui field ] keyboard = create_company_selection_keyboard(companies) assert len(keyboard.inline_keyboard) == 2 # First company with empty CUI - should still appear first_button = keyboard.inline_keyboard[0][0] assert "ACME SRL" in first_button.text # Second company without cui field second_button = keyboard.inline_keyboard[1][0] assert "Beta Industries" in second_button.text def test_limits_to_max_buttons(self): """Test that keyboard respects max_buttons limit.""" companies = [ {"id": i, "nume_firma": f"Company {i}", "cui": f"RO{i}"} for i in range(1, 16) # 15 companies ] keyboard = create_company_selection_keyboard(companies, max_buttons=5) # Should have 5 company buttons + 1 overflow indicator assert len(keyboard.inline_keyboard) == 6 # Last button should be overflow indicator overflow_button = keyboard.inline_keyboard[5][0] assert "încă 10 companii" in overflow_button.text assert overflow_button.callback_data == "noop" def test_no_overflow_indicator_when_exactly_max(self): """Test no overflow indicator when company count equals max.""" companies = [ {"id": i, "nume_firma": f"Company {i}", "cui": f"RO{i}"} for i in range(1, 6) # 5 companies ] keyboard = create_company_selection_keyboard(companies, max_buttons=5) # Should have exactly 5 buttons, no overflow assert len(keyboard.inline_keyboard) == 5 # Last button should be a regular company button last_button = keyboard.inline_keyboard[4][0] assert last_button.callback_data.startswith("select_company:") def test_empty_company_list(self): """Test keyboard with empty company list.""" companies = [] keyboard = create_company_selection_keyboard(companies) assert isinstance(keyboard, InlineKeyboardMarkup) assert len(keyboard.inline_keyboard) == 0 class TestFormatCompanyContextFooter: """Test format_company_context_footer function.""" def test_footer_format(self): """Test that footer has correct format.""" footer = format_company_context_footer("ACME SRL") assert "\n\n━━━━━━━━━━━━━━\n" in footer assert "Companie: ACME SRL" in footer def test_footer_with_long_company_name(self): """Test footer with very long company name.""" long_name = "Very Long Company Name With Many Words SRL Romania" footer = format_company_context_footer(long_name) assert long_name in footer assert "Companie:" in footer def test_footer_with_special_characters(self): """Test footer handles special characters in company name.""" special_name = "ACME & Partners (Romania) SRL" footer = format_company_context_footer(special_name) assert special_name in footer assert "Companie:" in footer def test_footer_structure(self): """Test that footer has consistent structure.""" footer = format_company_context_footer("Test Company") # Should start with double newline assert footer.startswith("\n\n") # Should contain separator line assert "━━━━━━━━━━━━━━" in footer # Should contain company name with prefix assert "Companie: Test Company" in footer def test_footer_is_discrete(self): """Test that footer is visually discrete and professional.""" footer = format_company_context_footer("ACME SRL") # Should be relatively short assert len(footer) < 100 # Should have visual separation assert footer.count("\n") >= 2 class TestHelpersIntegration: """Integration tests combining multiple helper functions.""" @pytest.mark.asyncio async def test_search_and_create_keyboard_workflow(self): """Test workflow of searching companies and creating keyboard.""" mock_companies = [ {"id": 1, "nume_firma": "ACME Corporation SRL", "cui": "RO123"}, {"id": 2, "nume_firma": "ACME Industries", "cui": "RO456"} ] with patch('app.bot.helpers.get_backend_client') as mock_get_client: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_user_companies = AsyncMock(return_value=mock_companies) mock_get_client.return_value = mock_client # Search results = await search_companies_by_name("acme", "fake-token") # Create keyboard from results keyboard = create_company_selection_keyboard(results) assert len(keyboard.inline_keyboard) == 2 assert "ACME Corporation" in keyboard.inline_keyboard[0][0].text assert "ACME Industries" in keyboard.inline_keyboard[1][0].text def test_footer_appends_to_message(self): """Test that footer can be appended to a message.""" message = "**Dashboard Financiar**\n\nSold Total: 10,000 RON" footer = format_company_context_footer("ACME SRL") full_message = message + footer assert "Dashboard Financiar" in full_message assert "10,000 RON" in full_message assert "━━━━━━━━━━━━━━" in full_message assert "Companie: ACME SRL" in full_message if __name__ == "__main__": pytest.main([__file__, "-v"])