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>
472 lines
21 KiB
Python
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
|