from types import SimpleNamespace from unittest import mock from django.test import TestCase, override_settings from telebot import types from . import interfaces from .dispatcher import handle_update from .interfaces import Interface, command, register, state, text from .models import Bot, Chat, Message, User from .testing import FakeApi, message_update @register class EchoInterface(Interface): title = 'Echo' @command('start') @text('меню') def menu(self, ctx): ctx.reset() ctx.reply('menu', keyboard=[['ask']]) @text('ask') def ask(self, ctx): ctx.state = 'await_name' ctx.reply('name?') @text('отмена', state='await_name') def cancel_name(self, ctx): ctx.state = '' ctx.reply('cancelled') @text('отмена') def cancel(self, ctx): ctx.reply('nothing to cancel') @state('await_name') def name(self, ctx): ctx.data['name'] = ctx.text ctx.state = '' ctx.reply(f'hi {ctx.text}') def fallback(self, ctx): ctx.reply(f'echo {ctx.text}') @register class ChildInterface(EchoInterface): @command('start') def child_start(self, ctx): ctx.reply('child') def member_update(update_id, status): return types.Update.de_json({ 'update_id': update_id, 'my_chat_member': { 'chat': {'id': -100, 'type': 'group', 'title': 'group'}, 'from': {'id': 7, 'is_bot': False, 'first_name': 'Ann'}, 'date': 0, 'old_chat_member': {'user': {'id': 1, 'is_bot': True, 'first_name': 'bot'}, 'status': 'left'}, 'new_chat_member': {'user': {'id': 1, 'is_bot': True, 'first_name': 'bot'}, 'status': status}, }, }) class DispatcherTests(TestCase): def setUp(self): self.api = FakeApi() self.bot = Bot.objects.create(token='1:test', interface=interfaces.key(EchoInterface)) def send(self, text, update_id=1, **kwargs): handle_update(self.api, self.bot, message_update(update_id, text, **kwargs)) return self.api.last_text def test_command_text_state_fallback(self): self.assertEqual(self.send('/start'), 'menu') self.assertEqual(self.send('ask'), 'name?') self.assertEqual(self.send('Bob'), 'hi Bob') self.assertEqual(self.send('whatever'), 'echo whatever') self.assertEqual(Chat.objects.get().data, {'name': 'Bob'}) def test_state_button_has_priority(self): self.assertEqual(self.send('отмена'), 'nothing to cancel') self.send('ask') self.assertEqual(self.send('отмена'), 'cancelled') self.assertEqual(Chat.objects.get().state, '') def test_global_button_works_in_state(self): self.send('ask') self.assertEqual(self.send('меню'), 'menu') def test_command_with_bot_name_and_args(self): self.assertEqual(self.send('/START@my_bot payload'), 'menu') def test_saves_user_chat_and_messages(self): self.send('/start') user = User.objects.get() self.assertEqual((user.tg_id, user.username), (7, 'user7')) chat = Chat.objects.get() self.assertEqual((chat.chat_id, chat.user, chat.type), (42, user, 'private')) self.assertEqual( list(Message.objects.order_by('pk').values_list('direction', 'text')), [('in', '/start'), ('out', 'menu')], ) def test_ignores_group_messages(self): self.assertIsNone(self.send('/start', chat_type='group')) self.assertFalse(Message.objects.exists()) def test_interface_switch_applies_immediately(self): self.bot.interface = interfaces.key(ChildInterface) self.assertEqual(self.send('/start'), 'child') self.assertEqual(self.send('меню'), 'menu') def test_no_interface(self): self.bot.interface = '' self.assertIsNone(self.send('/start')) self.assertEqual(Message.objects.count(), 1) def test_membership_marks_chat_deleted(self): handle_update(self.api, self.bot, member_update(1, 'member')) self.assertIsNone(Chat.objects.get().deleted_at) handle_update(self.api, self.bot, member_update(2, 'kicked')) self.assertIsNotNone(Chat.objects.get().deleted_at) self.assertEqual(Chat.objects.count(), 1) class PublishTests(TestCase): def setUp(self): self.bot = Bot.objects.create(token='1:test') self.api = mock.Mock() self.api.get_me.return_value = SimpleNamespace(username='my_bot', first_name='My') def publish(self): with mock.patch.object(Bot, 'api', return_value=self.api), \ mock.patch('Telegram.models.log_out_from_cloud') as log_out: self.bot.publish() return log_out @override_settings(TELEGRAM_API_URL='http://127.0.0.1:8081') def test_logs_out_from_cloud_once(self): self.assertEqual(self.publish().call_count, 1) self.bot.refresh_from_db() self.assertTrue(self.bot.cloud_logged_out) self.assertTrue(self.bot.is_active) self.assertEqual(self.bot.username, 'my_bot') self.assertEqual(self.publish().call_count, 0) @override_settings(TELEGRAM_API_URL='') def test_cloud_api_without_log_out(self): self.assertEqual(self.publish().call_count, 0) self.assertFalse(self.bot.cloud_logged_out) class InterfaceRegistryTests(TestCase): def test_registered_interfaces_are_choices(self): self.assertIn((interfaces.key(EchoInterface), 'Echo'), interfaces.choices()) class Unregistered(Interface): pass self.assertIsNone(interfaces.get(interfaces.key(Unregistered))) class AdminTests(TestCase): def test_bot_admin_pages(self): from django.contrib.auth import get_user_model admin = get_user_model().objects.create_superuser('root', 'secret-pass-123') self.client.force_login(admin) bot = Bot.objects.create(token='1:test') for url in ('/admin/Telegram/bot/', '/admin/Telegram/bot/add/', f'/admin/Telegram/bot/{bot.pk}/change/'): self.assertEqual(self.client.get(url).status_code, 200, url)