# backend/tests/api/v1/test_search.py """ Тесты для глобального поиска через django-watson. """ import itertools from datetime import timedelta import pytest from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.management import call_command from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient from schedule.models import Calendar, CalendarRelation, Event from api.v1.search.views import _can_view_search_result from communications.models import Chat, Message from documents.models import Document from employees.models import Employee, Department, Position from feed.models import Post from notifications.models import Notification from requests_app.models import Request _phone_counter = itertools.count(100_000_000) _email_counter = itertools.count(1) @pytest.fixture def make_user(db): """Фабрика пользователей.""" User = get_user_model() def _make_user(email=None, password="pass12345", **extra): if email is None: email = f"user{next(_email_counter)}@example.com" phone = extra.pop("phone_number", None) or f"+79{next(_phone_counter):09d}" user = User.objects.create_user( email=email, phone_number=phone, send_activation_email=False, **extra ) if password: user.set_password(password) user.save() return user return _make_user @pytest.fixture def regular_user(make_user): """Обычный пользователь без спецправ.""" return make_user( email="regular@example.com", first_name="Регуляр", last_name="Пользователь", is_active=True, ) @pytest.fixture def api_client(regular_user): """Авторизованный DRF-клиент.""" client = APIClient() client.force_authenticate(user=regular_user) return client @pytest.fixture def sample_data(db): """Создаёт тестовые данные для поиска.""" # Отделы dept_it = Department.objects.create(name="IT отдел", description="Информационные технологии") dept_hr = Department.objects.create(name="HR отдел", description="Управление персоналом") # Должности pos_dev = Position.objects.create(name="Разработчик", description="Python/Django разработчик") pos_manager = Position.objects.create(name="Менеджер", description="HR менеджер") # Сотрудники emp1 = Employee.objects.create( email="ivanov@test.com", first_name="Иван", last_name="Иванов", patronymic="Иванович", position=pos_dev, phone_number="+79001234567" ) emp1.departments_links.create(department=dept_it) emp2 = Employee.objects.create( email="petrov@test.com", first_name="Пётр", last_name="Петров", patronymic="Петрович", position=pos_manager, phone_number="+79007654321" ) emp2.departments_links.create(department=dept_hr) # Посты post1 = Post.objects.create( author=emp1, title="Новые технологии в разработке", body="Сегодня мы внедрили новый фреймворк Django для нашего проекта" ) post2 = Post.objects.create( author=emp2, title="Вакансии в HR отделе", body="Требуется менеджер по подбору персонала" ) # Заявления req1 = Request.objects.create( employee=emp1, title="Заявка на отпуск", comment="Прошу предоставить отпуск с 1 по 14 февраля", type="vacation", status="pending" ) return { 'departments': [dept_it, dept_hr], 'employees': [emp1, emp2], 'posts': [post1, post2], 'requests': [req1], } @pytest.mark.django_db class TestSearchAPI: """Тесты для API /api/v1/search/""" def test_search_requires_authentication(self, client): """Поиск требует аутентификации.""" response = client.get('/api/v1/search/?q=test') assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_search_empty_query(self, api_client): """Пустой запрос возвращает пустые результаты.""" response = api_client.get('/api/v1/search/?q=') assert response.status_code == status.HTTP_200_OK assert response.data['query'] == '' assert response.data['total'] == 0 assert len(response.data['results']) == 0 def test_search_no_results(self, api_client, sample_data): """Поиск без результатов.""" # Пересоздаём индексы watson для тестовых данных from django.core.management import call_command call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=несуществующийтекст12345') assert response.status_code == status.HTTP_200_OK assert response.data['total'] == 0 def test_search_finds_employee_by_name(self, api_client, sample_data): """Поиск находит сотрудника по имени.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=Иванов') assert response.status_code == status.HTTP_200_OK assert response.data['total'] > 0 assert 'employee' in response.data['counts'] # Проверяем, что в результатах есть сотрудник employees = [r for r in response.data['results'] if r['model_name'] == 'employee'] assert len(employees) > 0 assert 'Иванов' in employees[0]['title'] def test_search_finds_employee_by_email(self, api_client, sample_data): """Поиск находит сотрудника по email.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=ivanov@test.com') assert response.status_code == status.HTTP_200_OK employees = [r for r in response.data['results'] if r['model_name'] == 'employee'] assert len(employees) > 0 assert 'ivanov@test.com' in employees[0]['meta']['email'].lower() def test_search_finds_department(self, api_client, sample_data): """Поиск находит отдел.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=IT') assert response.status_code == status.HTTP_200_OK departments = [r for r in response.data['results'] if r['model_name'] == 'department'] assert len(departments) > 0 assert 'IT' in departments[0]['title'] def test_search_finds_post(self, api_client, sample_data): """Поиск находит пост.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=Django') assert response.status_code == status.HTTP_200_OK posts = [r for r in response.data['results'] if r['model_name'] == 'post'] assert len(posts) > 0 assert 'Django' in posts[0]['description'] or 'Django' in posts[0]['title'] def test_search_finds_request(self, api_client, sample_data): """Поиск находит заявление.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) # Авторизуемся как автор заявления emp = sample_data['employees'][0] api_client.force_authenticate(user=emp) response = api_client.get('/api/v1/search/?q=отпуск') assert response.status_code == status.HTTP_200_OK requests = [r for r in response.data['results'] if r['model_name'] == 'request'] assert len(requests) > 0 assert 'отпуск' in requests[0]['title'].lower() def test_search_limit_parameter(self, api_client, sample_data): """Параметр limit ограничивает количество результатов на тип.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) # Создадим больше 2 постов emp = sample_data['employees'][0] for i in range(5): Post.objects.create( author=emp, title=f"Тестовый пост {i}", body="Django разработка" ) call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=Django&limit=2') assert response.status_code == status.HTTP_200_OK posts = [r for r in response.data['results'] if r['model_name'] == 'post'] assert len(posts) <= 2 def test_search_response_structure(self, api_client, sample_data): """Проверка структуры ответа API.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=Иванов') assert response.status_code == status.HTTP_200_OK # Проверяем ключи верхнего уровня assert 'query' in response.data assert 'results' in response.data assert 'counts' in response.data assert 'total' in response.data # Проверяем структуру результата if len(response.data['results']) > 0: result = response.data['results'][0] assert 'model_name' in result assert 'object_id' in result assert 'title' in result assert 'url' not in result def test_search_request_permissions(self, api_client, sample_data): """Пользователи видят только свои заявления.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) emp1 = sample_data['employees'][0] emp2 = sample_data['employees'][1] # Создаём заявление от emp1 Request.objects.create( employee=emp1, title="Личная заявка Иванова", comment="Конфиденциально", type="vacation", status="pending" ) call_command('buildwatson', verbosity=0) # Авторизуемся как emp2 api_client.force_authenticate(user=emp2) response = api_client.get('/api/v1/search/?q=Иванова') assert response.status_code == status.HTTP_200_OK # emp2 не должен видеть заявку emp1 requests = [r for r in response.data['results'] if r['model_name'] == 'request'] for req in requests: # Если есть заявления в результатах, они не должны быть от emp1 if 'Личная заявка' in req['title']: pytest.fail("Пользователь видит чужое заявление") def test_search_does_not_bypass_domain_object_permissions( self, api_client, regular_user, make_user ): owner = make_user(email="search-owner@example.com") marker = "ClassifiedSearchMarker" chat = Chat.objects.create( type="private", name=marker, created_by=owner, ) chat.participants.add(owner) Message.objects.create(chat=chat, author=owner, content=marker) document = Document.objects.create( title=marker, uploaded_by=owner, sent_to_all=False, ) calendar = Calendar.objects.create( name="Private search calendar", slug=f"private-search-{owner.id}", ) Event.objects.create( calendar=calendar, creator=owner, title=marker, start=timezone.now(), end=timezone.now() + timedelta(hours=1), ) call_command("buildwatson", verbosity=0) hidden_response = api_client.get(f"/api/v1/search/?q={marker}") hidden_models = { item["model_name"] for item in hidden_response.data["results"] } assert not _can_view_search_result(regular_user, document, "document") assert hidden_models.isdisjoint( {"chat", "message", "schedule_event"} ) chat.participants.add(regular_user) document.recipients.add(regular_user) CalendarRelation.objects.create( calendar=calendar, content_type=ContentType.objects.get_for_model(get_user_model()), object_id=regular_user.id, distinction="viewer", ) visible_response = api_client.get(f"/api/v1/search/?q={marker}") visible_models = { item["model_name"] for item in visible_response.data["results"] } assert _can_view_search_result(regular_user, document, "document") assert {"chat", "message", "schedule_event"} <= visible_models def test_search_cyrillic_query(self, api_client, sample_data): """Поиск по кириллице работает корректно.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=технологии') assert response.status_code == status.HTTP_200_OK # Должны найти пост "Новые технологии в разработке" posts = [r for r in response.data['results'] if r['model_name'] == 'post'] assert len(posts) > 0 def test_search_formats_notification_v2_without_500(self, api_client, regular_user): """Поиск по уведомлениям v2 не падает на legacy-ожиданиях формата.""" from django.core.management import call_command Notification.objects.create( recipient=regular_user, verb="system_notice", description="Это обычный текст уведомления для поиска", data={"title": "Важное уведомление"}, action_url="/documents/5/", ) call_command('buildwatson', verbosity=0) response = api_client.get('/api/v1/search/?q=обычный') assert response.status_code == status.HTTP_200_OK notifications = [ r for r in response.data['results'] if r['model_name'] == 'notification' ] assert len(notifications) > 0 assert notifications[0]['title'] == 'Важное уведомление' assert 'обычный текст уведомления' in notifications[0]['description'].lower() assert 'url' not in notifications[0] @pytest.mark.django_db class TestSearchIndexing: """Тесты индексации watson.""" def test_watson_index_auto_updates(self, api_client, sample_data): """Индекс watson обновляется автоматически при изменении данных.""" from django.core.management import call_command call_command('buildwatson', verbosity=0) # Создаём нового сотрудника Employee.objects.create( email="sidorov@test.com", first_name="Сидор", last_name="Сидоров", patronymic="Сидорович", ) # watson должен автоматически добавить в индекс response = api_client.get('/api/v1/search/?q=Сидоров') assert response.status_code == status.HTTP_200_OK employees = [r for r in response.data['results'] if r['model_name'] == 'employee'] assert len(employees) > 0 assert 'Сидоров' in employees[0]['title']