Files
roa2web-service-auto/reports-app/telegram-bot/tests/test_handlers_menu.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

393 lines
18 KiB
Python

"""
Tests for FAZA 3 - New Command Handlers with Button Interface
Tests for menu_command, trezorerie_casa_command, trezorerie_banca_command,
clienti_command, furnizori_command, evolutie_command and modifications to
existing commands.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from telegram import Update, User, Message
from telegram.ext import ContextTypes
from app.bot.handlers import (
menu_command,
trezorerie_casa_command,
trezorerie_banca_command,
clienti_command,
furnizori_command,
evolutie_command,
start_command,
dashboard_command,
facturi_command,
trezorerie_command
)
@pytest.fixture
def mock_update():
"""Create mock Update object"""
update = MagicMock(spec=Update)
update.effective_user = MagicMock(spec=User)
update.effective_user.id = 12345
update.effective_user.username = "testuser"
update.message = MagicMock(spec=Message)
update.message.reply_text = AsyncMock()
return update
@pytest.fixture
def mock_context():
"""Create mock Context object"""
context = MagicMock(spec=ContextTypes.DEFAULT_TYPE)
context.args = []
return context
# =============================================================================
# TEST: menu_command
# =============================================================================
@pytest.mark.asyncio
async def test_menu_command_linked_user(mock_update, mock_context):
"""Test /menu for linked user with active company"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock) as mock_check:
mock_check.return_value = True
with patch('app.bot.handlers.get_session_manager') as mock_session:
# Mock session with active company
mock_session_obj = MagicMock()
mock_session_obj.get_active_company.return_value = {
'id': 1, 'name': 'Test Co', 'cui': '12345'
}
mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj)
await menu_command(mock_update, mock_context)
# Verify message was sent with keyboard
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
assert call_kwargs['reply_markup'] is not None
@pytest.mark.asyncio
async def test_menu_command_unlinked_user(mock_update, mock_context):
"""Test /menu for unlinked user"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock) as mock_check:
mock_check.return_value = False
await menu_command(mock_update, mock_context)
# Verify error message was sent
assert mock_update.message.reply_text.called
call_args = mock_update.message.reply_text.call_args.args
assert "nelinkuit" in call_args[0].lower() or "link" in call_args[0].lower()
@pytest.mark.asyncio
async def test_menu_command_no_company(mock_update, mock_context):
"""Test /menu when no company is selected"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock) as mock_check:
mock_check.return_value = True
with patch('app.bot.handlers.get_session_manager') as mock_session:
# Mock session with NO active company
mock_session_obj = MagicMock()
mock_session_obj.get_active_company.return_value = None
mock_session.return_value.get_or_create_session = AsyncMock(return_value=mock_session_obj)
await menu_command(mock_update, mock_context)
# Verify menu was still shown (with company selection button)
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
# =============================================================================
# TEST: trezorerie_casa_command
# =============================================================================
@pytest.mark.asyncio
async def test_trezorerie_casa_command(mock_update, mock_context):
"""Test /trezorerie_casa command"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
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.helpers.get_treasury_breakdown_split', new_callable=AsyncMock) as mock_treasury:
mock_treasury.return_value = {
'casa': {'accounts': [], 'total': 5000},
'banca': {'accounts': [], 'total': 10000}
}
await trezorerie_casa_command(mock_update, mock_context)
# Verify message was sent with keyboard
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
assert call_kwargs['reply_markup'] is not None
@pytest.mark.asyncio
async def test_trezorerie_casa_unlinked_user(mock_update, mock_context):
"""Test /trezorerie_casa for unlinked user"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=False):
await trezorerie_casa_command(mock_update, mock_context)
# Verify error message
assert mock_update.message.reply_text.called
call_args = mock_update.message.reply_text.call_args.args
assert "nelinkuit" in call_args[0].lower()
# =============================================================================
# TEST: trezorerie_banca_command
# =============================================================================
@pytest.mark.asyncio
async def test_trezorerie_banca_command(mock_update, mock_context):
"""Test /trezorerie_banca command"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
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.helpers.get_treasury_breakdown_split', new_callable=AsyncMock) as mock_treasury:
mock_treasury.return_value = {
'casa': {'accounts': [], 'total': 5000},
'banca': {'accounts': [], 'total': 10000}
}
await trezorerie_banca_command(mock_update, mock_context)
# Verify message was sent with keyboard
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
# =============================================================================
# TEST: clienti_command
# =============================================================================
@pytest.mark.asyncio
async def test_clienti_command(mock_update, mock_context):
"""Test /clienti command"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
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.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 clienti_command(mock_update, mock_context)
# Verify message was sent with client list keyboard
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
@pytest.mark.asyncio
async def test_clienti_command_no_data(mock_update, mock_context):
"""Test /clienti command when API returns no data"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
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.helpers.get_clients_with_maturity', new_callable=AsyncMock) as mock_clients:
mock_clients.return_value = None
await clienti_command(mock_update, mock_context)
# Verify error message
assert mock_update.message.reply_text.called
call_args = mock_update.message.reply_text.call_args.args
assert "Eroare" in call_args[0]
# =============================================================================
# TEST: furnizori_command
# =============================================================================
@pytest.mark.asyncio
async def test_furnizori_command(mock_update, mock_context):
"""Test /furnizori command"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
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.helpers.get_suppliers_with_maturity', new_callable=AsyncMock) as mock_suppliers:
mock_suppliers.return_value = {
'suppliers': [{'id': 1, 'name': 'Supplier A', 'balance': 5000}],
'maturity': {'in_term': 4000, 'overdue': 1000, 'total': 5000}
}
await furnizori_command(mock_update, mock_context)
# Verify message was sent with supplier list keyboard
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
# =============================================================================
# TEST: evolutie_command
# =============================================================================
@pytest.mark.asyncio
async def test_evolutie_command(mock_update, mock_context):
"""Test /evolutie command"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
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.helpers.get_cashflow_evolution_data', new_callable=AsyncMock) as mock_evolution:
mock_evolution.return_value = {
'performance': {'incasari_total': 100000, 'plati_total': 80000},
'monthly': {'months': ['Ian', 'Feb'], 'incasari': [50000, 50000]}
}
await evolutie_command(mock_update, mock_context)
# Verify message was sent with action buttons (no export)
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
# =============================================================================
# TEST: Modified existing commands
# =============================================================================
@pytest.mark.asyncio
async def test_start_command_linked_shows_menu(mock_update, mock_context):
"""Test that /start shows menu for linked users"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock) as mock_check:
mock_check.return_value = True
with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth:
mock_auth.return_value = {'username': 'testuser', '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)
await start_command(mock_update, mock_context)
# Verify menu keyboard was sent
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
assert call_kwargs['reply_markup'] is not None
@pytest.mark.asyncio
async def test_dashboard_command_has_action_buttons(mock_update, mock_context):
"""Test that /dashboard command now includes action buttons"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth:
mock_auth.return_value = {'jwt_token': 'fake_token'}
# Mock backend client
mock_backend_client = MagicMock()
mock_backend_client.__aenter__ = AsyncMock(return_value=mock_backend_client)
mock_backend_client.__aexit__ = AsyncMock()
mock_backend_client.get_dashboard_data = AsyncMock(return_value={
'sold_total': 10000,
'facturi_emise': 10,
'facturi_platite': 5
})
with patch('app.bot.handlers.get_backend_client', return_value=mock_backend_client):
await dashboard_command(mock_update, mock_context)
# Verify action buttons were added
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
assert call_kwargs['reply_markup'] is not None
@pytest.mark.asyncio
async def test_facturi_command_has_action_buttons(mock_update, mock_context):
"""Test that /facturi command now includes action buttons"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth:
mock_auth.return_value = {'jwt_token': 'fake_token'}
# Mock backend client
mock_backend_client = MagicMock()
mock_backend_client.__aenter__ = AsyncMock(return_value=mock_backend_client)
mock_backend_client.__aexit__ = AsyncMock()
mock_backend_client.search_invoices = AsyncMock(return_value=[
{'id': 1, 'number': 'FV001', 'amount': 5000}
])
with patch('app.bot.handlers.get_backend_client', return_value=mock_backend_client):
await facturi_command(mock_update, mock_context)
# Verify action buttons were added
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs
@pytest.mark.asyncio
async def test_trezorerie_command_has_action_buttons(mock_update, mock_context):
"""Test that /trezorerie command now includes action buttons"""
with patch('app.bot.handlers.check_user_linked', new_callable=AsyncMock, return_value=True):
with patch('app.bot.helpers.get_active_company_or_prompt', new_callable=AsyncMock) as mock_company:
mock_company.return_value = {'id': 1, 'name': 'Test Co'}
with patch('app.bot.handlers.get_user_auth_data', new_callable=AsyncMock) as mock_auth:
mock_auth.return_value = {'jwt_token': 'fake_token'}
# Mock backend client
mock_backend_client = MagicMock()
mock_backend_client.__aenter__ = AsyncMock(return_value=mock_backend_client)
mock_backend_client.__aexit__ = AsyncMock()
mock_backend_client.get_treasury_data = AsyncMock(return_value={
'cash_total': 5000,
'bank_total': 10000
})
with patch('app.bot.handlers.get_backend_client', return_value=mock_backend_client):
await trezorerie_command(mock_update, mock_context)
# Verify action buttons were added
assert mock_update.message.reply_text.called
call_kwargs = mock_update.message.reply_text.call_args.kwargs
assert 'reply_markup' in call_kwargs