# backend/api/v1/schedule/tests.py """ Тесты для django-scheduler API endpoints. """ from datetime import timedelta import pytest from django.contrib.contenttypes.models import ContentType from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient from schedule.models import Calendar, CalendarRelation, Event, Rule from employees.models import Employee @pytest.fixture def api_client(): """Клиент API с авторизацией.""" client = APIClient() return client @pytest.fixture def test_user(db): """Создание тестового пользователя.""" user = Employee.objects.create_user( email="test@example.com", phone_number="+79991234567", # Обязательное поле password="testpass123", username="testuser", first_name="Test", last_name="User", send_activation_email=False, # Не отправляем email в тестах ) return user @pytest.fixture def authenticated_client(api_client, test_user): """Авторизованный клиент.""" api_client.force_authenticate(user=test_user) return api_client @pytest.fixture def test_calendar(test_user): """Создание тестового календаря.""" calendar = Calendar.objects.create(name="Тестовый календарь", slug="test-calendar") CalendarRelation.objects.create( calendar=calendar, content_type=ContentType.objects.get_for_model(Employee), object_id=test_user.id, distinction="owner", inheritable=True, ) return calendar @pytest.fixture def test_event(test_calendar, test_user): """Создание тестового события.""" now = timezone.now() return Event.objects.create( calendar=test_calendar, title="Тестовое событие", description="Описание", start=now, end=now + timedelta(hours=1), creator=test_user, color_event="#3498db", ) # ==================== ТЕСТЫ КАЛЕНДАРЕЙ ==================== @pytest.mark.django_db class TestCalendarAPI: """Тесты для Calendar ViewSet.""" def test_list_calendars_unauthorized(self, api_client): """Проверка: неавторизованный доступ запрещён.""" response = api_client.get("/api/v1/schedule/calendars/") assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_list_calendars(self, authenticated_client, test_calendar): """Проверка: список календарей возвращается.""" response = authenticated_client.get("/api/v1/schedule/calendars/") assert response.status_code == status.HTTP_200_OK # Проверяем что данные - это список data = ( response.data if isinstance(response.data, list) else response.data.get("results", []) ) assert len(data) > 0 # Проверяем структуру calendar_data = data[0] assert calendar_data["name"] == test_calendar.name assert calendar_data["slug"] == test_calendar.slug assert "events_count" in calendar_data def test_create_calendar(self, authenticated_client): """Проверка: создание календаря.""" data = {"name": "Новый календарь", "slug": "new-calendar"} response = authenticated_client.post( "/api/v1/schedule/calendars/", data, format="json" ) assert response.status_code == status.HTTP_201_CREATED assert response.data["name"] == data["name"] assert response.data["slug"] == data["slug"] # Проверяем что календарь создался в БД assert Calendar.objects.filter(slug="new-calendar").exists() def test_create_calendar_without_slug(self, authenticated_client): """Проверка: slug автогенерируется если не указан.""" data = {"name": "Календарь без slug"} response = authenticated_client.post( "/api/v1/schedule/calendars/", data, format="json" ) assert response.status_code == status.HTTP_201_CREATED assert "slug" in response.data assert response.data["slug"] # slug не пустой def test_create_calendar_with_title(self, authenticated_client): """Проверка: поддержка поля 'title' (EUSRR совместимость).""" data = { "title": "Календарь через title", } response = authenticated_client.post( "/api/v1/schedule/calendars/", data, format="json" ) assert response.status_code == status.HTTP_201_CREATED # title конвертируется в name assert response.data["name"] == data["title"] or "name" in response.data def test_get_calendar_detail(self, authenticated_client, test_calendar): """Проверка: получение деталей календаря.""" response = authenticated_client.get( f"/api/v1/schedule/calendars/{test_calendar.id}/" ) assert response.status_code == status.HTTP_200_OK assert response.data["id"] == test_calendar.id assert response.data["name"] == test_calendar.name def test_update_calendar(self, authenticated_client, test_calendar): """Проверка: обновление календаря.""" data = {"name": "Обновлённое имя"} response = authenticated_client.patch( f"/api/v1/schedule/calendars/{test_calendar.id}/", data, format="json" ) assert response.status_code == status.HTTP_200_OK assert response.data["name"] == data["name"] test_calendar.refresh_from_db() assert test_calendar.name == data["name"] def test_delete_calendar(self, authenticated_client, test_calendar): """Проверка: удаление календаря.""" calendar_id = test_calendar.id response = authenticated_client.delete( f"/api/v1/schedule/calendars/{calendar_id}/" ) assert response.status_code == status.HTTP_204_NO_CONTENT assert not Calendar.objects.filter(id=calendar_id).exists() def test_filter_calendars_by_name(self, authenticated_client, test_user): """Проверка: фильтрация по имени.""" first = Calendar.objects.create(name="Первый", slug="first") second = Calendar.objects.create(name="Второй", slug="second") for calendar in (first, second): calendar.create_relation( test_user, distinction="owner", inheritable=True, ) response = authenticated_client.get("/api/v1/schedule/calendars/?name=Первый") assert response.status_code == status.HTTP_200_OK data = ( response.data if isinstance(response.data, list) else response.data.get("results", []) ) assert len(data) == 1 assert data[0]["name"] == "Первый" # ==================== ТЕСТЫ СОБЫТИЙ ==================== @pytest.mark.django_db class TestEventAPI: """Тесты для Event ViewSet.""" def test_list_events(self, authenticated_client, test_event): """Проверка: список событий.""" response = authenticated_client.get("/api/v1/schedule/events/") assert response.status_code == status.HTTP_200_OK assert len(response.data) > 0 def test_create_event(self, authenticated_client, test_calendar): """Проверка: создание события.""" now = timezone.now() data = { "calendar": test_calendar.id, "title": "Новое событие", "description": "Описание события", "start": now.isoformat(), "end": (now + timedelta(hours=2)).isoformat(), "color_event": "#ff5733", } response = authenticated_client.post( "/api/v1/schedule/events/", data, format="json" ) assert response.status_code == status.HTTP_201_CREATED assert response.data["title"] == data["title"] assert response.data["calendar"] == test_calendar.id assert "creator" in response.data def test_get_event_detail(self, authenticated_client, test_event): """Проверка: получение деталей события.""" response = authenticated_client.get(f"/api/v1/schedule/events/{test_event.id}/") assert response.status_code == status.HTTP_200_OK assert response.data["id"] == test_event.id assert response.data["title"] == test_event.title assert response.data["calendar_name"] == test_event.calendar.name def test_update_event(self, authenticated_client, test_event): """Проверка: обновление события.""" data = {"title": "Обновлённое событие", "description": "Новое описание"} response = authenticated_client.patch( f"/api/v1/schedule/events/{test_event.id}/", data, format="json" ) assert response.status_code == status.HTTP_200_OK assert response.data["title"] == data["title"] test_event.refresh_from_db() assert test_event.title == data["title"] def test_delete_event(self, authenticated_client, test_event): """Проверка: удаление события.""" event_id = test_event.id response = authenticated_client.delete(f"/api/v1/schedule/events/{event_id}/") assert response.status_code == status.HTTP_204_NO_CONTENT assert not Event.objects.filter(id=event_id).exists() def test_filter_events_by_calendar( self, authenticated_client, test_calendar, test_event ): """Проверка: фильтрация по календарю.""" response = authenticated_client.get( f"/api/v1/schedule/events/?calendar={test_calendar.id}" ) assert response.status_code == status.HTTP_200_OK data = ( response.data if isinstance(response.data, list) else response.data.get("results", []) ) assert all(e["calendar"] == test_calendar.id for e in data) def test_filter_events_by_date_range(self, authenticated_client, test_calendar): """Проверка: фильтрация по диапазону дат.""" now = timezone.now() # Создаём события в разные даты Event.objects.create( calendar=test_calendar, title="Прошлое", start=now - timedelta(days=10), end=now - timedelta(days=10) + timedelta(hours=1), ) Event.objects.create( calendar=test_calendar, title="Сегодня", start=now, end=now + timedelta(hours=1), ) Event.objects.create( calendar=test_calendar, title="Будущее", start=now + timedelta(days=10), end=now + timedelta(days=10, hours=1), ) # Запрашиваем только сегодняшние start = (now - timedelta(hours=1)).isoformat() end = (now + timedelta(hours=2)).isoformat() response = authenticated_client.get( "/api/v1/schedule/events/", {"start": start, "end": end}, ) assert response.status_code == status.HTTP_200_OK data = ( response.data if isinstance(response.data, list) else response.data.get("results", []) ) assert len(data) == 1 assert data[0]["title"] == "Сегодня" def test_event_validation_end_before_start( self, authenticated_client, test_calendar ): """Проверка: валидация - конец не может быть раньше начала.""" now = timezone.now() data = { "calendar": test_calendar.id, "title": "Невалидное событие", "start": now.isoformat(), "end": (now - timedelta(hours=1)).isoformat(), # Раньше начала! } response = authenticated_client.post( "/api/v1/schedule/events/", data, format="json" ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "end" in response.data # ==================== ТЕСТЫ ПРАВИЛ (RRULE) ==================== @pytest.mark.django_db class TestRuleAPI: """Тесты для Rule ViewSet.""" def test_list_rules(self, authenticated_client): """Проверка: список правил.""" Rule.objects.create( name="Еженедельно", frequency="WEEKLY", params={"byweekday": [0, 2, 4]}, # Пн, Ср, Пт ) response = authenticated_client.get("/api/v1/schedule/rules/") assert response.status_code == status.HTTP_200_OK assert len(response.data) > 0 def test_create_rule(self, authenticated_client): """Проверка: создание правила.""" data = { "name": "Каждый день", "description": "Ежедневное повторение", "frequency": "DAILY", "params": {}, } response = authenticated_client.post( "/api/v1/schedule/rules/", data, format="json" ) assert response.status_code == status.HTTP_201_CREATED assert response.data["name"] == data["name"] assert response.data["frequency"] == "DAILY" # ==================== ИНТЕГРАЦИОННЫЕ ТЕСТЫ ==================== @pytest.mark.django_db class TestIntegration: """Интеграционные тесты полного workflow.""" def test_full_crud_workflow(self, authenticated_client, test_user): """Проверка: полный цикл CRUD для календаря и события.""" # 1. Создаём календарь cal_response = authenticated_client.post( "/api/v1/schedule/calendars/", {"name": "Workflow Calendar"}, format="json" ) assert cal_response.status_code == status.HTTP_201_CREATED calendar_id = cal_response.data["id"] # 2. Создаём событие now = timezone.now() event_response = authenticated_client.post( "/api/v1/schedule/events/", { "calendar": calendar_id, "title": "Workflow Event", "start": now.isoformat(), "end": (now + timedelta(hours=1)).isoformat(), }, format="json", ) assert event_response.status_code == status.HTTP_201_CREATED event_id = event_response.data["id"] # 3. Проверяем, что событие видно в календаре list_response = authenticated_client.get( f"/api/v1/schedule/events/?calendar={calendar_id}" ) assert list_response.status_code == status.HTTP_200_OK data = ( list_response.data if isinstance(list_response.data, list) else list_response.data.get("results", []) ) assert len(data) == 1 # 4. Обновляем событие update_response = authenticated_client.patch( f"/api/v1/schedule/events/{event_id}/", {"title": "Updated Event"}, format="json", ) assert update_response.status_code == status.HTTP_200_OK assert update_response.data["title"] == "Updated Event" # 5. Удаляем событие delete_event_response = authenticated_client.delete( f"/api/v1/schedule/events/{event_id}/" ) assert delete_event_response.status_code == status.HTTP_204_NO_CONTENT # 6. Удаляем календарь delete_cal_response = authenticated_client.delete( f"/api/v1/schedule/calendars/{calendar_id}/" ) assert delete_cal_response.status_code == status.HTTP_204_NO_CONTENT def test_events_count_in_calendar(self, authenticated_client, test_calendar): """Проверка: счётчик событий в календаре.""" # Создаём несколько событий now = timezone.now() for i in range(3): Event.objects.create( calendar=test_calendar, title=f"Event {i}", start=now + timedelta(hours=i), end=now + timedelta(hours=i + 1), ) response = authenticated_client.get( f"/api/v1/schedule/calendars/{test_calendar.id}/" ) assert response.status_code == status.HTTP_200_OK assert response.data["events_count"] == 3 if __name__ == "__main__": pytest.main([__file__, "-v"])