Files
roa2web-service-auto/reports-app/telegram-bot/tests/test_callbacks.py
Marius Mutu a7a1bef375 Add missing test files and update .gitignore to allow test files
This commit addresses the overly restrictive .gitignore pattern that
was excluding all test files (test_*.py), including legitimate pytest
and unittest test suites essential for code quality and CI/CD.

Changes to .gitignore:
- Added negation patterns !**/tests/test_*.py and !**/test_*.py
  to allow proper test files while still blocking temporary scripts
- This enables pytest test suites to be tracked by git

Added test files (17 files):

Telegram Bot Tests (15 files):
- reports-app/telegram-bot/tests/test_auth.py
  Tests for authentication and account linking flow
- reports-app/telegram-bot/tests/test_callbacks.py
  Tests for callback query handlers
- reports-app/telegram-bot/tests/test_formatters.py
  Tests for message formatting utilities
- reports-app/telegram-bot/tests/test_formatters_extended.py
  Extended formatter tests
- reports-app/telegram-bot/tests/test_handlers_menu.py
  Tests for menu handlers
- reports-app/telegram-bot/tests/test_helpers.py
  Tests for helper functions
- reports-app/telegram-bot/tests/test_helpers_extended.py
  Extended helper tests
- reports-app/telegram-bot/tests/test_helpers_real.py
  Real integration tests for helpers
- reports-app/telegram-bot/tests/test_helpers_real_simple.py
  Simplified integration tests
- reports-app/telegram-bot/tests/test_login_flow.py
  Complete login flow integration tests
- reports-app/telegram-bot/tests/test_menus.py
  Menu system tests
- reports-app/telegram-bot/tests/test_session_company.py
  Session and company management tests
- reports-app/telegram-bot/test_claude_integration.py
  Manual integration test (Claude AI)
- reports-app/telegram-bot/test_claude_response.py
  Response formatting test
- reports-app/telegram-bot/test_db.py
  Database operations manual test

Shared Module Tests (2 files):
- shared/auth/test_auth.py
  Authentication system tests
- shared/database/test_pool.py
  Oracle connection pool tests

Security verification:
 All test files use mock objects, fixtures, and environment variables
 No hardcoded credentials or secrets found
 Safe for version control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 15:09:43 +03:00

472 lines
21 KiB
Python

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