FlyRates / scratch /test_bot_local.py
Sadeep Sachintha
test: add scripts for local bot handler testing and database migration verification
f57f933
import asyncio
import sys
import os
from datetime import datetime
# Append the project root to sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from aiogram import Dispatcher, Bot, types
from aiogram.client.session.base import BaseSession
from bot.handlers import router as bot_router
from db.session import init_db, async_session
from db.models import User
from sqlalchemy import select
# Create a lightweight mock session subclassing BaseSession
class MockSession(BaseSession):
def __init__(self):
super().__init__()
self.requests = []
async def make_request(self, bot, method, timeout=None):
"""Intercepts outgoing methods, records them, and returns dummy results with bound bot."""
self.requests.append(method)
# Determine dummy return based on the method
from aiogram.methods import SendMessage, DeleteMessage
dummy_chat = types.Chat(id=999111222, type="private")
if isinstance(method, SendMessage):
msg = types.Message(
message_id=100,
date=datetime.now(),
chat=dummy_chat,
text=method.text
)
# Set the private _bot attribute to bind the bot instance natively
msg._bot = bot
return msg
elif isinstance(method, DeleteMessage):
return True
msg = types.Message(
message_id=100,
date=datetime.now(),
chat=dummy_chat,
text="Mock reply"
)
msg._bot = bot
return msg
async def stream_content(self, url, headers=None, timeout=None, chunk_size=65536):
"""Dummy implementation of abstract method stream_content."""
yield b""
async def close(self):
pass
# Create a dummy bot with our MockSession
mock_session = MockSession()
mock_bot = Bot(token="123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", session=mock_session)
async def test_bot_handlers():
print("πŸš€ Initializing database for Bot testing...")
await init_db()
print("\nπŸ“¦ Setting up Dispatcher...")
dp = Dispatcher()
dp.include_router(bot_router)
# Mock user details
test_chat_id = 999111222
mock_user = types.User(id=test_chat_id, is_bot=False, first_name="Tester", last_name="Fly")
mock_chat = types.Chat(id=test_chat_id, type="private")
# Helper to create mock messages
def create_mock_message(text: str) -> types.Message:
msg = types.Message(
message_id=1,
date=datetime.now(),
chat=mock_chat,
from_user=mock_user,
text=text
)
msg._bot = mock_bot
return msg
# Clean up any existing test user in the DB
async with async_session() as session:
existing = await session.scalar(select(User).where(User.chat_id == test_chat_id))
if existing:
await session.delete(existing)
await session.commit()
def print_last_request(title: str):
"""Helper to extract and print what was recorded by MockSession."""
if mock_session.requests:
req = mock_session.requests[-1]
print(f"πŸ€– {title} Response:")
# Extract text and buttons from the SendMessage object
from aiogram.methods import SendMessage
if isinstance(req, SendMessage):
print(req.text)
if req.reply_markup and hasattr(req.reply_markup, 'inline_keyboard'):
buttons = [b.text for row in req.reply_markup.inline_keyboard for b in row]
print(f"πŸ”˜ Buttons: {buttons}")
else:
print(f"[Method: {type(req).__name__}]")
print()
# Clear requests list
mock_session.requests.clear()
print("\n--- Test Case 1: Sending /start ---")
msg_start = create_mock_message("/start")
await dp.feed_update(mock_bot, types.Update(update_id=1, message=msg_start))
print_last_request("START")
print("--- Test Case 2: Sending /subscribe (already subscribed) ---")
msg_sub = create_mock_message("/subscribe")
await dp.feed_update(mock_bot, types.Update(update_id=2, message=msg_sub))
print_last_request("SUBSCRIBE")
print("--- Test Case 3: Sending /help ---")
msg_help = create_mock_message("/help")
await dp.feed_update(mock_bot, types.Update(update_id=3, message=msg_help))
print_last_request("HELP")
print("--- Test Case 4: Sending /current ---")
msg_current = create_mock_message("/current")
# We monkeypatch get_formatted_current_rates to run quickly without scraping
from bot import handlers
original_get_rates = handlers.get_formatted_current_rates
async def mock_get_rates():
return "🌍 <b>Mock LKR Exchange Rates</b>\nπŸ‡ΊπŸ‡Έ 1 USD = 310.00 LKR\nπŸ‡ͺπŸ‡Ί 1 EUR = 335.00 LKR\nπŸ‡¬πŸ‡§ 1 GBP = 390.00 LKR\n\n❌ Type /unsubscribe to stop."
handlers.get_formatted_current_rates = mock_get_rates
await dp.feed_update(mock_bot, types.Update(update_id=4, message=msg_current))
# Find the SendMessage request in recorded requests queue (loading is deleted first)
from aiogram.methods import SendMessage
rates_req = None
for req in reversed(mock_session.requests):
if isinstance(req, SendMessage) and "Mock LKR Exchange Rates" in req.text:
rates_req = req
break
if rates_req:
print("πŸ€– CURRENT Response:")
print(rates_req.text)
if rates_req.reply_markup and hasattr(rates_req.reply_markup, 'inline_keyboard'):
buttons = [b.text for row in rates_req.reply_markup.inline_keyboard for b in row]
print(f"πŸ”˜ Buttons: {buttons}")
print()
mock_session.requests.clear()
# Restore original function
handlers.get_formatted_current_rates = original_get_rates
print("--- Test Case 5: Sending /unsubscribe ---")
msg_unsub = create_mock_message("/unsubscribe")
await dp.feed_update(mock_bot, types.Update(update_id=5, message=msg_unsub))
print_last_request("UNSUBSCRIBE")
# Verification in DB
print("πŸ”Ž Verifying DB state after /unsubscribe...")
async with async_session() as session:
user = await session.scalar(select(User).where(User.chat_id == test_chat_id))
if user:
print(f"βœ… DB User: chat_id={user.chat_id}, is_subscribed={user.is_subscribed}")
assert user.is_subscribed is False, "User should be unsubscribed in database"
# Cleanup
await session.delete(user)
await session.commit()
print("βœ… Cleanup test user from DB successful.")
else:
print("❌ Test user not found in DB!")
print("\nπŸŽ‰ ALL LOCAL HANDLER TESTS PASSED PERFECTLY!")
if __name__ == "__main__":
asyncio.run(test_bot_handlers())