388 lines
14 KiB
Python
388 lines
14 KiB
Python
"""
|
||
Тесты для API модуля закупок.
|
||
"""
|
||
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
from django.urls import reverse
|
||
from django.utils import timezone
|
||
from rest_framework import status
|
||
from rest_framework.test import APIClient
|
||
|
||
from employees.models import Department, Employee
|
||
from procurement.constants import ProcurementStatus, UrgencyLevel
|
||
from procurement.models import (
|
||
Budget,
|
||
Equipment,
|
||
EquipmentCategory,
|
||
ProcurementItem,
|
||
ProcurementRequest,
|
||
ProcurementRequestView,
|
||
Supplier,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def api_client():
|
||
"""API клиент."""
|
||
return APIClient()
|
||
|
||
|
||
@pytest.fixture
|
||
def user(db):
|
||
"""Обычный пользователь."""
|
||
return Employee.objects.create_user(
|
||
email="user@example.com",
|
||
password="testpass123",
|
||
phone_number="+79991111111",
|
||
first_name="Иван",
|
||
last_name="Иванов",
|
||
send_activation_email=False,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def staff_user(db):
|
||
"""Staff пользователь."""
|
||
return Employee.objects.create_user(
|
||
email="staff@example.com",
|
||
password="testpass123",
|
||
phone_number="+79992222222",
|
||
first_name="Админ",
|
||
last_name="Админов",
|
||
is_staff=True,
|
||
send_activation_email=False,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def department(db):
|
||
"""Тестовый отдел."""
|
||
return Department.objects.create(name="Тестовый отдел", description="Для тестов")
|
||
|
||
|
||
@pytest.fixture
|
||
def procurement_request(db, department, user):
|
||
"""Тестовая заявка."""
|
||
return ProcurementRequest.objects.create(
|
||
title="Тестовая заявка",
|
||
description="Описание",
|
||
department=department,
|
||
requestor=user,
|
||
status=ProcurementStatus.DRAFT,
|
||
urgency=UrgencyLevel.MEDIUM,
|
||
)
|
||
|
||
|
||
@pytest.mark.django_db
|
||
class TestProcurementRequestAPI:
|
||
"""Тесты API заявок на закупку."""
|
||
|
||
def test_list_requests_unauthorized(self, api_client):
|
||
"""Тест: неавторизованный доступ запрещен."""
|
||
url = reverse("api:v1:procurement:procurementrequest-list")
|
||
response = api_client.get(url)
|
||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||
|
||
def test_list_requests_authorized(self, api_client, user, procurement_request):
|
||
"""Тест: авторизованный пользователь видит свои заявки."""
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse("api:v1:procurement:procurementrequest-list")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert len(response.data["results"]) == 1
|
||
assert response.data["results"][0]["title"] == "Тестовая заявка"
|
||
|
||
def test_list_completed_request_includes_completed_at(
|
||
self, api_client, user, procurement_request
|
||
):
|
||
"""Тест: список отдаёт дату завершения для завершённых заявок."""
|
||
procurement_request.status = ProcurementStatus.COMPLETED
|
||
procurement_request.completed_at = timezone.now()
|
||
procurement_request.save(update_fields=["status", "completed_at"])
|
||
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse("api:v1:procurement:procurementrequest-list")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert response.data["results"][0]["status"] == ProcurementStatus.COMPLETED
|
||
assert response.data["results"][0]["completed_at"] is not None
|
||
|
||
def test_create_request(self, api_client, user, department, link_factory):
|
||
"""Тест: создание заявки."""
|
||
# Связываем пользователя с отделом (необходимо для создания заявки)
|
||
link_factory(user, department, is_active=True)
|
||
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse("api:v1:procurement:procurementrequest-list")
|
||
|
||
data = {
|
||
"title": "Новая заявка",
|
||
"description": "Нужны компьютеры",
|
||
"department": department.id,
|
||
"urgency": UrgencyLevel.HIGH,
|
||
"items": [
|
||
{
|
||
"name": "Ноутбук",
|
||
"quantity": 2,
|
||
"unit": "шт",
|
||
"estimated_unit_price": "50000.00",
|
||
}
|
||
],
|
||
}
|
||
|
||
response = api_client.post(url, data, format="json")
|
||
assert response.status_code == status.HTTP_201_CREATED
|
||
assert response.data["title"] == "Новая заявка"
|
||
assert response.data["requestor"] == user.id
|
||
|
||
def test_retrieve_request(self, api_client, user, procurement_request):
|
||
"""Тест: получение детальной информации о заявке."""
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse(
|
||
"api:v1:procurement:procurementrequest-detail",
|
||
kwargs={"pk": procurement_request.id},
|
||
)
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert response.data["id"] == procurement_request.id
|
||
assert response.data["title"] == "Тестовая заявка"
|
||
assert "total_cost" in response.data
|
||
assert "required_approval_priorities" in response.data
|
||
assert "is_editable" in response.data
|
||
|
||
def test_list_request_quantity_unit_label_single_unit(
|
||
self, api_client, user, procurement_request
|
||
):
|
||
"""Тест: для одной единицы измерения возвращается ее подпись."""
|
||
ProcurementItem.objects.create(
|
||
request=procurement_request,
|
||
name="Кабель",
|
||
quantity=20,
|
||
unit="м",
|
||
)
|
||
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse("api:v1:procurement:procurementrequest-list")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert response.data["results"][0]["quantity_unit_label"] == "метры"
|
||
|
||
def test_list_request_quantity_unit_label_mixed_units(
|
||
self, api_client, user, procurement_request
|
||
):
|
||
"""Тест: для смешанных единиц возвращается обобщенная подпись."""
|
||
ProcurementItem.objects.create(
|
||
request=procurement_request,
|
||
name="Кабель",
|
||
quantity=20,
|
||
unit="м",
|
||
)
|
||
ProcurementItem.objects.create(
|
||
request=procurement_request,
|
||
name="Крепеж",
|
||
quantity=10,
|
||
unit="шт",
|
||
)
|
||
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse("api:v1:procurement:procurementrequest-list")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert response.data["results"][0]["quantity_unit_label"] == "единиц"
|
||
|
||
def test_set_request_viewed_flag(self, api_client, user, procurement_request):
|
||
"""Тест: пользователь может отметить видимую заявку просмотренной."""
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse(
|
||
"api:v1:procurement:procurementrequest-set-viewed",
|
||
kwargs={"pk": procurement_request.id},
|
||
)
|
||
|
||
response = api_client.post(url, {"is_viewed": True}, format="json")
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert response.data["is_viewed"] is True
|
||
view_state = ProcurementRequestView.objects.get(
|
||
request=procurement_request,
|
||
user=user,
|
||
)
|
||
assert view_state.is_viewed is True
|
||
assert view_state.viewed_at is not None
|
||
|
||
response = api_client.post(url, {"is_viewed": False}, format="json")
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert response.data["is_viewed"] is False
|
||
view_state.refresh_from_db()
|
||
assert view_state.is_viewed is False
|
||
assert view_state.viewed_at is None
|
||
|
||
def test_update_own_request_draft(self, api_client, user, procurement_request):
|
||
"""Тест: обновление своей заявки в статусе DRAFT."""
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse(
|
||
"api:v1:procurement:procurementrequest-detail",
|
||
kwargs={"pk": procurement_request.id},
|
||
)
|
||
|
||
data = {"title": "Обновленная заявка"}
|
||
response = api_client.patch(url, data, format="json")
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert response.data["title"] == "Обновленная заявка"
|
||
|
||
|
||
@pytest.mark.django_db
|
||
class TestEquipmentAPI:
|
||
"""Тесты API оборудования."""
|
||
|
||
def test_list_equipment(self, api_client, user, department):
|
||
"""Тест: список оборудования."""
|
||
api_client.force_authenticate(user=user)
|
||
|
||
# Создаем категорию и оборудование
|
||
category = EquipmentCategory.objects.create(name="Компьютеры")
|
||
Equipment.objects.create(
|
||
name="Ноутбук Dell",
|
||
inventory_number="INV-2025-001",
|
||
category=category,
|
||
department=department,
|
||
purchase_date="2025-01-01",
|
||
purchase_cost=Decimal("80000.00"),
|
||
)
|
||
|
||
url = reverse("api:v1:procurement:equipment-list")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert len(response.data["results"]) == 1
|
||
|
||
def test_create_equipment_staff_only(
|
||
self, api_client, user, staff_user, department
|
||
):
|
||
"""Тест: создание оборудования только для staff."""
|
||
category = EquipmentCategory.objects.create(name="Техника")
|
||
|
||
# Обычный пользователь не может создать
|
||
api_client.force_authenticate(user=user)
|
||
url = reverse("api:v1:procurement:equipment-list")
|
||
|
||
data = {
|
||
"name": "Принтер HP",
|
||
"inventory_number": "INV-2025-002",
|
||
"category": category.id,
|
||
"department": department.id,
|
||
"purchase_date": "2025-01-01",
|
||
"purchase_cost": "20000.00",
|
||
}
|
||
|
||
response = api_client.post(url, data, format="json")
|
||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||
|
||
# Staff может создать
|
||
api_client.force_authenticate(user=staff_user)
|
||
response = api_client.post(url, data, format="json")
|
||
assert response.status_code == status.HTTP_201_CREATED
|
||
|
||
|
||
@pytest.mark.django_db
|
||
class TestBudgetAPI:
|
||
"""Тесты API бюджетов."""
|
||
|
||
def test_list_budgets_staff(self, api_client, staff_user, department):
|
||
"""Тест: список бюджетов для staff."""
|
||
api_client.force_authenticate(user=staff_user)
|
||
|
||
Budget.objects.create(
|
||
department=department,
|
||
year=2025,
|
||
quarter=4,
|
||
allocated_amount=Decimal("500000.00"),
|
||
)
|
||
|
||
url = reverse("api:v1:procurement:budget-list")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert len(response.data["results"]) == 1
|
||
|
||
def test_list_budgets_regular_user(self, api_client, user):
|
||
"""Тест: обычный пользователь не видит бюджеты (нет прав)."""
|
||
api_client.force_authenticate(user=user)
|
||
|
||
url = reverse("api:v1:procurement:budget-list")
|
||
response = api_client.get(url)
|
||
|
||
# Обычный пользователь не имеет доступа к бюджетам
|
||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||
|
||
|
||
@pytest.mark.django_db
|
||
class TestSupplierAPI:
|
||
"""Тесты API поставщиков."""
|
||
|
||
def test_list_suppliers(self, api_client, user):
|
||
"""Тест: список поставщиков."""
|
||
api_client.force_authenticate(user=user)
|
||
|
||
Supplier.objects.create(
|
||
name="ООО Тестовый поставщик",
|
||
rating=Decimal("4.50"),
|
||
)
|
||
|
||
url = reverse("api:v1:procurement:supplier-list")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
assert len(response.data["results"]) == 1
|
||
|
||
def test_top_rated_suppliers(self, api_client, user):
|
||
"""Тест: лучшие поставщики."""
|
||
api_client.force_authenticate(user=user)
|
||
|
||
Supplier.objects.create(
|
||
name="Отличный поставщик",
|
||
rating=Decimal("4.80"),
|
||
is_active=True,
|
||
)
|
||
Supplier.objects.create(
|
||
name="Плохой поставщик",
|
||
rating=Decimal("2.50"),
|
||
is_active=True,
|
||
)
|
||
|
||
url = reverse("api:v1:procurement:supplier-top-rated")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
# Должен вернуть только с рейтингом >= 4.0
|
||
assert len(response.data) == 1
|
||
assert response.data[0]["name"] == "Отличный поставщик"
|
||
|
||
|
||
@pytest.mark.django_db
|
||
class TestEquipmentCategoryAPI:
|
||
"""Тесты API категорий оборудования."""
|
||
|
||
def test_category_tree(self, api_client, user):
|
||
"""Тест: дерево категорий."""
|
||
api_client.force_authenticate(user=user)
|
||
|
||
parent = EquipmentCategory.objects.create(name="Компьютеры")
|
||
EquipmentCategory.objects.create(name="Ноутбуки", parent=parent)
|
||
|
||
url = reverse("api:v1:procurement:equipmentcategory-tree")
|
||
response = api_client.get(url)
|
||
|
||
assert response.status_code == status.HTTP_200_OK
|
||
# Должен вернуть только корневые категории
|
||
assert len(response.data) == 1
|
||
assert response.data[0]["name"] == "Компьютеры"
|