Corp/backend/requests_app/tests/test_signals.py
lizerk 46daefcdec test: добавлены тесты для сигналов и migration команда
- Добавлена management команда migrate_requests_to_actions для миграции существующих заявок
- Добавлен conftest.py с фикстурой user_factory для тестов requests_app
- Добавлены 13 тестов для проверки автоматизации создания EmployeeAction
- Покрытие: немедленные/отложенные события, дубликаты, условия срабатывания, маппинги
2026-03-18 09:42:13 +03:00

299 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Тесты для автоматического создания EmployeeAction из заявок (signals).
"""
import pytest
from datetime import date, timedelta
from unittest.mock import patch, MagicMock
from django.utils import timezone
from requests_app.models import Request
from requests_app.enums import RequestStatus
from employees.models import EmployeeAction
from employees.signals import IMMEDIATE_ACTION_MAPPING, SCHEDULED_ACTION_MAPPING
@pytest.mark.django_db
class TestRequestSignals:
"""Тесты сигналов создания EmployeeAction из Request."""
def test_immediate_action_dismissal(self, user_factory):
"""Увольнение создаёт EmployeeAction немедленно при одобрении."""
employee = user_factory()
approver = user_factory()
# Создаём заявку на увольнение
request = Request.objects.create(
employee=employee,
type='dismissal',
status=RequestStatus.PENDING,
date_from=date.today(),
comment='Увольнение по собственному желанию'
)
# Проверяем что события ещё нет
assert not EmployeeAction.objects.filter(
extra__request_id=request.id
).exists()
# Одобряем заявку
request.status = RequestStatus.APPROVED
request.approver = approver
request.decided_at = timezone.now()
request.save()
# Проверяем что событие создалось
action = EmployeeAction.objects.get(
employee=employee,
action='dismissed',
extra__request_id=request.id
)
assert action.action == 'dismissed'
assert action.extra['request_id'] == request.id
assert action.extra['approved_by'] == approver.id
assert action.extra['immediate'] is True
assert 'Заявление #' in action.comment
def test_immediate_action_transfer(self, user_factory):
"""Перевод создаёт EmployeeAction немедленно при одобрении."""
employee = user_factory()
approver = user_factory()
request = Request.objects.create(
employee=employee,
type='transfer',
status=RequestStatus.PENDING,
date_from=date.today(),
)
# Одобряем
request.status = RequestStatus.APPROVED
request.approver = approver
request.save()
# Проверяем событие
action = EmployeeAction.objects.get(
employee=employee,
action='transferred',
extra__request_id=request.id
)
assert action.action == 'transferred'
assert action.extra['immediate'] is True
@patch('employees.signals._schedule_delayed_action')
def test_scheduled_action_vacation(self, mock_schedule, user_factory):
"""Отпуск запускает планирование через Celery."""
employee = user_factory()
approver = user_factory()
future_date = date.today() + timedelta(days=7)
request = Request.objects.create(
employee=employee,
type='vacation',
status=RequestStatus.PENDING,
date_from=future_date,
date_to=future_date + timedelta(days=10),
)
# Одобряем
request.status = RequestStatus.APPROVED
request.approver = approver
request.save()
# Проверяем что вызвалась функция планирования
mock_schedule.assert_called_once()
called_request = mock_schedule.call_args[0][0]
assert called_request.id == request.id
@patch('employees.signals._schedule_delayed_action')
def test_scheduled_action_sick_leave(self, mock_schedule, user_factory):
"""Больничный запускает планирование через Celery."""
employee = user_factory()
approver = user_factory()
future_date = date.today() + timedelta(days=3)
request = Request.objects.create(
employee=employee,
type='sick_leave',
status=RequestStatus.PENDING,
date_from=future_date,
)
request.status = RequestStatus.APPROVED
request.approver = approver
request.save()
mock_schedule.assert_called_once()
def test_no_duplicate_actions(self, user_factory):
"""Повторное сохранение одобренной заявки не создаёт дубликаты."""
employee = user_factory()
approver = user_factory()
request = Request.objects.create(
employee=employee,
type='dismissal',
status=RequestStatus.PENDING,
date_from=date.today(),
)
# Одобряем
request.status = RequestStatus.APPROVED
request.approver = approver
request.save()
# Первое событие создалось
assert EmployeeAction.objects.filter(
employee=employee,
action='dismissed',
extra__request_id=request.id
).count() == 1
# Меняем комментарий и сохраняем снова
request.comment = 'Обновлённый комментарий'
request.save()
# Дубликат не создался
assert EmployeeAction.objects.filter(
employee=employee,
action='dismissed',
extra__request_id=request.id
).count() == 1
def test_signal_not_fired_on_creation(self, user_factory):
"""Сигнал не срабатывает при создании (только при одобрении)."""
employee = user_factory()
request = Request.objects.create(
employee=employee,
type='dismissal',
status=RequestStatus.PENDING,
)
# События нет
assert not EmployeeAction.objects.filter(
extra__request_id=request.id
).exists()
def test_signal_not_fired_on_rejection(self, user_factory):
"""Сигнал не срабатывает при отклонении."""
employee = user_factory()
approver = user_factory()
request = Request.objects.create(
employee=employee,
type='dismissal',
status=RequestStatus.PENDING,
)
# Отклоняем
request.status = RequestStatus.REJECTED
request.approver = approver
request.save()
# События нет
assert not EmployeeAction.objects.filter(
extra__request_id=request.id
).exists()
def test_unknown_request_type_no_action(self, user_factory):
"""Неизвестный тип заявки не создаёт события."""
employee = user_factory()
approver = user_factory()
# Используем тип, который не в маппингах
request = Request.objects.create(
employee=employee,
type='other', # Не в IMMEDIATE/SCHEDULED маппингах
status=RequestStatus.PENDING,
)
# Одобряем
request.status = RequestStatus.APPROVED
request.approver = approver
request.save()
# События нет
assert not EmployeeAction.objects.filter(
employee=employee,
extra__request_id=request.id
).exists()
def test_action_date_from_request_date_from(self, user_factory):
"""Дата события берётся из date_from заявки."""
employee = user_factory()
approver = user_factory()
specific_date = date(2026, 6, 15)
request = Request.objects.create(
employee=employee,
type='dismissal',
status=RequestStatus.PENDING,
date_from=specific_date,
)
# Одобряем
request.status = RequestStatus.APPROVED
request.approver = approver
request.save()
action = EmployeeAction.objects.get(
employee=employee,
action='dismissed',
extra__request_id=request.id
)
# Проверяем дату (может быть datetime, поэтому берём .date())
action_date = action.date.date() if hasattr(action.date, 'date') else action.date
assert action_date == specific_date
@patch('employees.signals._apply_action_effects')
def test_effects_applied_for_immediate_actions(self, mock_apply, user_factory):
"""Эффекты применяются для немедленных действий."""
employee = user_factory()
approver = user_factory()
request = Request.objects.create(
employee=employee,
type='dismissal',
status=RequestStatus.PENDING,
)
# Одобряем
request.status = RequestStatus.APPROVED
request.approver = approver
request.save()
# Проверяем что _apply_action_effects была вызвана
mock_apply.assert_called_once()
created_action = mock_apply.call_args[0][0]
assert created_action.action == 'dismissed'
@pytest.mark.django_db
class TestRequestMappings:
"""Тесты корректности маппингов типов заявок."""
def test_immediate_mapping_complete(self):
"""Проверка что все немедленные маппинги правильные."""
assert IMMEDIATE_ACTION_MAPPING == {
'transfer': 'transferred',
'dismissal': 'dismissed',
}
def test_scheduled_mapping_complete(self):
"""Проверка что все отложенные маппинги правильные."""
assert SCHEDULED_ACTION_MAPPING == {
'vacation': 'on_leave',
'sick_leave': 'on_sick_leave',
}
def test_no_overlap_in_mappings(self):
"""Проверка что маппинги не пересекаются."""
immediate_keys = set(IMMEDIATE_ACTION_MAPPING.keys())
scheduled_keys = set(SCHEDULED_ACTION_MAPPING.keys())
assert immediate_keys.isdisjoint(scheduled_keys), \
"Request types должны быть либо immediate, либо scheduled, но не оба"