feat: add digest-gated internal main deployment
This commit is contained in:
@@ -111,6 +111,21 @@ class CeleryModuleTest(SimpleTestCase):
|
||||
|
||||
apply_async_mock.assert_not_called()
|
||||
|
||||
def test_startup_refresh_skips_when_disabled(self):
|
||||
with patch.dict(
|
||||
os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True
|
||||
), patch.object(sys, "argv", ["python", "manage.py", "shell"]):
|
||||
module = _load_module("isolated_core_celery_startup_disabled")
|
||||
|
||||
with patch.object(
|
||||
module,
|
||||
"settings",
|
||||
SimpleNamespace(CELERY_STARTUP_REFRESH_ENABLED=False),
|
||||
), patch.object(module.cache, "add") as add_mock:
|
||||
module._queue_startup_sources_refresh()
|
||||
|
||||
add_mock.assert_not_called()
|
||||
|
||||
def test_debug_task_prints_request(self):
|
||||
with patch.dict(
|
||||
os.environ, {"DJANGO_SETTINGS_MODULE": "settings.test"}, clear=True
|
||||
|
||||
264
tests/apps/core/test_cloned_environment_commands.py
Normal file
264
tests/apps/core/test_cloned_environment_commands.py
Normal file
@@ -0,0 +1,264 @@
|
||||
"""Regression tests for commands used by the internal-main restore wrapper."""
|
||||
|
||||
import json
|
||||
from datetime import date, timedelta
|
||||
from io import StringIO
|
||||
|
||||
from apps.core.management.commands.sanitize_cloned_environment import (
|
||||
CLONE_TERMINATION_MESSAGE,
|
||||
)
|
||||
from apps.core.models import BackgroundJob, JobStatus
|
||||
from apps.exchange.models import ExchangeConnection
|
||||
from apps.parsers.models import (
|
||||
CheckoCollectionAttempt,
|
||||
FinancialReport,
|
||||
FinancialReportLine,
|
||||
ParserLoadLog,
|
||||
)
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
||||
from organizations.models import Organization
|
||||
|
||||
|
||||
class SanitizeClonedEnvironmentCommandTest(TestCase):
|
||||
def _run_command(self) -> dict[str, int]:
|
||||
stdout = StringIO()
|
||||
call_command(
|
||||
"sanitize_cloned_environment",
|
||||
silent=True,
|
||||
stdout=stdout,
|
||||
)
|
||||
return json.loads(stdout.getvalue())
|
||||
|
||||
def test_sanitizes_only_environment_bound_and_in_flight_state_idempotently(self):
|
||||
interval = IntervalSchedule.objects.create(
|
||||
every=1,
|
||||
period=IntervalSchedule.HOURS,
|
||||
)
|
||||
enabled_schedule = PeriodicTask.objects.create(
|
||||
name="clone-enabled-schedule",
|
||||
task="tests.clone.enabled",
|
||||
interval=interval,
|
||||
enabled=True,
|
||||
)
|
||||
disabled_schedule = PeriodicTask.objects.create(
|
||||
name="clone-disabled-schedule",
|
||||
task="tests.clone.disabled",
|
||||
interval=interval,
|
||||
enabled=False,
|
||||
)
|
||||
|
||||
checked_at = timezone.now() - timedelta(hours=1)
|
||||
connection = ExchangeConnection.objects.create(
|
||||
server="database.internal",
|
||||
username="clone-user",
|
||||
password="environment-secret", # noqa: S106 - synthetic test value
|
||||
database_name="environment-db",
|
||||
is_active=True,
|
||||
last_checked_at=checked_at,
|
||||
last_error="old check error",
|
||||
)
|
||||
clean_connection = ExchangeConnection.objects.create(
|
||||
server="disabled.internal",
|
||||
username="disabled-user",
|
||||
password="",
|
||||
database_name="disabled-db",
|
||||
is_active=False,
|
||||
)
|
||||
|
||||
active_jobs = [
|
||||
BackgroundJob.objects.create(
|
||||
task_id=f"clone-{status}",
|
||||
task_name="tests.clone",
|
||||
status=status,
|
||||
result={"partial": status},
|
||||
)
|
||||
for status in (JobStatus.PENDING, JobStatus.STARTED, JobStatus.RETRY)
|
||||
]
|
||||
completed_jobs = [
|
||||
BackgroundJob.objects.create(
|
||||
task_id=f"clone-finished-{status}",
|
||||
task_name="tests.clone",
|
||||
status=status,
|
||||
result={"preserved": status},
|
||||
completed_at=checked_at,
|
||||
)
|
||||
for status in (JobStatus.SUCCESS, JobStatus.FAILURE, JobStatus.REVOKED)
|
||||
]
|
||||
|
||||
active_load_logs = [
|
||||
ParserLoadLog.objects.create(
|
||||
batch_id=index,
|
||||
source=ParserLoadLog.Source.INDUSTRIAL,
|
||||
status=status,
|
||||
records_count=index,
|
||||
)
|
||||
for index, status in enumerate(
|
||||
(ParserLoadLog.Status.PENDING, ParserLoadLog.Status.IN_PROGRESS),
|
||||
start=1,
|
||||
)
|
||||
]
|
||||
completed_load_log = ParserLoadLog.objects.create(
|
||||
batch_id=3,
|
||||
source=ParserLoadLog.Source.INDUSTRIAL,
|
||||
status=ParserLoadLog.Status.SUCCESS,
|
||||
records_count=37,
|
||||
)
|
||||
|
||||
organization = Organization.objects.create(name="Clone command test")
|
||||
active_attempt = CheckoCollectionAttempt.objects.create(
|
||||
organization=organization,
|
||||
source=CheckoCollectionAttempt.Source.ARBITRATION,
|
||||
period_month=date(2026, 8, 1),
|
||||
status=CheckoCollectionAttempt.Status.IN_PROGRESS,
|
||||
records_count=4,
|
||||
)
|
||||
completed_attempt = CheckoCollectionAttempt.objects.create(
|
||||
organization=organization,
|
||||
source=CheckoCollectionAttempt.Source.BANKRUPTCY,
|
||||
period_month=date(2026, 8, 1),
|
||||
status=CheckoCollectionAttempt.Status.SUCCESS,
|
||||
records_count=11,
|
||||
)
|
||||
|
||||
active_reports = [
|
||||
FinancialReport.objects.create(
|
||||
external_id=f"clone-active-{index}",
|
||||
ogrn=f"100000000000{index}",
|
||||
file_name=f"active-{index}.xlsx",
|
||||
file_hash=str(index) * 64,
|
||||
load_batch=index,
|
||||
status=status,
|
||||
source=FinancialReport.SourceType.API,
|
||||
)
|
||||
for index, status in enumerate(
|
||||
(FinancialReport.Status.PENDING, FinancialReport.Status.PROCESSING),
|
||||
start=1,
|
||||
)
|
||||
]
|
||||
completed_report = FinancialReport.objects.create(
|
||||
external_id="clone-completed",
|
||||
ogrn="1000000000003",
|
||||
file_name="completed.xlsx",
|
||||
file_hash="3" * 64,
|
||||
load_batch=3,
|
||||
status=FinancialReport.Status.SUCCESS,
|
||||
source=FinancialReport.SourceType.API,
|
||||
)
|
||||
completed_line = FinancialReportLine.objects.create(
|
||||
report=completed_report,
|
||||
form_code="1",
|
||||
line_code="1100",
|
||||
line_name="Preserved business data",
|
||||
year=2025,
|
||||
period_start=10,
|
||||
period_end=20,
|
||||
)
|
||||
|
||||
first_result = self._run_command()
|
||||
|
||||
self.assertEqual(
|
||||
first_result,
|
||||
{
|
||||
"background_jobs": 3,
|
||||
"checko_attempts": 1,
|
||||
"exchange_connections": 1,
|
||||
"financial_reports": 2,
|
||||
"parser_load_logs": 2,
|
||||
"periodic_tasks": 1,
|
||||
},
|
||||
)
|
||||
|
||||
enabled_schedule.refresh_from_db()
|
||||
disabled_schedule.refresh_from_db()
|
||||
self.assertFalse(enabled_schedule.enabled)
|
||||
self.assertFalse(disabled_schedule.enabled)
|
||||
|
||||
connection.refresh_from_db()
|
||||
self.assertFalse(connection.is_active)
|
||||
self.assertEqual(connection.password, "")
|
||||
self.assertIsNone(connection.last_checked_at)
|
||||
self.assertEqual(connection.last_error, "")
|
||||
clean_connection.refresh_from_db()
|
||||
self.assertEqual(clean_connection.password, "")
|
||||
|
||||
for job in active_jobs:
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.status, JobStatus.REVOKED)
|
||||
self.assertIsNotNone(job.completed_at)
|
||||
self.assertEqual(
|
||||
job.result, {"partial": job.task_id.removeprefix("clone-")}
|
||||
)
|
||||
for job in completed_jobs:
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.result, {"preserved": job.status})
|
||||
self.assertEqual(job.completed_at, checked_at)
|
||||
|
||||
for load_log in active_load_logs:
|
||||
load_log.refresh_from_db()
|
||||
self.assertEqual(load_log.status, ParserLoadLog.Status.FAILED)
|
||||
self.assertEqual(load_log.error_message, CLONE_TERMINATION_MESSAGE)
|
||||
completed_load_log.refresh_from_db()
|
||||
self.assertEqual(completed_load_log.status, ParserLoadLog.Status.SUCCESS)
|
||||
self.assertEqual(completed_load_log.records_count, 37)
|
||||
|
||||
active_attempt.refresh_from_db()
|
||||
self.assertEqual(active_attempt.status, CheckoCollectionAttempt.Status.FAILED)
|
||||
self.assertEqual(active_attempt.error_message, CLONE_TERMINATION_MESSAGE)
|
||||
completed_attempt.refresh_from_db()
|
||||
self.assertEqual(
|
||||
completed_attempt.status, CheckoCollectionAttempt.Status.SUCCESS
|
||||
)
|
||||
self.assertEqual(completed_attempt.records_count, 11)
|
||||
|
||||
for report in active_reports:
|
||||
report.refresh_from_db()
|
||||
self.assertEqual(report.status, FinancialReport.Status.FAILED)
|
||||
self.assertEqual(report.error_message, CLONE_TERMINATION_MESSAGE)
|
||||
completed_report.refresh_from_db()
|
||||
completed_line.refresh_from_db()
|
||||
self.assertEqual(completed_report.status, FinancialReport.Status.SUCCESS)
|
||||
self.assertEqual(completed_line.period_start, 10)
|
||||
self.assertEqual(completed_line.period_end, 20)
|
||||
|
||||
first_timestamps = {
|
||||
"connection": connection.updated_at,
|
||||
"job": active_jobs[0].updated_at,
|
||||
"load_log": active_load_logs[0].updated_at,
|
||||
"attempt": active_attempt.updated_at,
|
||||
"report": active_reports[0].updated_at,
|
||||
}
|
||||
second_result = self._run_command()
|
||||
|
||||
self.assertEqual(second_result, {key: 0 for key in first_result})
|
||||
connection.refresh_from_db()
|
||||
active_jobs[0].refresh_from_db()
|
||||
active_load_logs[0].refresh_from_db()
|
||||
active_attempt.refresh_from_db()
|
||||
active_reports[0].refresh_from_db()
|
||||
self.assertEqual(connection.updated_at, first_timestamps["connection"])
|
||||
self.assertEqual(active_jobs[0].updated_at, first_timestamps["job"])
|
||||
self.assertEqual(active_load_logs[0].updated_at, first_timestamps["load_log"])
|
||||
self.assertEqual(active_attempt.updated_at, first_timestamps["attempt"])
|
||||
self.assertEqual(active_reports[0].updated_at, first_timestamps["report"])
|
||||
|
||||
|
||||
class ValidateClonedMigrationsCommandTest(TestCase):
|
||||
def test_accepts_database_when_all_applied_migrations_exist_in_image(self):
|
||||
call_command("validate_cloned_migrations", stdout=StringIO())
|
||||
|
||||
def test_rejects_applied_migration_absent_from_image(self):
|
||||
MigrationRecorder.Migration.objects.create(
|
||||
app="removed_clone_app",
|
||||
name="9999_unknown_migration",
|
||||
)
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
CommandError,
|
||||
"removed_clone_app.9999_unknown_migration",
|
||||
):
|
||||
call_command("validate_cloned_migrations", stdout=StringIO())
|
||||
29
tests/settings/test_base_settings.py
Normal file
29
tests/settings/test_base_settings.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from settings.base import _env_bool
|
||||
|
||||
|
||||
def test_env_bool_uses_explicit_default(monkeypatch):
|
||||
monkeypatch.delenv("TEST_BOOLEAN_FLAG", raising=False)
|
||||
|
||||
assert _env_bool("TEST_BOOLEAN_FLAG", default=True) is True
|
||||
assert _env_bool("TEST_BOOLEAN_FLAG", default=False) is False
|
||||
|
||||
|
||||
def test_env_bool_accepts_supported_false_values(monkeypatch):
|
||||
for value in ("0", "false", "NO", " off "):
|
||||
monkeypatch.setenv("TEST_BOOLEAN_FLAG", value)
|
||||
assert _env_bool("TEST_BOOLEAN_FLAG", default=True) is False
|
||||
|
||||
|
||||
def test_env_bool_accepts_supported_true_values(monkeypatch):
|
||||
for value in ("1", "true", "YES", " on "):
|
||||
monkeypatch.setenv("TEST_BOOLEAN_FLAG", value)
|
||||
assert _env_bool("TEST_BOOLEAN_FLAG", default=False) is True
|
||||
|
||||
|
||||
def test_env_bool_rejects_ambiguous_values(monkeypatch):
|
||||
monkeypatch.setenv("TEST_BOOLEAN_FLAG", "enabled")
|
||||
|
||||
with pytest.raises(ImproperlyConfigured, match="TEST_BOOLEAN_FLAG must be one of"):
|
||||
_env_bool("TEST_BOOLEAN_FLAG", default=True)
|
||||
Reference in New Issue
Block a user