Files
mostovik-backend/tests/apps/backups/test_views.py
Aleksandr Meshchriakov 3d298ce352
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Telegram Notify Success (push) Has been cancelled
CI/CD Pipeline / Run Tests (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (push) Has been cancelled
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 2m54s
CI/CD Pipeline / Telegram Notify Success (pull_request) Has been skipped
feat: expand platform APIs, sources, and test coverage
2026-03-17 12:56:48 +01:00

238 lines
9.1 KiB
Python

"""Tests for async backups export API."""
from __future__ import annotations
import hashlib
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import patch
from apps.backups.models import BackupExportJob
from apps.backups.services import (
BackupExportError,
BackupExportJobService,
BackupRequestResult,
)
from django.db import IntegrityError
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
from tests.apps.user.factories import UserFactory
class BackupExportViewTest(APITestCase):
"""Tests for async backup export endpoint."""
def setUp(self):
self.user = UserFactory.create_user()
self.admin = UserFactory.create_superuser()
self.url = reverse("api_v1:backups:export")
def test_export_admin_only(self):
response = self.client.post(self.url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.client.force_authenticate(self.user)
response = self.client.post(self.url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_starts_job_when_absent(self, enqueue_mock):
self.client.force_authenticate(self.admin)
today = timezone.localdate()
with self.captureOnCommitCallbacks(execute=True):
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "started")
job = BackupExportJob.objects.get(actual_date=today)
self.assertEqual(job.status, BackupExportJob.Status.PENDING)
self.assertEqual(response.data["task_id"], job.task_id)
enqueue_mock.assert_called_once_with(job_id=job.id, task_id=job.task_id)
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_returns_wait_when_job_in_progress(self, enqueue_mock):
today = timezone.localdate()
BackupExportJob.objects.create(
actual_date=today,
status=BackupExportJob.Status.STARTED,
task_id="task-running-1",
)
self.client.force_authenticate(self.admin)
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "wait")
self.assertEqual(response.data["task_id"], "task-running-1")
enqueue_mock.assert_not_called()
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_returns_file_and_deletes_after_download(self, enqueue_mock):
with TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
archive_bytes = b"zip-content"
archive_path = tmp_path / "backup.zip"
archive_path.write_bytes(archive_bytes)
today = timezone.localdate()
job = BackupExportJob.objects.create(
actual_date=today,
status=BackupExportJob.Status.SUCCESS,
task_id="task-success-1",
archive_path=str(archive_path),
archive_filename="backup.zip",
checksum_filename="backup.zip.sha256",
checksum_sha256=hashlib.sha256(archive_bytes).hexdigest(),
organizations_count=7,
)
self.client.force_authenticate(self.admin)
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.content, archive_bytes)
self.assertEqual(response["Content-Type"], "application/zip")
self.assertIn(
'attachment; filename="backup.zip"', response["Content-Disposition"]
)
self.assertEqual(response["X-Backup-Organizations"], "7")
self.assertEqual(
response["X-Backup-SHA256"],
hashlib.sha256(archive_bytes).hexdigest(),
)
self.assertFalse(archive_path.exists())
self.assertFalse(BackupExportJob.objects.filter(id=job.id).exists())
enqueue_mock.assert_not_called()
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_export_restarts_when_success_job_has_no_file(self, enqueue_mock):
today = timezone.localdate()
with TemporaryDirectory() as tmp_dir:
missing_archive_path = Path(tmp_dir) / "non-existent-backup.zip"
BackupExportJob.objects.create(
actual_date=today,
status=BackupExportJob.Status.SUCCESS,
task_id="task-old",
archive_path=str(missing_archive_path),
archive_filename="non-existent-backup.zip",
)
self.client.force_authenticate(self.admin)
with self.captureOnCommitCallbacks(execute=True):
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "started")
restarted_job = BackupExportJob.objects.get(actual_date=today)
self.assertEqual(response.data["task_id"], restarted_job.task_id)
enqueue_mock.assert_called_once_with(
job_id=restarted_job.id,
task_id=restarted_job.task_id,
)
@patch("apps.backups.services.BackupExportJob.objects.create")
def test_check_or_start_job_handles_integrity_race(self, create_mock):
today = timezone.localdate()
concurrent_job = BackupExportJob(
actual_date=today,
status=BackupExportJob.Status.STARTED,
task_id="task-race-running",
)
concurrent_job.save()
create_mock.side_effect = IntegrityError("duplicate key value")
with patch.object(
BackupExportJobService,
"_get_job_for_update",
side_effect=[None, concurrent_job],
):
result = BackupExportJobService.check_or_start_job(
actual_date=today,
requested_by_id=self.admin.id,
)
self.assertEqual(result.action, "wait")
self.assertEqual(result.task_id, "task-race-running")
@patch("apps.backups.services.BackupExportJobService._enqueue_backup_task")
def test_check_or_start_job_enqueues_on_commit(self, enqueue_mock):
today = timezone.localdate()
with self.captureOnCommitCallbacks(execute=False) as callbacks:
result = BackupExportJobService.check_or_start_job(
actual_date=today,
requested_by_id=self.admin.id,
)
self.assertEqual(result.action, "started")
self.assertEqual(len(callbacks), 1)
enqueue_mock.assert_not_called()
job = BackupExportJob.objects.get(actual_date=today)
self.assertEqual(job.task_id, result.task_id)
callbacks[0]()
enqueue_mock.assert_called_once_with(job_id=job.id, task_id=job.task_id)
@patch("apps.backups.views.BackupExportJobService.check_or_start_job")
def test_export_returns_400_when_job_start_fails(self, check_or_start_mock):
self.client.force_authenticate(self.admin)
check_or_start_mock.side_effect = BackupExportError("broken")
response = self.client.post(self.url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data["errors"][0]["details"]["fields"]["backup"], ["broken"]
)
@patch("apps.backups.views.BackupExportJobService.consume_ready_archive")
@patch("apps.backups.views.BackupExportJobService.check_or_start_job")
def test_export_returns_400_when_archive_consumption_fails(
self,
check_or_start_mock,
consume_mock,
):
today = timezone.localdate()
self.client.force_authenticate(self.admin)
check_or_start_mock.return_value = BackupRequestResult(
action="download",
message="ready",
actual_date=today,
task_id="task-ready",
)
consume_mock.side_effect = BackupExportError("missing archive")
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data["errors"][0]["details"]["fields"]["backup"],
["missing archive"],
)