feat: expand platform APIs, sources, and test coverage
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

This commit is contained in:
2026-03-17 12:56:48 +01:00
parent b505c67968
commit 3d298ce352
101 changed files with 8387 additions and 292 deletions

View File

@@ -5,10 +5,14 @@ from __future__ import annotations
import hashlib
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import Mock, patch
from unittest.mock import patch
from apps.backups.models import BackupExportJob
from apps.backups.services import BackupExportJobService
from apps.backups.services import (
BackupExportError,
BackupExportJobService,
BackupRequestResult,
)
from django.db import IntegrityError
from django.urls import reverse
from django.utils import timezone
@@ -34,29 +38,27 @@ class BackupExportViewTest(APITestCase):
response = self.client.post(self.url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
@patch("apps.backups.tasks.generate_backup_for_date.delay")
def test_export_starts_job_when_absent(self, delay_mock):
delay_mock.return_value = Mock(id="task-backup-1")
@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()
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
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")
self.assertEqual(response.data["task_id"], "task-backup-1")
job = BackupExportJob.objects.get(actual_date=today)
self.assertEqual(job.status, BackupExportJob.Status.PENDING)
self.assertEqual(job.task_id, "task-backup-1")
delay_mock.assert_called_once_with(job_id=job.id)
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.tasks.generate_backup_for_date.delay")
def test_export_returns_wait_when_job_in_progress(self, delay_mock):
@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,
@@ -74,10 +76,10 @@ class BackupExportViewTest(APITestCase):
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["status"], "wait")
self.assertEqual(response.data["task_id"], "task-running-1")
delay_mock.assert_not_called()
enqueue_mock.assert_not_called()
@patch("apps.backups.tasks.generate_backup_for_date.delay")
def test_export_returns_file_and_deletes_after_download(self, delay_mock):
@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"
@@ -106,7 +108,9 @@ class BackupExportViewTest(APITestCase):
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.assertIn(
'attachment; filename="backup.zip"', response["Content-Disposition"]
)
self.assertEqual(response["X-Backup-Organizations"], "7")
self.assertEqual(
response["X-Backup-SHA256"],
@@ -115,11 +119,10 @@ class BackupExportViewTest(APITestCase):
self.assertFalse(archive_path.exists())
self.assertFalse(BackupExportJob.objects.filter(id=job.id).exists())
delay_mock.assert_not_called()
enqueue_mock.assert_not_called()
@patch("apps.backups.tasks.generate_backup_for_date.delay")
def test_export_restarts_when_success_job_has_no_file(self, delay_mock):
delay_mock.return_value = Mock(id="task-backup-retry")
@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"
@@ -132,16 +135,21 @@ class BackupExportViewTest(APITestCase):
)
self.client.force_authenticate(self.admin)
response = self.client.post(
self.url,
{"actual_date": today.isoformat()},
format="json",
)
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")
self.assertEqual(response.data["task_id"], "task-backup-retry")
delay_mock.assert_called_once()
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):
@@ -166,3 +174,64 @@ class BackupExportViewTest(APITestCase):
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"],
)