Add periodic exchange task management API
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Successful in 3m16s
CI/CD Pipeline / Run Tests (push) Successful in 3m26s
CI/CD Pipeline / Telegram Notify Success (push) Failing after 1m29s
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m44s
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 20m19s
CI/CD Pipeline / Telegram Notify Success (pull_request) Failing after 1m34s
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Successful in 3m16s
CI/CD Pipeline / Run Tests (push) Successful in 3m26s
CI/CD Pipeline / Telegram Notify Success (push) Failing after 1m29s
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m44s
CI/CD Pipeline / Code Quality Checks (pull_request) Successful in 20m19s
CI/CD Pipeline / Telegram Notify Success (pull_request) Failing after 1m34s
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
"""Tests for exchange serializers."""
|
||||
|
||||
from apps.exchange.serializers import ExchangeCopyRequestSerializer
|
||||
from types import SimpleNamespace
|
||||
|
||||
from apps.exchange.serializers import (
|
||||
ExchangeCopyRequestSerializer,
|
||||
ExchangePeriodicTaskUpsertSerializer,
|
||||
)
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
|
||||
@@ -27,3 +32,61 @@ class ExchangeCopyRequestSerializerTest(SimpleTestCase):
|
||||
)
|
||||
self.assertFalse(serializer_with_tables.is_valid())
|
||||
self.assertIn("tables", serializer_with_tables.errors)
|
||||
|
||||
|
||||
class ExchangePeriodicTaskUpsertSerializerTest(SimpleTestCase):
|
||||
def test_interval_schedule_requires_fields(self):
|
||||
serializer = ExchangePeriodicTaskUpsertSerializer(
|
||||
data={"name": "copy-job", "schedule_type": "interval"}
|
||||
)
|
||||
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("interval_every", serializer.errors)
|
||||
self.assertIn("interval_period", serializer.errors)
|
||||
|
||||
def test_crontab_schedule_requires_fields(self):
|
||||
serializer = ExchangePeriodicTaskUpsertSerializer(
|
||||
data={"name": "copy-job", "schedule_type": "crontab"}
|
||||
)
|
||||
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("crontab_minute", serializer.errors)
|
||||
self.assertIn("crontab_hour", serializer.errors)
|
||||
|
||||
def test_update_mode_to_all_clears_old_single_table(self):
|
||||
instance = SimpleNamespace(
|
||||
interval_id=1,
|
||||
interval=SimpleNamespace(every=5, period="minutes"),
|
||||
crontab_id=None,
|
||||
kwargs='{"payload": {"mode": "single", "table": "old_table"}}',
|
||||
)
|
||||
serializer = ExchangePeriodicTaskUpsertSerializer(
|
||||
instance,
|
||||
data={"mode": "all"},
|
||||
partial=True,
|
||||
)
|
||||
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
self.assertEqual(
|
||||
serializer.validated_data["payload"],
|
||||
{
|
||||
"mode": "all",
|
||||
"table": None,
|
||||
"tables": None,
|
||||
"truncate_before_copy": True,
|
||||
},
|
||||
)
|
||||
|
||||
def test_periodic_task_uses_copy_payload_validation(self):
|
||||
serializer = ExchangePeriodicTaskUpsertSerializer(
|
||||
data={
|
||||
"name": "copy-job",
|
||||
"schedule_type": "interval",
|
||||
"interval_every": 1,
|
||||
"interval_period": "hours",
|
||||
"mode": "single",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("table", serializer.errors)
|
||||
|
||||
@@ -2,11 +2,51 @@ from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from apps.exchange.tasks import copy_parsers_data_async
|
||||
from apps.exchange.services import ExchangeServiceError
|
||||
from apps.exchange.tasks import copy_parsers_data_async, dispatch_periodic_exchange_copy
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
|
||||
class ExchangeTasksTest(SimpleTestCase):
|
||||
def test_dispatch_periodic_exchange_copy_enqueues_copy_with_active_connection(self):
|
||||
active_connection = MagicMock(id=15)
|
||||
|
||||
with patch(
|
||||
"apps.exchange.tasks.ExchangeConnectionService.get_active_connection",
|
||||
return_value=active_connection,
|
||||
) as get_connection_mock, patch(
|
||||
"apps.exchange.tasks.copy_parsers_data_async.delay",
|
||||
return_value=MagicMock(id="task-15"),
|
||||
) as delay_mock:
|
||||
result = dispatch_periodic_exchange_copy.run(
|
||||
payload={"mode": "all", "truncate_before_copy": True}
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
{
|
||||
"status": "queued",
|
||||
"task_id": "task-15",
|
||||
"connection_id": 15,
|
||||
},
|
||||
)
|
||||
get_connection_mock.assert_called_once_with()
|
||||
delay_mock.assert_called_once_with(
|
||||
connection_id=15,
|
||||
payload={"mode": "all", "truncate_before_copy": True},
|
||||
requested_by_id=None,
|
||||
)
|
||||
|
||||
def test_dispatch_periodic_exchange_copy_fails_without_active_connection(self):
|
||||
with patch(
|
||||
"apps.exchange.tasks.ExchangeConnectionService.get_active_connection",
|
||||
side_effect=ExchangeServiceError("Активное подключение не найдено"),
|
||||
), self.assertRaisesMessage(
|
||||
ExchangeServiceError,
|
||||
"Активное подключение не найдено",
|
||||
):
|
||||
dispatch_periodic_exchange_copy.run(payload={"mode": "all"})
|
||||
|
||||
def test_copy_parsers_data_async_completes_with_existing_job(self):
|
||||
background_job = MagicMock()
|
||||
connection = MagicMock()
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Tests for exchange API views."""
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from apps.exchange.models import ExchangeConnection
|
||||
from apps.exchange.services import ExchangeServiceError
|
||||
from apps.exchange.services import ExchangePeriodicTaskService, ExchangeServiceError
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
@@ -20,6 +23,7 @@ class ExchangeViewsTest(APITestCase):
|
||||
self.connections_url = reverse("api_v1:exchange:connections")
|
||||
self.test_connection_url = reverse("api_v1:exchange:connections-test")
|
||||
self.copy_url = reverse("api_v1:exchange:copy")
|
||||
self.periodic_tasks_url = reverse("api_v1:exchange:periodic-tasks")
|
||||
|
||||
def test_connections_endpoint_admin_only(self):
|
||||
response = self.client.get(self.connections_url)
|
||||
@@ -164,3 +168,141 @@ class ExchangeViewsTest(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn("table", str(response.data))
|
||||
|
||||
def test_periodic_tasks_endpoint_admin_only(self):
|
||||
response = self.client.get(self.periodic_tasks_url)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
self.client.force_authenticate(self.user)
|
||||
response = self.client.get(self.periodic_tasks_url)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
self.client.force_authenticate(self.admin)
|
||||
response = self.client.get(self.periodic_tasks_url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data["success"])
|
||||
self.assertEqual(response.data["data"], [])
|
||||
|
||||
def test_create_periodic_interval_task_success(self):
|
||||
payload = {
|
||||
"name": "exchange-copy-hourly",
|
||||
"description": "Hourly sync",
|
||||
"enabled": True,
|
||||
"schedule_type": "interval",
|
||||
"interval_every": 1,
|
||||
"interval_period": "hours",
|
||||
"mode": "all",
|
||||
}
|
||||
|
||||
self.client.force_authenticate(self.admin)
|
||||
response = self.client.post(self.periodic_tasks_url, payload, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
task = PeriodicTask.objects.get(id=response.data["data"]["id"])
|
||||
self.assertEqual(task.task, ExchangePeriodicTaskService.TASK_NAME)
|
||||
self.assertEqual(response.data["data"]["schedule_type"], "interval")
|
||||
self.assertEqual(response.data["data"]["interval_every"], 1)
|
||||
self.assertEqual(response.data["data"]["interval_period"], "hours")
|
||||
self.assertEqual(
|
||||
json.loads(task.kwargs),
|
||||
{
|
||||
"payload": {
|
||||
"mode": "all",
|
||||
"table": None,
|
||||
"tables": None,
|
||||
"truncate_before_copy": True,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def test_list_periodic_tasks_returns_only_exchange_tasks(self):
|
||||
interval = IntervalSchedule.objects.create(every=1, period="hours")
|
||||
PeriodicTask.objects.create(
|
||||
name="exchange-copy-hourly",
|
||||
task=ExchangePeriodicTaskService.TASK_NAME,
|
||||
interval=interval,
|
||||
kwargs=json.dumps(
|
||||
{"payload": {"mode": "all", "truncate_before_copy": True}}
|
||||
),
|
||||
)
|
||||
PeriodicTask.objects.create(
|
||||
name="another-task",
|
||||
task="apps.parsers.tasks.fake_task",
|
||||
interval=interval,
|
||||
kwargs="{}",
|
||||
)
|
||||
|
||||
self.client.force_authenticate(self.admin)
|
||||
response = self.client.get(self.periodic_tasks_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data["data"]), 1)
|
||||
self.assertEqual(response.data["data"][0]["name"], "exchange-copy-hourly")
|
||||
|
||||
def test_update_periodic_task_switches_to_crontab(self):
|
||||
interval = IntervalSchedule.objects.create(every=1, period="hours")
|
||||
task = PeriodicTask.objects.create(
|
||||
name="exchange-copy-hourly",
|
||||
task=ExchangePeriodicTaskService.TASK_NAME,
|
||||
description="Hourly sync",
|
||||
enabled=True,
|
||||
interval=interval,
|
||||
kwargs=json.dumps(
|
||||
{
|
||||
"payload": {
|
||||
"mode": "all",
|
||||
"table": None,
|
||||
"tables": None,
|
||||
"truncate_before_copy": True,
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
detail_url = reverse(
|
||||
"api_v1:exchange:periodic-task-detail",
|
||||
kwargs={"task_id": task.id},
|
||||
)
|
||||
payload = {
|
||||
"schedule_type": "crontab",
|
||||
"crontab_minute": "0",
|
||||
"crontab_hour": "4",
|
||||
"crontab_day_of_week": "*",
|
||||
"crontab_day_of_month": "*",
|
||||
"crontab_month_of_year": "*",
|
||||
"mode": "single",
|
||||
"table": "parsers_proxy",
|
||||
"enabled": False,
|
||||
}
|
||||
|
||||
self.client.force_authenticate(self.admin)
|
||||
response = self.client.patch(detail_url, payload, format="json")
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
task.refresh_from_db()
|
||||
self.assertIsNone(task.interval)
|
||||
self.assertIsNotNone(task.crontab)
|
||||
self.assertEqual(str(task.crontab.timezone), settings.TIME_ZONE)
|
||||
self.assertFalse(task.enabled)
|
||||
self.assertEqual(response.data["data"]["schedule_type"], "crontab")
|
||||
self.assertEqual(response.data["data"]["crontab_hour"], "4")
|
||||
self.assertEqual(response.data["data"]["mode"], "single")
|
||||
self.assertEqual(response.data["data"]["table"], "parsers_proxy")
|
||||
self.assertFalse(IntervalSchedule.objects.filter(id=interval.id).exists())
|
||||
|
||||
def test_periodic_task_detail_returns_404_for_non_exchange_task(self):
|
||||
interval = IntervalSchedule.objects.create(every=1, period="hours")
|
||||
task = PeriodicTask.objects.create(
|
||||
name="another-task",
|
||||
task="apps.parsers.tasks.fake_task",
|
||||
interval=interval,
|
||||
kwargs="{}",
|
||||
)
|
||||
detail_url = reverse(
|
||||
"api_v1:exchange:periodic-task-detail",
|
||||
kwargs={"task_id": task.id},
|
||||
)
|
||||
|
||||
self.client.force_authenticate(self.admin)
|
||||
response = self.client.get(detail_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
Reference in New Issue
Block a user