chore: переезд на serWebhook

This commit is contained in:
protokey committed 2026-09-26 18:20:35 +04:00
1 parent 45e7a1d22f
commit f1414e5ace
42 files changed
+1391 -224

No files matched your search

+186 -13
View File
@@ -1,11 +1,14 @@
import datetime
from unittest import mock
from django.test import TestCase
from django.utils import timezone
from dramatiq.middleware.time_limit import TimeLimitExceeded
from Telegram import interfaces
from Telegram.models import Bot, Chat, User as TelegramUser
from Telegram.testing import Conversation, FakeApi
from . import fields, tasks
from . import fields, services, tasks
from .bot import texts
from .bot.interface import MyPointInterface, is_proxy_url, parse_credentials
from .models import Credentials, Point, Server
@@ -83,7 +86,9 @@ class AddServerTests(BotTestCase):
self.assertEqual((point.mask.address, point.mask.root_password), ('10.0.0.2', 'mask-pw'))
self.assertEqual((point.domain, point.outbound_proxy), ('example.com', 'cf_warp'))
self.assertEqual(point.owner, TelegramUser.objects.get())
send.assert_called_once_with(point.pk, Chat.objects.get().pk)
send.assert_called_once_with(point.pk, mock.ANY)
point.refresh_from_db()
self.assertEqual((point.task_chat, point.task_token), (Chat.objects.get(), send.call_args.args[1]))
# сообщения с паролями удалены из чата
self.assertEqual(len(self.api.deleted), 2)
self.assertEqual(Chat.objects.get().state, '')
@@ -158,7 +163,7 @@ class PointMenuTests(BotTestCase):
self.assertIn(self.point.name, self.say(texts.NO))
self.assertIn('Продолжить?', self.say(texts.RELEASE))
self.assertEqual(self.say(texts.YES), texts.RELEASE_STARTED)
send.assert_called_once_with(self.point.pk, Chat.objects.get(user__tg_id=7).pk)
send.assert_called_once_with(self.point.pk, mock.ANY)
self.point.refresh_from_db()
self.assertEqual(self.point.status, Point.Status.RELEASING)
@@ -311,8 +316,9 @@ class PointSettingsTests(BotTestCase):
self.point.outbound_proxy = 'socks5://user:secret@proxy.example:1080'
self.point.save()
card = self.say(texts.SERVER_MENU)
self.assertIn('socks5://user:***@proxy.example:1080', card)
self.assertIn('socks5://***@proxy.example:1080', card)
self.assertNotIn('secret', card)
self.assertNotIn('user', card)
class TasksTests(TestCase):
@@ -323,13 +329,21 @@ class TasksTests(TestCase):
self.point = create_point(owner)
self.api = FakeApi()
def run_task(self, actor, *args, runner=None):
with patch_runner(runner or FakeRunner()), mock.patch.object(Bot, 'api', return_value=self.api):
actor.fn(*args)
def run_task(self, actor, *args, status=Point.Status.INSTALLING, runner=None):
"""Запускает задачу, как бот, и выполняет ее сообщение."""
self.point.refresh_from_db()
with mock.patch.object(actor, 'send') as send:
tasks.start(actor, self.point, status, self.chat, *args)
with (
patch_runner(runner or FakeRunner()),
mock.patch.object(Bot, 'api', return_value=self.api),
mock.patch.object(tasks.check_point, 'send'),
):
actor.fn(*send.call_args.args)
return self.api.last_text
def test_install_notifies_success(self):
text = self.run_task(tasks.install_point, self.point.pk, self.chat.pk)
text = self.run_task(tasks.install_point)
self.assertIn(self.point.name, text)
self.assertIn('10.0.0.2', text)
self.assertIn('готов к работе', text)
@@ -337,12 +351,12 @@ class TasksTests(TestCase):
def test_install_notifies_failure(self):
with self.assertLogs('App.tasks', 'ERROR'):
text = self.run_task(tasks.install_point, self.point.pk, self.chat.pk, runner=FakeRunner(fail='xui'))
text = self.run_task(tasks.install_point, runner=FakeRunner(fail='xui'))
self.assertIn('Не удалось', text)
self.assertIn('boom', text)
def test_release_notifies_passwords(self):
text = self.run_task(tasks.release_point, self.point.pk, self.chat.pk)
text = self.run_task(tasks.release_point, status=Point.Status.RELEASING)
self.assertIn('10.0.0.1 - пароль root', text)
self.assertFalse(Point.objects.exists())
@@ -351,10 +365,169 @@ class TasksTests(TestCase):
self.point.status = Point.Status.READY
self.point.save()
proxy = fields.encrypt('socks5://user:secret@proxy.example:1080')
text = self.run_task(tasks.set_outbound_proxy, self.point.pk, self.chat.pk, proxy)
self.assertIn('user:***@proxy.example', text)
text = self.run_task(tasks.set_outbound_proxy, proxy, status=Point.Status.UPDATING)
self.assertIn('socks5://***@proxy.example', text)
with self.assertLogs('App.tasks', 'ERROR'):
text = self.run_task(tasks.set_domain, self.point.pk, self.chat.pk, 'vpn.example.com', runner=FakeRunner(fail='xui_cert'))
text = self.run_task(
tasks.set_domain, 'vpn.example.com', status=Point.Status.UPDATING, runner=FakeRunner(fail='xui_cert'),
)
self.assertIn('прежними настройками', text)
self.point.refresh_from_db()
self.assertEqual((self.point.status, self.point.domain), (Point.Status.READY, ''))
class SupervisorTests(TestCase):
"""Задача, которая прервалась, не оставляет точку занятой навсегда."""
def setUp(self):
bot = Bot.objects.create(token='1:test')
owner = TelegramUser.objects.create(tg_id=1)
self.chat = Chat.objects.create(bot=bot, chat_id=1, type='private', user=owner)
self.point = create_point(owner)
self.api = FakeApi()
patcher = mock.patch.object(Bot, 'api', return_value=self.api)
patcher.start()
self.addCleanup(patcher.stop)
def start(self, actor=tasks.install_point, status=Point.Status.INSTALLING, *args):
with mock.patch.object(actor, 'send') as send, mock.patch.object(tasks.supervise, 'send_with_options') as sup:
tasks.start(actor, self.point, status, self.chat, *args)
sup.assert_called_once_with(args=(self.point.pk, send.call_args.args[1]), delay=tasks.SUPERVISE_DELAY_MS)
return send.call_args.args
def supervise(self, token):
with mock.patch.object(tasks.supervise, 'send_with_options') as again:
tasks.supervise.fn(self.point.pk, token)
self.point.refresh_from_db()
return again.called
def age_heartbeat(self, delta):
Point.objects.filter(pk=self.point.pk).update(task_heartbeat=timezone.now() - delta)
def test_stuck_task_is_aborted(self):
_, token = self.start()
Point.objects.claim_task(self.point.pk, token)
self.assertTrue(self.supervise(token))
self.age_heartbeat(tasks.TASK_STALE + datetime.timedelta(seconds=1))
self.assertFalse(self.supervise(token))
self.assertEqual(self.point.status, Point.Status.ERROR)
self.assertIn('Переустановить', self.point.error)
self.assertEqual(self.point.task_token, '')
self.assertIn('установка прервалась', self.api.last_text)
def test_queued_task_waits_longer(self):
_, token = self.start()
self.age_heartbeat(tasks.TASK_STALE * 2)
self.assertTrue(self.supervise(token))
self.age_heartbeat(tasks.QUEUE_TIMEOUT + datetime.timedelta(seconds=1))
self.assertFalse(self.supervise(token))
self.assertEqual(self.point.status, Point.Status.ERROR)
# сообщение все же дошло: задача снята, работы нет
runner = FakeRunner()
with patch_runner(runner):
tasks.install_point.fn(self.point.pk, token)
self.assertEqual(runner.calls, [])
def test_interrupted_update_keeps_point_ready(self):
token = self.start(tasks.set_domain, Point.Status.UPDATING, 'vpn.example.com')[1]
Point.objects.claim_task(self.point.pk, token)
self.age_heartbeat(tasks.TASK_STALE * 2)
self.supervise(token)
self.assertEqual((self.point.status, self.point.domain), (Point.Status.READY, ''))
self.assertIn('прервалась', self.point.error)
def test_supervisor_stops_after_task(self):
args = self.start()
with patch_runner(FakeRunner()), mock.patch.object(tasks.check_point, 'send'):
tasks.install_point.fn(*args)
self.assertFalse(self.supervise(args[1]))
self.assertEqual((self.point.status, self.point.task_token), (Point.Status.READY, ''))
def test_redelivered_message_is_not_repeated(self):
# воркер упал посреди установки, RabbitMQ доставил сообщение снова
args = self.start()
Point.objects.claim_task(*args)
runner = FakeRunner()
with patch_runner(runner):
tasks.install_point.fn(*args)
self.assertEqual(runner.calls, [])
self.point.refresh_from_db()
self.assertEqual(self.point.status, Point.Status.ERROR)
def test_time_limit_aborts_task(self):
args = self.start()
with (
mock.patch.object(services, 'install_point', side_effect=TimeLimitExceeded),
self.assertRaises(TimeLimitExceeded),
self.assertLogs('App.tasks', 'ERROR'),
):
tasks.install_point.fn(*args)
self.point.refresh_from_db()
self.assertEqual((self.point.status, self.point.task_token), (Point.Status.ERROR, ''))
def test_failed_notification_keeps_result(self):
args = self.start()
with (
patch_runner(FakeRunner()),
mock.patch.object(tasks, 'schedule_checks', side_effect=RuntimeError('boom')),
self.assertRaises(RuntimeError),
self.assertLogs('App.tasks', 'ERROR'),
):
tasks.install_point.fn(*args)
self.point.refresh_from_db()
self.assertEqual((self.point.status, self.point.task_token), (Point.Status.READY, ''))
class CheckTests(TestCase):
def setUp(self):
bot = Bot.objects.create(token='1:test')
owner = TelegramUser.objects.create(tg_id=1)
Chat.objects.create(bot=bot, chat_id=1, type='private', user=owner)
self.point = create_point(owner)
self.point.status = Point.Status.READY
self.point.save()
self.api = FakeApi()
patcher = mock.patch.object(Bot, 'api', return_value=self.api)
patcher.start()
self.addCleanup(patcher.stop)
with mock.patch.object(tasks.check_point, 'send') as send:
tasks.schedule_checks(self.point)
self.token = send.call_args.args[1]
def check(self, problem='', token=None):
with (
mock.patch.object(services, 'check_point', return_value=problem) as check,
mock.patch.object(tasks.check_point, 'send_with_options') as again,
):
tasks.check_point.fn(self.point.pk, token or self.token)
self.point.refresh_from_db()
return check.called, again.called
def test_checks_once_a_day_and_reports_changes(self):
self.assertEqual(self.check('панель не отвечает'), (True, True))
self.assertEqual(self.point.health_error, 'панель не отвечает')
self.assertIn('панель не отвечает', self.api.last_text)
self.assertIn('Проверка: панель не отвечает', texts.point_card(self.point))
# до следующих суток не проверяет, но цепочка живет
self.assertEqual(self.check(), (False, True))
Point.objects.filter(pk=self.point.pk).update(checked_at=timezone.now() - tasks.CHECK_INTERVAL)
sent = len(self.api.sent)
self.check('панель не отвечает')
self.assertEqual(len(self.api.sent), sent)
Point.objects.filter(pk=self.point.pk).update(checked_at=timezone.now() - tasks.CHECK_INTERVAL)
self.check()
self.assertIn('снова все в порядке', self.api.last_text)
def test_busy_point_is_not_checked(self):
self.point.set_status(Point.Status.INSTALLING)
self.assertEqual(self.check(), (False, True))
def test_old_chain_stops(self):
with mock.patch.object(tasks.check_point, 'send'):
tasks.schedule_checks(self.point)
self.assertEqual(self.check(token=self.token), (False, False))
point_id = self.point.pk
self.point.delete()
with mock.patch.object(tasks.check_point, 'send_with_options') as again:
tasks.check_point.fn(point_id, self.token)
again.assert_not_called()