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
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:
341
tests/apps/registers/test_services.py
Normal file
341
tests/apps/registers/test_services.py
Normal file
@@ -0,0 +1,341 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from datetime import date
|
||||
|
||||
from apps.registers.services import ParsedOrganization, RegisterImportError, RegisterImportService
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from openpyxl import Workbook
|
||||
|
||||
from tests.apps.registers.factories import (
|
||||
OrganizationFactory,
|
||||
RegisterFactory,
|
||||
RegisterUploadFactory,
|
||||
RegistryMembershipPeriodFactory,
|
||||
)
|
||||
|
||||
|
||||
def _build_workbook(rows: list[list[object]]) -> bytes:
|
||||
workbook = Workbook()
|
||||
worksheet = workbook.active
|
||||
for row in rows:
|
||||
worksheet.append(row)
|
||||
buffer = io.BytesIO()
|
||||
workbook.save(buffer)
|
||||
workbook.close()
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def _upload(name: str, rows: list[list[object]]) -> SimpleUploadedFile:
|
||||
return SimpleUploadedFile(
|
||||
name,
|
||||
_build_workbook(rows),
|
||||
content_type=(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RegisterImportServiceTest(TestCase):
|
||||
def test_update_organization_fields_returns_false_when_nothing_changed(self):
|
||||
organization = OrganizationFactory(
|
||||
pn_name="Org",
|
||||
in_kpp=123456789,
|
||||
mn_okpo="12345678",
|
||||
)
|
||||
row = ParsedOrganization(
|
||||
pn_name="Org",
|
||||
mn_ogrn=organization.mn_ogrn,
|
||||
mn_inn=organization.mn_inn,
|
||||
in_kpp=123456789,
|
||||
mn_okpo="12345678",
|
||||
)
|
||||
|
||||
updated = RegisterImportService._update_organization_fields(
|
||||
organization=organization,
|
||||
row=row,
|
||||
)
|
||||
|
||||
self.assertFalse(updated)
|
||||
|
||||
def test_update_organization_fields_updates_changed_values(self):
|
||||
organization = OrganizationFactory(
|
||||
pn_name="Old",
|
||||
in_kpp=123456789,
|
||||
mn_okpo="12345678",
|
||||
)
|
||||
row = ParsedOrganization(
|
||||
pn_name="New",
|
||||
mn_ogrn=organization.mn_ogrn,
|
||||
mn_inn=organization.mn_inn,
|
||||
in_kpp=987654321,
|
||||
mn_okpo="87654321",
|
||||
)
|
||||
|
||||
updated = RegisterImportService._update_organization_fields(
|
||||
organization=organization,
|
||||
row=row,
|
||||
)
|
||||
|
||||
self.assertTrue(updated)
|
||||
organization.refresh_from_db()
|
||||
self.assertEqual(organization.pn_name, "New")
|
||||
self.assertEqual(organization.in_kpp, 987654321)
|
||||
self.assertEqual(organization.mn_okpo, "87654321")
|
||||
|
||||
def test_get_active_periods_by_org_returns_mapping(self):
|
||||
registry = RegisterFactory()
|
||||
active_period = RegistryMembershipPeriodFactory(registry=registry, ended_at=None)
|
||||
RegistryMembershipPeriodFactory(
|
||||
registry=registry,
|
||||
started_at=date(2026, 1, 1),
|
||||
ended_at=date(2026, 2, 1),
|
||||
)
|
||||
|
||||
active_by_org = RegisterImportService._get_active_periods_by_org(registry)
|
||||
|
||||
self.assertEqual(list(active_by_org.keys()), [active_period.organization_id])
|
||||
|
||||
def test_close_missing_periods_updates_and_deletes(self):
|
||||
registry = RegisterFactory()
|
||||
upload = RegisterUploadFactory(registry=registry, actual_date=date(2026, 3, 1))
|
||||
active_period = RegistryMembershipPeriodFactory(
|
||||
registry=registry,
|
||||
started_at=date(2026, 2, 1),
|
||||
ended_at=None,
|
||||
)
|
||||
same_day_period = RegistryMembershipPeriodFactory(
|
||||
registry=registry,
|
||||
started_at=date(2026, 3, 1),
|
||||
ended_at=None,
|
||||
)
|
||||
|
||||
closed = RegisterImportService._close_missing_periods(
|
||||
active_by_org={
|
||||
active_period.organization_id: active_period,
|
||||
same_day_period.organization_id: same_day_period,
|
||||
},
|
||||
snapshot_org_ids=set(),
|
||||
snapshot_date=date(2026, 3, 1),
|
||||
upload=upload,
|
||||
)
|
||||
|
||||
self.assertEqual(closed, 2)
|
||||
active_period.refresh_from_db()
|
||||
self.assertEqual(active_period.ended_at, date(2026, 3, 1))
|
||||
self.assertFalse(
|
||||
active_period.__class__.objects.filter(id=same_day_period.id).exists()
|
||||
)
|
||||
|
||||
def test_open_new_periods_returns_zero_when_all_active(self):
|
||||
registry = RegisterFactory()
|
||||
upload = RegisterUploadFactory(registry=registry)
|
||||
organization = OrganizationFactory()
|
||||
|
||||
opened = RegisterImportService._open_new_periods(
|
||||
registry=registry,
|
||||
snapshot_org_ids={organization.id},
|
||||
active_org_ids={organization.id},
|
||||
snapshot_date=date(2026, 3, 1),
|
||||
upload=upload,
|
||||
)
|
||||
|
||||
self.assertEqual(opened, 0)
|
||||
|
||||
def test_open_new_periods_creates_missing_memberships(self):
|
||||
registry = RegisterFactory()
|
||||
upload = RegisterUploadFactory(registry=registry)
|
||||
organization = OrganizationFactory()
|
||||
|
||||
opened = RegisterImportService._open_new_periods(
|
||||
registry=registry,
|
||||
snapshot_org_ids={organization.id},
|
||||
active_org_ids=set(),
|
||||
snapshot_date=date(2026, 3, 1),
|
||||
upload=upload,
|
||||
)
|
||||
|
||||
self.assertEqual(opened, 1)
|
||||
|
||||
def test_resolve_actual_date_returns_requested_latest_or_today(self):
|
||||
registry = RegisterFactory()
|
||||
requested = RegisterImportService.resolve_actual_date(
|
||||
registry=registry,
|
||||
requested_date=date(2026, 3, 10),
|
||||
)
|
||||
self.assertEqual(requested, date(2026, 3, 10))
|
||||
|
||||
RegisterUploadFactory(registry=registry, actual_date=date(2026, 2, 1))
|
||||
latest_upload = RegisterUploadFactory(
|
||||
registry=registry,
|
||||
actual_date=date(2026, 3, 5),
|
||||
)
|
||||
latest = RegisterImportService.resolve_actual_date(
|
||||
registry=registry,
|
||||
requested_date=None,
|
||||
)
|
||||
self.assertEqual(latest, latest_upload.actual_date)
|
||||
|
||||
empty_registry = RegisterFactory()
|
||||
today = RegisterImportService.resolve_actual_date(
|
||||
registry=empty_registry,
|
||||
requested_date=None,
|
||||
)
|
||||
self.assertEqual(today, date.today())
|
||||
|
||||
def test_get_organizations_queryset_applies_registry_filters_and_search(self):
|
||||
registry = RegisterFactory()
|
||||
upload = RegisterUploadFactory(registry=registry, actual_date=date(2026, 3, 1))
|
||||
match = OrganizationFactory(pn_name="Target organization", mn_okpo="11111111")
|
||||
other = OrganizationFactory(pn_name="Other company", mn_okpo="22222222")
|
||||
RegistryMembershipPeriodFactory(
|
||||
registry=registry,
|
||||
organization=match,
|
||||
started_at=date(2026, 3, 1),
|
||||
started_by_upload=upload,
|
||||
)
|
||||
RegistryMembershipPeriodFactory(
|
||||
registry=registry,
|
||||
organization=other,
|
||||
started_at=date(2026, 1, 1),
|
||||
ended_at=date(2026, 2, 1),
|
||||
started_by_upload=upload,
|
||||
ended_by_upload=upload,
|
||||
)
|
||||
|
||||
queryset, resolved_date = RegisterImportService.get_organizations_queryset(
|
||||
registry=registry,
|
||||
search="Target",
|
||||
mn_okpo="11111111",
|
||||
)
|
||||
|
||||
self.assertEqual(resolved_date, date(2026, 3, 1))
|
||||
self.assertEqual(list(queryset.values_list("id", flat=True)), [match.id])
|
||||
|
||||
def test_get_registry_organizations_queryset_applies_exact_filters(self):
|
||||
registry = RegisterFactory()
|
||||
upload = RegisterUploadFactory(registry=registry, actual_date=date(2026, 3, 1))
|
||||
organization = OrganizationFactory()
|
||||
RegistryMembershipPeriodFactory(
|
||||
registry=registry,
|
||||
organization=organization,
|
||||
started_at=date(2026, 3, 1),
|
||||
started_by_upload=upload,
|
||||
)
|
||||
|
||||
queryset, resolved_date = RegisterImportService.get_registry_organizations_queryset(
|
||||
registry=registry,
|
||||
mn_ogrn=organization.mn_ogrn,
|
||||
mn_inn=organization.mn_inn,
|
||||
in_kpp=organization.in_kpp,
|
||||
mn_okpo=organization.mn_okpo,
|
||||
)
|
||||
|
||||
self.assertEqual(resolved_date, date(2026, 3, 1))
|
||||
self.assertEqual(list(queryset.values_list("id", flat=True)), [organization.id])
|
||||
|
||||
def test_parse_xlsx_parses_rows_and_supports_optional_kpp(self):
|
||||
upload = _upload(
|
||||
"register.xlsx",
|
||||
[
|
||||
["pn_name", "mn_ogrn", "mn_inn", "mn_okpo"],
|
||||
["Org", "1027700118984", "7702000000", "12345678"],
|
||||
],
|
||||
)
|
||||
|
||||
rows = RegisterImportService.parse_xlsx(upload)
|
||||
|
||||
self.assertEqual(
|
||||
rows,
|
||||
[
|
||||
ParsedOrganization(
|
||||
pn_name="Org",
|
||||
mn_ogrn=1027700118984,
|
||||
mn_inn=7702000000,
|
||||
in_kpp=None,
|
||||
mn_okpo="12345678",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_parse_xlsx_raises_on_invalid_workbook_headers_and_empty_rows(self):
|
||||
broken = SimpleUploadedFile("broken.xlsx", b"not-an-excel")
|
||||
with self.assertRaisesMessage(RegisterImportError, "Не удалось прочитать Excel файл"):
|
||||
RegisterImportService.parse_xlsx(broken)
|
||||
|
||||
no_headers = _upload("no-headers.xlsx", [])
|
||||
with self.assertRaisesMessage(RegisterImportError, "Файл не содержит заголовков"):
|
||||
RegisterImportService.parse_xlsx(no_headers)
|
||||
|
||||
missing_headers = _upload("missing.xlsx", [["pn_name", "mn_ogrn"]])
|
||||
with self.assertRaisesMessage(RegisterImportError, "Отсутствуют обязательные колонки"):
|
||||
RegisterImportService.parse_xlsx(missing_headers)
|
||||
|
||||
empty_rows = _upload(
|
||||
"empty.xlsx",
|
||||
[["pn_name", "mn_ogrn", "mn_inn", "mn_okpo"], ["", "", "", ""]],
|
||||
)
|
||||
with self.assertRaisesMessage(
|
||||
RegisterImportError,
|
||||
"Файл не содержит строк с организациями",
|
||||
):
|
||||
RegisterImportService.parse_xlsx(empty_rows)
|
||||
|
||||
def test_validate_snapshot_date_and_unique_identities(self):
|
||||
registry = RegisterFactory()
|
||||
RegisterUploadFactory(registry=registry, actual_date=date(2026, 3, 5))
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
RegisterImportError,
|
||||
"Дата актуальности не может быть раньше последней загрузки",
|
||||
):
|
||||
RegisterImportService._validate_snapshot_date(
|
||||
registry=registry,
|
||||
snapshot_date=date(2026, 3, 1),
|
||||
)
|
||||
|
||||
row = ParsedOrganization(
|
||||
pn_name="Org",
|
||||
mn_ogrn=1,
|
||||
mn_inn=2,
|
||||
in_kpp=None,
|
||||
mn_okpo="12345678",
|
||||
)
|
||||
with self.assertRaisesMessage(
|
||||
RegisterImportError,
|
||||
"Файл содержит дубли по ключу",
|
||||
):
|
||||
RegisterImportService._ensure_unique_identities([row, row])
|
||||
|
||||
def test_scalar_parsers_validate_required_and_numeric_values(self):
|
||||
self.assertEqual(RegisterImportService._normalize_header(" Mn_Inn "), "mn_inn")
|
||||
self.assertTrue(RegisterImportService._is_empty_row((None, " ")))
|
||||
self.assertFalse(RegisterImportService._is_empty_row((None, "x")))
|
||||
self.assertEqual(
|
||||
RegisterImportService._as_required_text(" Org ", field_name="pn_name", row_number=2),
|
||||
"Org",
|
||||
)
|
||||
self.assertEqual(
|
||||
RegisterImportService._as_required_int("123.0", field_name="mn_inn", row_number=2),
|
||||
123,
|
||||
)
|
||||
self.assertIsNone(
|
||||
RegisterImportService._as_optional_int(" ", field_name="in_kpp", row_number=2)
|
||||
)
|
||||
self.assertEqual(
|
||||
RegisterImportService._as_numeric_text("123 456.0", field_name="mn_okpo", row_number=2),
|
||||
"123456",
|
||||
)
|
||||
|
||||
with self.assertRaisesMessage(RegisterImportError, "поле pn_name обязательно"):
|
||||
RegisterImportService._as_required_text("", field_name="pn_name", row_number=2)
|
||||
with self.assertRaisesMessage(RegisterImportError, "поле mn_inn обязательно"):
|
||||
RegisterImportService._as_required_int(None, field_name="mn_inn", row_number=2)
|
||||
with self.assertRaisesMessage(RegisterImportError, "поле in_kpp должно быть числом"):
|
||||
RegisterImportService._as_optional_int("abc", field_name="in_kpp", row_number=2)
|
||||
with self.assertRaisesMessage(
|
||||
RegisterImportError,
|
||||
"поле mn_okpo должно содержать только цифры",
|
||||
):
|
||||
RegisterImportService._as_numeric_text("ab12", field_name="mn_okpo", row_number=2)
|
||||
Reference in New Issue
Block a user