""" Tests for callback handlers (FAZA 4) Tests all callback handler functions for button interactions: - handle_menu_callback - Main menu button clicks - handle_action_callback - Action buttons (Refresh, Export, Menu) - handle_details_callback - Client/Supplier details - handle_invoice_callback - Invoice details - handle_navigation_back - Back navigation - button_callback - Main callback router """ import pytest from unittest.mock import AsyncMock, MagicMock, patch from telegram import Update, CallbackQuery, User from app.bot.handlers import ( button_callback, handle_menu_callback, handle_action_callback, handle_details_callback, handle_invoice_callback, handle_navigation_back ) # ============================================================================ # FIXTURES # ============================================================================ @pytest.fixture def mock_callback_query(): """Create mock CallbackQuery with Update wrapper.""" query = MagicMock(spec=CallbackQuery) query.answer = AsyncMock() query.edit_message_text = AsyncMock() query.data = "menu:sold" update = MagicMock(spec=Update) update.callback_query = query update.effective_user = MagicMock(spec=User) update.effective_user.id = 12345 return update @pytest.fixture def mock_query(): """Create standalone mock CallbackQuery.""" query = MagicMock(spec=CallbackQuery) query.answer = AsyncMock() query.edit_message_text = AsyncMock() return query # ============================================================================ # TESTS: handle_menu_callback # ============================================================================ @pytest.mark.asyncio async def test_handle_menu_callback_sold(mock_query): """Test menu callback for 'sold' (dashboard).""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.handlers.get_backend_client') as mock_client_fn: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_dashboard_data = AsyncMock(return_value={ 'sold_total': 10000, 'facturi_emise': 10 }) mock_client_fn.return_value = mock_client await handle_menu_callback(mock_query, 12345, "menu:sold") # Verify query was edited with dashboard response assert mock_query.edit_message_text.called call_kwargs = mock_query.edit_message_text.call_args.kwargs assert 'reply_markup' in call_kwargs assert 'parse_mode' in call_kwargs @pytest.mark.asyncio async def test_handle_menu_callback_casa(mock_query): """Test menu callback for 'casa' (cash treasury).""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.helpers.get_treasury_breakdown_split', new_callable=AsyncMock) as mock_treasury: mock_treasury.return_value = { 'casa': {'accounts': [], 'total': 5000}, 'banca': {'accounts': [], 'total': 10000} } await handle_menu_callback(mock_query, 12345, "menu:casa") assert mock_query.edit_message_text.called @pytest.mark.asyncio async def test_handle_menu_callback_clienti(mock_query): """Test menu callback for 'clienti' (clients).""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.helpers.get_clients_with_maturity', new_callable=AsyncMock) as mock_clients: mock_clients.return_value = { 'clients': [{'id': 1, 'name': 'Client A', 'balance': 5000}], 'maturity': {'in_term': 3000, 'overdue': 2000, 'total': 5000} } await handle_menu_callback(mock_query, 12345, "menu:clienti") assert mock_query.edit_message_text.called # Should include client list keyboard call_kwargs = mock_query.edit_message_text.call_args.kwargs assert 'reply_markup' in call_kwargs @pytest.mark.asyncio async def test_handle_menu_callback_no_company(mock_query): """Test menu callback when no company is selected.""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = None # No company mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) await handle_menu_callback(mock_query, 12345, "menu:sold") # Should prompt to select company assert mock_query.edit_message_text.called call_args = mock_query.edit_message_text.call_args.args assert "Nu ai selectat" in call_args[0] or "selectcompany" in call_args[0] # ============================================================================ # TESTS: handle_action_callback # ============================================================================ @pytest.mark.asyncio async def test_handle_action_callback_menu(mock_query): """Test action callback for returning to menu.""" with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) await handle_action_callback(mock_query, 12345, "action:menu") # Should edit message with main menu assert mock_query.edit_message_text.called call_kwargs = mock_query.edit_message_text.call_args.kwargs assert 'reply_markup' in call_kwargs # Check for menu text call_args = mock_query.edit_message_text.call_args.args assert "Meniu" in call_args[0] @pytest.mark.asyncio async def test_handle_action_callback_refresh(mock_query): """Test action callback for refresh button.""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.handlers.get_backend_client') as mock_client_fn: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_dashboard_data = AsyncMock(return_value={'sold_total': 10000}) mock_client_fn.return_value = mock_client await handle_action_callback(mock_query, 12345, "action:refresh:sold") # Should re-trigger the view (same as menu:sold) assert mock_query.edit_message_text.called @pytest.mark.asyncio async def test_handle_action_callback_export(mock_query): """Test action callback for export button (placeholder).""" await handle_action_callback(mock_query, 12345, "action:export:facturi") # Should show placeholder alert assert mock_query.answer.called call_kwargs = mock_query.answer.call_args.kwargs assert call_kwargs.get('show_alert') is True # ============================================================================ # TESTS: handle_details_callback # ============================================================================ @pytest.mark.asyncio async def test_handle_details_callback_client(mock_query): """Test details callback for client details.""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.helpers.get_client_invoices', new_callable=AsyncMock) as mock_invoices: mock_invoices.return_value = [ {'id': 1, 'number': 'FV001', 'amount': 5000} ] with patch('app.bot.helpers.get_clients_with_maturity', new_callable=AsyncMock) as mock_clients: mock_clients.return_value = { 'clients': [{'id': 123, 'name': 'Client A', 'balance': 5000}], 'maturity': {} } await handle_details_callback(mock_query, 12345, "details:client:123") # Should edit message with client details assert mock_query.edit_message_text.called call_kwargs = mock_query.edit_message_text.call_args.kwargs assert 'reply_markup' in call_kwargs # Should have invoice list keyboard @pytest.mark.asyncio async def test_handle_details_callback_supplier(mock_query): """Test details callback for supplier details.""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.helpers.get_supplier_invoices', new_callable=AsyncMock) as mock_invoices: mock_invoices.return_value = [ {'id': 1, 'number': 'FC001', 'amount': 3000} ] with patch('app.bot.helpers.get_suppliers_with_maturity', new_callable=AsyncMock) as mock_suppliers: mock_suppliers.return_value = { 'suppliers': [{'id': 456, 'name': 'Supplier A', 'balance': 3000}], 'maturity': {} } await handle_details_callback(mock_query, 12345, "details:supplier:456") assert mock_query.edit_message_text.called @pytest.mark.asyncio async def test_handle_details_callback_client_not_found(mock_query): """Test details callback when client is not found.""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.helpers.get_client_invoices', new_callable=AsyncMock): with patch('app.bot.helpers.get_clients_with_maturity', new_callable=AsyncMock) as mock_clients: mock_clients.return_value = { 'clients': [], # No clients 'maturity': {} } await handle_details_callback(mock_query, 12345, "details:client:999") # Should show error alert assert mock_query.answer.called call_kwargs = mock_query.answer.call_args.kwargs assert call_kwargs.get('show_alert') is True # ============================================================================ # TESTS: handle_invoice_callback # ============================================================================ @pytest.mark.asyncio async def test_handle_invoice_callback(mock_query): """Test invoice callback (placeholder).""" await handle_invoice_callback(mock_query, 12345, "invoice:CLIENTI:123") # Should show placeholder alert assert mock_query.answer.called call_kwargs = mock_query.answer.call_args.kwargs assert call_kwargs.get('show_alert') is True # ============================================================================ # TESTS: handle_navigation_back # ============================================================================ @pytest.mark.asyncio async def test_handle_navigation_back_menu(mock_query): """Test navigation back to main menu.""" with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) await handle_navigation_back(mock_query, 12345, "nav:back:menu") # Should navigate to main menu assert mock_query.edit_message_text.called @pytest.mark.asyncio async def test_handle_navigation_back_clienti(mock_query): """Test navigation back to clients list.""" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.helpers.get_clients_with_maturity', new_callable=AsyncMock) as mock_clients: mock_clients.return_value = { 'clients': [], 'maturity': {} } await handle_navigation_back(mock_query, 12345, "nav:back:clienti") assert mock_query.edit_message_text.called # ============================================================================ # TESTS: button_callback (main router) # ============================================================================ @pytest.mark.asyncio async def test_button_callback_menu_sold(mock_callback_query): """Test button_callback routing to menu handler.""" mock_callback_query.callback_query.data = "menu:sold" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.handlers.get_backend_client') as mock_client_fn: mock_client = MagicMock() mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock() mock_client.get_dashboard_data = AsyncMock(return_value={'sold_total': 10000}) mock_client_fn.return_value = mock_client await button_callback(mock_callback_query, None) # Verify callback was answered assert mock_callback_query.callback_query.answer.called # Verify message was edited assert mock_callback_query.callback_query.edit_message_text.called @pytest.mark.asyncio async def test_button_callback_action_menu(mock_callback_query): """Test button_callback routing to action handler.""" mock_callback_query.callback_query.data = "action:menu" with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) await button_callback(mock_callback_query, None) assert mock_callback_query.callback_query.answer.called assert mock_callback_query.callback_query.edit_message_text.called @pytest.mark.asyncio async def test_button_callback_details_client(mock_callback_query): """Test button_callback routing to details handler.""" mock_callback_query.callback_query.data = "details:client:123" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.get_active_company.return_value = {'id': 1, 'name': 'Test Co'} mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) with patch('app.bot.helpers.get_client_invoices', new_callable=AsyncMock) as mock_invoices: mock_invoices.return_value = [] with patch('app.bot.helpers.get_clients_with_maturity', new_callable=AsyncMock) as mock_clients: mock_clients.return_value = { 'clients': [{'id': 123, 'name': 'Client A', 'balance': 5000}], 'maturity': {} } await button_callback(mock_callback_query, None) assert mock_callback_query.callback_query.answer.called @pytest.mark.asyncio async def test_button_callback_noop(mock_callback_query): """Test button_callback with noop (no operation).""" mock_callback_query.callback_query.data = "noop" await button_callback(mock_callback_query, None) # Should just answer the callback assert mock_callback_query.callback_query.answer.called # Should not edit message assert not mock_callback_query.callback_query.edit_message_text.called @pytest.mark.asyncio async def test_button_callback_existing_select_company(mock_callback_query): """Test button_callback with existing select_company callback (backwards compatibility).""" mock_callback_query.callback_query.data = "select_company:1" with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth: mock_auth.return_value = {'jwt_token': 'fake_token'} with patch('app.bot.handlers.get_backend_client') as mock_client_fn: 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, 'id_firma': 1, 'name': 'Test Co', 'nume_firma': 'Test Co', 'cui': '12345'} ]) mock_client_fn.return_value = mock_client with patch('app.bot.handlers.get_session_manager') as mock_session: mock_session_obj = MagicMock() mock_session_obj.set_active_company = MagicMock() mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj) mock_session.return_value.save_session = AsyncMock() await button_callback(mock_callback_query, None) # Should handle company selection assert mock_callback_query.callback_query.answer.called assert mock_callback_query.callback_query.edit_message_text.called