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
238 lines
9.1 KiB
Python
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"],
|
|
)
|