""" Тесты интеграции модуля закупок. Сигналы, уведомления, WebSocket события. """ import pytest from decimal import Decimal from unittest.mock import MagicMock, patch from django.contrib.auth import get_user_model from notifications.models import ( Notification, NotificationCategory, NotificationType, ) from procurement.constants import ApprovalStatus, ProcurementStatus from procurement.models import ( Approval, Budget, ProcurementItem, ProcurementRequest, ) from procurement.signals import broadcast_request_update Employee = get_user_model() @pytest.fixture def notification_setup(db): """Настройка типов уведомлений для тестов.""" category, _ = NotificationCategory.objects.get_or_create( code='procurement', defaults={ 'name': 'Закупки', 'description': 'Уведомления закупок', 'icon': 'bi-cart3', 'color': '#17a2b8', 'order': 60, } ) notification_types = [ 'procurement_new_request', 'procurement_pending_approval', 'procurement_approved', 'procurement_rejected', 'procurement_completed', 'procurement_stage_approved', ] for type_code in notification_types: NotificationType.objects.get_or_create( code=type_code, defaults={ 'category': category, 'name': type_code.replace('_', ' ').title(), 'description': f'Уведомление {type_code}', 'priority': 'normal', 'default_channels': { 'web': True, 'email': False, 'telegram': False }, } ) return category @pytest.fixture def department_with_head(department_factory, user_factory, link_factory): """Отдел с руководителем.""" dept = department_factory(name="IT отдел") head = user_factory( email="head@test.com", first_name="Руководитель", last_name="Отдела", ) head.is_staff = True head.save() dept.head = head dept.save() link_factory(head, dept, is_active=True) return dept @pytest.fixture def requestor(user_factory, department_with_head, link_factory): """Пользователь-заявитель.""" user = user_factory( email="requestor@test.com", first_name="Заявитель", last_name="Тестовый", ) link_factory(user, department_with_head, is_active=True) return user @pytest.fixture def budget_for_department(department_with_head): """Бюджет для отдела.""" from django.utils import timezone now = timezone.now() return Budget.objects.create( department=department_with_head, year=now.year, quarter=(now.month - 1) // 3 + 1, allocated_amount=Decimal('100000.00'), ) @pytest.mark.django_db class TestProcurementSignals: """Тесты сигналов модуля закупок.""" def test_new_request_notifies_department_head( self, notification_setup, department_with_head, requestor ): """Создание заявки уведомляет руководителя отдела.""" # Очищаем уведомления Notification.objects.all().delete() request = ProcurementRequest.objects.create( title="Тестовая заявка", description="Описание", department=department_with_head, requestor=requestor, ) # Проверяем уведомление руководителю notifications = Notification.objects.filter( recipient=department_with_head.head ) assert notifications.exists() assert 'Новая заявка' in notifications.first().title def test_status_change_triggers_notification( self, notification_setup, department_with_head, requestor, budget_for_department ): """Изменение статуса создаёт уведомление.""" Notification.objects.all().delete() request = ProcurementRequest.objects.create( title="Заявка на согласование", department=department_with_head, requestor=requestor, ) # Добавляем позицию ProcurementItem.objects.create( request=request, name="Тестовый товар", quantity=1, estimated_unit_price=Decimal('5000.00'), ) # Создаём согласование Approval.objects.create( request=request, approver=department_with_head.head, priority=10, ) # Отправляем на согласование request.status = ProcurementStatus.PENDING request.save() # Проверяем уведомление согласующему pending_notifications = Notification.objects.filter( notification_type__code='procurement_pending_approval' ) assert pending_notifications.exists() @pytest.mark.django_db class TestBroadcastFunction: """Тесты функции broadcast.""" @patch('procurement.signals.get_channel_layer') def test_broadcast_sends_to_department_group( self, mock_get_channel_layer, notification_setup, department_with_head, requestor ): """broadcast_request_update отправляет в группу отдела.""" mock_channel_layer = MagicMock() mock_get_channel_layer.return_value = mock_channel_layer request = ProcurementRequest( id=1, title="Тест", status=ProcurementStatus.DRAFT, department=department_with_head, requestor=requestor, ) broadcast_request_update(request, 'test_event') # Проверяем вызов group_send assert mock_channel_layer.group_send.call_count >= 1 @patch('procurement.signals.get_channel_layer') def test_broadcast_handles_missing_channel_layer( self, mock_get_channel_layer, notification_setup, department_with_head, requestor ): """broadcast_request_update обрабатывает отсутствие channel_layer.""" mock_get_channel_layer.return_value = None request = ProcurementRequest( id=1, title="Тест", status=ProcurementStatus.DRAFT, department=department_with_head, requestor=requestor, ) # Не должно вызвать исключение broadcast_request_update(request, 'test_event') @pytest.mark.django_db class TestApprovalNotifications: """Тесты уведомлений при согласовании.""" def test_approval_approved_notifies_requestor( self, notification_setup, department_with_head, requestor, budget_for_department ): """Одобрение согласования уведомляет заявителя.""" Notification.objects.all().delete() request = ProcurementRequest.objects.create( title="Заявка", department=department_with_head, requestor=requestor, status=ProcurementStatus.PENDING, ) approval = Approval.objects.create( request=request, approver=department_with_head.head, priority=10, status=ApprovalStatus.PENDING, ) # Одобряем approval.status = ApprovalStatus.APPROVED approval.save() # Проверяем уведомление заявителю stage_notifications = Notification.objects.filter( recipient=requestor, notification_type__code='procurement_stage_approved' ) assert stage_notifications.exists() def test_approval_rejected_notifies_requestor( self, notification_setup, department_with_head, requestor, budget_for_department ): """Отклонение согласования уведомляет заявителя.""" Notification.objects.all().delete() request = ProcurementRequest.objects.create( title="Заявка", department=department_with_head, requestor=requestor, status=ProcurementStatus.PENDING, ) approval = Approval.objects.create( request=request, approver=department_with_head.head, priority=10, status=ApprovalStatus.PENDING, ) # Отклоняем approval.status = ApprovalStatus.REJECTED approval.comment = "Недостаточно обоснования" approval.save() # Проверяем уведомление заявителю reject_notifications = Notification.objects.filter( recipient=requestor, notification_type__code='procurement_rejected' ) assert reject_notifications.exists()