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>
This commit is contained in:
326
reports-app/telegram-bot/tests/test_menus.py
Normal file
326
reports-app/telegram-bot/tests/test_menus.py
Normal file
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
Tests for menu builder functions in app/bot/menus.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from app.bot.menus import (
|
||||
create_main_menu,
|
||||
create_action_buttons,
|
||||
create_client_list_keyboard,
|
||||
create_supplier_list_keyboard,
|
||||
create_invoice_list_keyboard,
|
||||
create_navigation_buttons
|
||||
)
|
||||
|
||||
|
||||
def test_create_main_menu_without_company():
|
||||
"""Test main menu when no company is selected"""
|
||||
keyboard = create_main_menu()
|
||||
assert keyboard is not None
|
||||
assert len(keyboard.inline_keyboard) >= 5 # At least 5 rows
|
||||
|
||||
# Verify first row is for company selection
|
||||
first_row_text = keyboard.inline_keyboard[0][0].text
|
||||
assert "compan" in first_row_text.lower() or "selectare" in first_row_text.lower()
|
||||
|
||||
|
||||
def test_create_main_menu_with_company():
|
||||
"""Test main menu with active company"""
|
||||
keyboard = create_main_menu(company_name="ACME SRL")
|
||||
assert keyboard is not None
|
||||
|
||||
# Verify that it shows active company
|
||||
first_row = keyboard.inline_keyboard[0][0].text
|
||||
assert "ACME SRL" in first_row or "Selectare" in first_row or "Companie" in first_row
|
||||
|
||||
|
||||
def test_main_menu_has_6_financial_buttons():
|
||||
"""Test that menu has 6 financial option buttons"""
|
||||
keyboard = create_main_menu("Test Co")
|
||||
buttons_text = []
|
||||
|
||||
# Collect all button texts except first (company) and last (help) rows
|
||||
for row in keyboard.inline_keyboard[1:-1]:
|
||||
for button in row:
|
||||
buttons_text.append(button.text)
|
||||
|
||||
# Verify we have the expected buttons (partial match for flexibility)
|
||||
expected = ["sold", "casa", "banca", "clien", "furniz", "evol"]
|
||||
found = [any(exp in btn.lower() for btn in buttons_text) for exp in expected]
|
||||
assert all(found), f"Missing buttons. Found: {buttons_text}"
|
||||
|
||||
|
||||
def test_main_menu_callback_data_format():
|
||||
"""Test that callback data is correctly formatted"""
|
||||
keyboard = create_main_menu("Test Co")
|
||||
|
||||
for row in keyboard.inline_keyboard:
|
||||
for button in row:
|
||||
if button.callback_data and button.callback_data.startswith("menu:"):
|
||||
# Verify format: menu:action
|
||||
parts = button.callback_data.split(":")
|
||||
assert len(parts) == 2
|
||||
assert parts[0] == "menu"
|
||||
assert parts[1] in ["sold", "casa", "banca", "clienti", "furnizori", "evolutie", "select_company"]
|
||||
|
||||
|
||||
def test_main_menu_layout_structure():
|
||||
"""Test that main menu has correct layout structure"""
|
||||
keyboard = create_main_menu("Test Co")
|
||||
|
||||
# Should have: 1 company row + 3 financial rows (2 cols each) + 1 help row = 5 rows minimum
|
||||
assert len(keyboard.inline_keyboard) >= 5
|
||||
|
||||
# First row should have 1 button (company selection)
|
||||
assert len(keyboard.inline_keyboard[0]) == 1
|
||||
|
||||
# Financial rows should have 2 buttons each
|
||||
for i in range(1, 4):
|
||||
assert len(keyboard.inline_keyboard[i]) == 2
|
||||
|
||||
# Last row should have 1 button (help)
|
||||
assert len(keyboard.inline_keyboard[-1]) == 1
|
||||
|
||||
|
||||
def test_create_action_buttons_with_export():
|
||||
"""Test action buttons with export enabled"""
|
||||
keyboard = create_action_buttons("sold", show_export=True)
|
||||
assert len(keyboard.inline_keyboard) == 2 # 2 rows
|
||||
|
||||
# First row should have 2 buttons (Refresh + Export)
|
||||
assert len(keyboard.inline_keyboard[0]) == 2
|
||||
|
||||
# Verify button texts
|
||||
row1_text = [btn.text for btn in keyboard.inline_keyboard[0]]
|
||||
assert any("Refresh" in txt for txt in row1_text)
|
||||
assert any("Export" in txt for txt in row1_text)
|
||||
|
||||
# Second row should have 1 button (Menu)
|
||||
assert len(keyboard.inline_keyboard[1]) == 1
|
||||
assert "Meniu" in keyboard.inline_keyboard[1][0].text
|
||||
|
||||
|
||||
def test_create_action_buttons_without_export():
|
||||
"""Test action buttons without export"""
|
||||
keyboard = create_action_buttons("sold", show_export=False)
|
||||
all_text = " ".join([btn.text for row in keyboard.inline_keyboard for btn in row])
|
||||
assert "Export" not in all_text
|
||||
|
||||
|
||||
def test_action_buttons_callback_format():
|
||||
"""Test callback format for action buttons"""
|
||||
keyboard = create_action_buttons("sold")
|
||||
|
||||
for row in keyboard.inline_keyboard:
|
||||
for button in row:
|
||||
if "refresh" in button.text.lower():
|
||||
assert button.callback_data.startswith("action:refresh:")
|
||||
assert button.callback_data == "action:refresh:sold"
|
||||
elif "menu" in button.text.lower():
|
||||
assert button.callback_data == "action:menu"
|
||||
elif "export" in button.text.lower():
|
||||
assert button.callback_data.startswith("action:export:")
|
||||
assert button.callback_data == "action:export:sold"
|
||||
|
||||
|
||||
def test_create_client_list_keyboard():
|
||||
"""Test client list keyboard creation"""
|
||||
clients = [
|
||||
{"id": 1, "name": "Client A", "balance": 15000},
|
||||
{"id": 2, "name": "Client B", "balance": 8500}
|
||||
]
|
||||
keyboard = create_client_list_keyboard(clients)
|
||||
|
||||
# Should have 2 clients + 1 navigation row = 3 rows
|
||||
assert len(keyboard.inline_keyboard) >= 3
|
||||
|
||||
# Verify first two rows are for clients
|
||||
assert "Client A" in keyboard.inline_keyboard[0][0].text
|
||||
assert "15" in keyboard.inline_keyboard[0][0].text # Amount
|
||||
|
||||
# Verify callback data
|
||||
assert keyboard.inline_keyboard[0][0].callback_data == "details:client:1"
|
||||
|
||||
# Verify second client
|
||||
assert "Client B" in keyboard.inline_keyboard[1][0].text
|
||||
assert keyboard.inline_keyboard[1][0].callback_data == "details:client:2"
|
||||
|
||||
# Last row should be navigation (2 buttons)
|
||||
assert len(keyboard.inline_keyboard[-1]) == 2
|
||||
|
||||
|
||||
def test_create_supplier_list_keyboard():
|
||||
"""Test supplier list keyboard creation"""
|
||||
suppliers = [
|
||||
{"id": 1, "name": "Supplier A", "balance": 5000}
|
||||
]
|
||||
keyboard = create_supplier_list_keyboard(suppliers)
|
||||
|
||||
assert "Supplier A" in keyboard.inline_keyboard[0][0].text
|
||||
assert "details:supplier:1" in keyboard.inline_keyboard[0][0].callback_data
|
||||
|
||||
# Last row should be navigation
|
||||
assert len(keyboard.inline_keyboard[-1]) == 2
|
||||
|
||||
|
||||
def test_client_list_max_items():
|
||||
"""Test client list respects max_items limit"""
|
||||
clients = [{"id": i, "name": f"Client {i}", "balance": 1000} for i in range(20)]
|
||||
keyboard = create_client_list_keyboard(clients, max_items=5)
|
||||
|
||||
# Should have: 5 clients + 1 overflow indicator + 1 navigation row = 7 rows
|
||||
assert len(keyboard.inline_keyboard) <= 7
|
||||
|
||||
# Verify only first 5 clients are displayed
|
||||
client_rows = [row for row in keyboard.inline_keyboard if len(row) == 1 and "Client" in row[0].text]
|
||||
displayed_clients = [row for row in client_rows if not "încă" in row[0].text] # Exclude overflow indicator
|
||||
assert len(displayed_clients) == 5
|
||||
|
||||
# Verify overflow indicator exists
|
||||
overflow_row = [row for row in keyboard.inline_keyboard if len(row) == 1 and "încă" in row[0].text]
|
||||
assert len(overflow_row) == 1
|
||||
assert "15" in overflow_row[0][0].text # Should say "și încă 15"
|
||||
|
||||
|
||||
def test_create_invoice_list_keyboard():
|
||||
"""Test invoice list keyboard creation"""
|
||||
invoices = [
|
||||
{"id": 1, "number": "FV001", "amount": 5000, "status": "unpaid"},
|
||||
{"id": 2, "number": "FV002", "amount": 3500, "status": "paid"}
|
||||
]
|
||||
keyboard = create_invoice_list_keyboard(invoices, partner_type="CLIENTI")
|
||||
|
||||
# Should have 2 invoices + 1 navigation row = 3 rows
|
||||
assert len(keyboard.inline_keyboard) >= 3
|
||||
|
||||
# Verify first invoice
|
||||
assert "FV001" in keyboard.inline_keyboard[0][0].text
|
||||
assert "invoice:CLIENTI:1" in keyboard.inline_keyboard[0][0].callback_data
|
||||
|
||||
# Verify status text indicator (no emojis)
|
||||
assert "[NEPLATIT]" in keyboard.inline_keyboard[0][0].text or "[PLATIT]" in keyboard.inline_keyboard[0][0].text
|
||||
|
||||
# Last row should be navigation (2 buttons)
|
||||
assert len(keyboard.inline_keyboard[-1]) == 2
|
||||
|
||||
|
||||
def test_invoice_list_callback_data():
|
||||
"""Test invoice list callback data format"""
|
||||
invoices = [
|
||||
{"id": 123, "number": "FV123", "amount": 1000, "status": "paid"}
|
||||
]
|
||||
|
||||
# Test CLIENTI
|
||||
keyboard_clienti = create_invoice_list_keyboard(invoices, partner_type="CLIENTI")
|
||||
assert keyboard_clienti.inline_keyboard[0][0].callback_data == "invoice:CLIENTI:123"
|
||||
|
||||
# Test FURNIZORI
|
||||
keyboard_furnizori = create_invoice_list_keyboard(invoices, partner_type="FURNIZORI")
|
||||
assert keyboard_furnizori.inline_keyboard[0][0].callback_data == "invoice:FURNIZORI:123"
|
||||
|
||||
|
||||
def test_invoice_list_navigation_buttons():
|
||||
"""Test invoice list navigation buttons are correct"""
|
||||
invoices = [{"id": 1, "number": "FV001", "amount": 1000, "status": "paid"}]
|
||||
|
||||
# For CLIENTI, back button should go to clienti
|
||||
keyboard_clienti = create_invoice_list_keyboard(invoices, partner_type="CLIENTI")
|
||||
back_button = keyboard_clienti.inline_keyboard[-1][0]
|
||||
assert "Înapoi" in back_button.text
|
||||
assert back_button.callback_data == "nav:back:clienti"
|
||||
|
||||
# For FURNIZORI, back button should go to furnizori
|
||||
keyboard_furnizori = create_invoice_list_keyboard(invoices, partner_type="FURNIZORI")
|
||||
back_button = keyboard_furnizori.inline_keyboard[-1][0]
|
||||
assert back_button.callback_data == "nav:back:furnizori"
|
||||
|
||||
|
||||
def test_create_navigation_buttons():
|
||||
"""Test simple navigation buttons"""
|
||||
keyboard = create_navigation_buttons("menu")
|
||||
|
||||
assert len(keyboard.inline_keyboard) == 1
|
||||
assert len(keyboard.inline_keyboard[0]) == 1
|
||||
|
||||
button = keyboard.inline_keyboard[0][0]
|
||||
assert "Înapoi" in button.text
|
||||
assert button.callback_data == "nav:back:menu"
|
||||
|
||||
|
||||
def test_navigation_buttons_different_targets():
|
||||
"""Test navigation buttons with different target locations"""
|
||||
# Test menu target
|
||||
kb_menu = create_navigation_buttons("menu")
|
||||
assert kb_menu.inline_keyboard[0][0].callback_data == "nav:back:menu"
|
||||
|
||||
# Test clienti target
|
||||
kb_clienti = create_navigation_buttons("clienti")
|
||||
assert kb_clienti.inline_keyboard[0][0].callback_data == "nav:back:clienti"
|
||||
|
||||
# Test furnizori target
|
||||
kb_furnizori = create_navigation_buttons("furnizori")
|
||||
assert kb_furnizori.inline_keyboard[0][0].callback_data == "nav:back:furnizori"
|
||||
|
||||
|
||||
def test_client_list_empty():
|
||||
"""Test client list with empty list"""
|
||||
keyboard = create_client_list_keyboard([])
|
||||
|
||||
# Should only have navigation row
|
||||
assert len(keyboard.inline_keyboard) == 1
|
||||
assert len(keyboard.inline_keyboard[0]) == 2 # Back + Refresh
|
||||
|
||||
|
||||
def test_supplier_list_empty():
|
||||
"""Test supplier list with empty list"""
|
||||
keyboard = create_supplier_list_keyboard([])
|
||||
|
||||
# Should only have navigation row
|
||||
assert len(keyboard.inline_keyboard) == 1
|
||||
assert len(keyboard.inline_keyboard[0]) == 2 # Back + Refresh
|
||||
|
||||
|
||||
def test_invoice_list_empty():
|
||||
"""Test invoice list with empty list"""
|
||||
keyboard = create_invoice_list_keyboard([], partner_type="CLIENTI")
|
||||
|
||||
# Should only have navigation row
|
||||
assert len(keyboard.inline_keyboard) == 1
|
||||
assert len(keyboard.inline_keyboard[0]) == 2 # Back + Export
|
||||
|
||||
|
||||
def test_balance_formatting():
|
||||
"""Test that balances are formatted with thousands separator"""
|
||||
clients = [
|
||||
{"id": 1, "name": "Client", "balance": 150000}
|
||||
]
|
||||
keyboard = create_client_list_keyboard(clients)
|
||||
|
||||
button_text = keyboard.inline_keyboard[0][0].text
|
||||
# Should have comma separator: 150,000
|
||||
assert "150,000" in button_text or "150.000" in button_text
|
||||
|
||||
|
||||
def test_callback_data_no_spaces():
|
||||
"""Test that callback_data contains no spaces"""
|
||||
keyboard = create_main_menu("Test Company With Spaces")
|
||||
|
||||
for row in keyboard.inline_keyboard:
|
||||
for button in row:
|
||||
if button.callback_data:
|
||||
assert " " not in button.callback_data, f"Callback data contains space: {button.callback_data}"
|
||||
|
||||
|
||||
def test_noop_callback_for_overflow():
|
||||
"""Test that overflow indicators use noop callback"""
|
||||
clients = [{"id": i, "name": f"Client {i}", "balance": 1000} for i in range(15)]
|
||||
keyboard = create_client_list_keyboard(clients, max_items=5)
|
||||
|
||||
# Find overflow indicator button
|
||||
overflow_buttons = [
|
||||
row[0] for row in keyboard.inline_keyboard
|
||||
if len(row) == 1 and "încă" in row[0].text
|
||||
]
|
||||
|
||||
assert len(overflow_buttons) == 1
|
||||
assert overflow_buttons[0].callback_data == "noop"
|
||||
Reference in New Issue
Block a user