feat: add digest-gated internal main deployment
Some checks failed
Mostovik Backend CI/CD / Tests and lint (push) Failing after 5s
Mostovik Backend CI/CD / Build linux/amd64 release images (push) Has been skipped
Mostovik Backend CI/CD / Deploy dev (push) Has been skipped
Mostovik Backend CI/CD / Deploy and verify internal main (push) Has been skipped
Mostovik Backend CI/CD / Deploy customer main (push) Has been skipped

This commit is contained in:
2026-08-27 12:49:43 +03:00
parent 7a8e5765ad
commit c884b4e3fd
11 changed files with 728 additions and 648 deletions

View File

@@ -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

View 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())

View 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)