From 744b71fbe73e7661cfb7dfb66f54f2420ca73555 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 12 Nov 2026 11:11:50 +0400 Subject: [PATCH] =?UTF-8?q?fix(vsp):=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B0=D0=B5=D1=82=20=D0=92=D0=A1?= =?UTF-8?q?=D0=9F=20=D0=BF=D0=BE=20id=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D1=8F=20=D1=83=D0=B4=D0=B0=D0=BB=D1=91=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5;=20=D0=BF=D1=80=D0=BE=D1=88=D0=B5=D0=B4=D1=88=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=B0=D1=82=D0=B0=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D0=B8=D1=8F=20=D1=81=D1=80=D0=B0=D0=B7=D1=83=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=92?= =?UTF-8?q?=D0=A1=D0=9F=20(AURORA-1885,=20#327)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VSPRepository.get: with_deleted=True означает «включая удалённые», а не «только удалённые» - VSPService.update: ключ closed_at (имя поля модели) вместо close_date (входной alias) - тесты: integration test_vsp.py, unit test_vsp_service_close_date.py Co-Authored-By: Claude Opus 5.5 --- api/src/repository/vsp_repository.py | 4 +- api/src/services/vsp_service.py | 2 +- api/tests/integration/test_vsp.py | 60 +++++++++++++++++++ api/tests/unit/test_vsp_service_close_date.py | 45 ++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 api/tests/unit/test_vsp_service_close_date.py diff --git a/api/src/repository/vsp_repository.py b/api/src/repository/vsp_repository.py index 87c0de5..1cd5caa 100644 --- a/api/src/repository/vsp_repository.py +++ b/api/src/repository/vsp_repository.py @@ -32,7 +32,9 @@ class VSPRepository: load_org_unit: bool = False, with_deleted: bool = False, ) -> Optional[Vsp]: - queries = [Vsp.id == vsp_id, Vsp.is_deleted.is_(with_deleted)] + queries = [Vsp.id == vsp_id] + if not with_deleted: + queries.append(Vsp.is_deleted.is_(False)) if is_active is not None: queries.append(Vsp.is_active == is_active) if load_org_unit: diff --git a/api/src/services/vsp_service.py b/api/src/services/vsp_service.py index df6d30c..63e7847 100644 --- a/api/src/services/vsp_service.py +++ b/api/src/services/vsp_service.py @@ -264,7 +264,7 @@ class VSPService: if not set(data.keys()).issubset(self.EXECUTOR_EDITABLE_FIELDS): raise AccessDeniedException() - effective_close_date = data.get("close_date", vsp.closed_at) + effective_close_date = data.get("closed_at", vsp.closed_at) if effective_close_date is not None and effective_close_date <= date.today(): data["is_active"] = False diff --git a/api/tests/integration/test_vsp.py b/api/tests/integration/test_vsp.py index cdbd118..4ea505c 100644 --- a/api/tests/integration/test_vsp.py +++ b/api/tests/integration/test_vsp.py @@ -293,3 +293,63 @@ def test_vsp_delete_forbidden_for_executor(client, isp_tokens, auth_headers): def test_vsp_requires_auth(client): response = client.get("/api/v1/dict/info") assert response.status_code == 403 + + +def _create_vsp(client, admin_tokens, auth_headers, **extra) -> int: + created = client.post( + "/api/v1/dict/info", + json={ + "registration_number": _unique_reg_number(), + "address": "AURORA-1885", + "ssp_id": 1, + **extra, + }, + headers=auth_headers(admin_tokens), + ) + assert created.status_code == 201 + return created.json()["result"]["id"] + + +def test_vsp_get_by_id_admin_active(client, admin_tokens, auth_headers): + vsp_id = _create_vsp(client, admin_tokens, auth_headers) + response = client.get(f"/api/v1/dict/info/{vsp_id}", headers=auth_headers(admin_tokens)) + assert response.status_code == 200 + assert response.json()["result"]["id"] == vsp_id + assert response.json()["result"]["is_deleted"] is False + + +def test_vsp_get_by_id_admin_sees_deleted(client, admin_tokens, auth_headers): + vsp_id = _create_vsp(client, admin_tokens, auth_headers) + deleted = client.delete(f"/api/v1/dict/info/{vsp_id}", headers=auth_headers(admin_tokens)) + assert deleted.status_code == 200 + + response = client.get(f"/api/v1/dict/info/{vsp_id}", headers=auth_headers(admin_tokens)) + assert response.status_code == 200 + assert response.json()["result"]["is_deleted"] is True + + +def test_vsp_get_by_id_executor_deleted_not_found(client, admin_tokens, isp_tokens, auth_headers): + vsp_id = _create_vsp(client, admin_tokens, auth_headers) + client.delete(f"/api/v1/dict/info/{vsp_id}", headers=auth_headers(admin_tokens)) + + response = client.get(f"/api/v1/dict/info/{vsp_id}", headers=auth_headers(isp_tokens)) + assert response.status_code == 404 + + +def test_vsp_update_past_close_date_deactivates(client, admin_tokens, auth_headers): + from datetime import date, timedelta + + vsp_id = _create_vsp(client, admin_tokens, auth_headers, open_date="2020-01-01") + yesterday = (date.today() - timedelta(days=1)).isoformat() + + response = client.put( + f"/api/v1/dict/info/{vsp_id}", + json={"close_date": yesterday}, + headers=auth_headers(admin_tokens), + ) + assert response.status_code == 200 + assert response.json()["result"]["is_active"] is False + + listing = client.get("/api/v1/dict/info", headers=auth_headers(admin_tokens)) + assert listing.status_code == 200 + assert vsp_id not in [item["id"] for item in listing.json()["result"]] diff --git a/api/tests/unit/test_vsp_service_close_date.py b/api/tests/unit/test_vsp_service_close_date.py new file mode 100644 index 0000000..e29814b --- /dev/null +++ b/api/tests/unit/test_vsp_service_close_date.py @@ -0,0 +1,45 @@ +from datetime import date, timedelta +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from src.db.models import UserRoleEnum +from src.domain.schemas import VSPUpdate +from src.services.vsp_service import VSPService + + +def _service(vsp) -> VSPService: + service = VSPService.__new__(VSPService) + service.vsp_repo = MagicMock() + service.vsp_repo.get = AsyncMock(return_value=vsp) + service.vsp_repo.update = AsyncMock(return_value=vsp) + return service + + +def _admin(): + user = MagicMock() + user.id = 1 + user.role_id = UserRoleEnum.ADMIN + return user + + +async def test_update_past_close_date_sets_inactive(): + vsp = MagicMock(closed_at=None) + service = _service(vsp) + payload = VSPUpdate(close_date=date.today() - timedelta(days=1)) + + await service.update(vsp_id=1, payload=payload, user=_admin()) + + data = service.vsp_repo.update.await_args.kwargs["data"] + assert data["is_active"] is False + + +async def test_update_future_close_date_keeps_active_key_absent(): + vsp = MagicMock(closed_at=None) + service = _service(vsp) + payload = VSPUpdate(close_date=date.today() + timedelta(days=30)) + + await service.update(vsp_id=1, payload=payload, user=_admin()) + + data = service.vsp_repo.update.await_args.kwargs["data"] + assert "is_active" not in data -- 2.47.2