"
-
-# Получатель
-TO_EMAIL = "igor_26reg@mail.ru"
-
-def test_email():
- print("=" * 50)
- print("Тест отправки email через Яндекс SMTP")
- print("=" * 50)
- print(f"Хост: {EMAIL_HOST}:{EMAIL_PORT}")
- print(f"От кого: {EMAIL_HOST_USER}")
- print(f"Кому: {TO_EMAIL}")
- print(f"SSL: {EMAIL_USE_SSL}")
- print()
-
- # Создаем сообщение
- msg = MIMEMultipart('alternative')
- msg['Subject'] = "Тестовое письмо - проверка SMTP"
- msg['From'] = DEFAULT_FROM_EMAIL
- msg['To'] = TO_EMAIL
-
- # Текст письма
- text = "Это тестовое письмо для проверки настроек SMTP."
- html = """
-
-
-
- Тестовое письмо
- Это тестовое письмо для проверки настроек SMTP Яндекса.
- Если вы получили это письмо, значит настройки корректны.
-
-
- """
-
- part1 = MIMEText(text, 'plain', 'utf-8')
- part2 = MIMEText(html, 'html', 'utf-8')
- msg.attach(part1)
- msg.attach(part2)
-
- try:
- print("Подключение к SMTP серверу...")
- if EMAIL_USE_SSL:
- server = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT, timeout=10)
- else:
- server = smtplib.SMTP(EMAIL_HOST, EMAIL_PORT, timeout=10)
- server.starttls()
-
- print("✓ Подключение установлено")
-
- print("Авторизация...")
- server.login(EMAIL_HOST_USER, EMAIL_HOST_PASSWORD)
- print("✓ Авторизация успешна")
-
- print("Отправка письма...")
- server.send_message(msg)
- print("✓ Письмо отправлено!")
-
- server.quit()
- print()
- print("=" * 50)
- print("SUCCESS! Письмо успешно отправлено на", TO_EMAIL)
- print("=" * 50)
-
- except smtplib.SMTPAuthenticationError as e:
- print()
- print("=" * 50)
- print("ОШИБКА АВТОРИЗАЦИИ!")
- print("=" * 50)
- print(f"Код ошибки: {e.smtp_code}")
- print(f"Сообщение: {e.smtp_error.decode('utf-8')}")
- print()
- print("Возможные причины:")
- print("1. Неверный пароль приложения")
- print("2. Доступ заблокирован для данного IP-адреса")
- print("3. Необходимо создать новый пароль приложения в настройках Яндекса")
- print()
- print("Решение:")
- print("1. Зайдите на https://passport.yandex.ru/profile")
- print("2. Перейдите в раздел 'Пароли и авторизация'")
- print("3. Создайте новый пароль приложения")
- print("4. Обновите EMAIL_HOST_PASSWORD в .env файле")
-
- except smtplib.SMTPException as e:
- print()
- print("=" * 50)
- print("ОШИБКА SMTP!")
- print("=" * 50)
- print(f"Ошибка: {e}")
-
- except Exception as e:
- print()
- print("=" * 50)
- print("ОБЩАЯ ОШИБКА!")
- print("=" * 50)
- print(f"Ошибка: {e}")
- import traceback
- traceback.print_exc()
-
-if __name__ == "__main__":
- test_email()
diff --git a/backend/tests/manual/test_ip_restrictions.py b/backend/tests/manual/test_ip_restrictions.py
deleted file mode 100644
index 8b190a0a..00000000
--- a/backend/tests/manual/test_ip_restrictions.py
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/usr/bin/env python
-"""
-Тестирование IP-ограничений для регистрации.
-
-Запуск:
- python backend/test_ip_restrictions.py
-"""
-import os
-import sys
-import django
-
-# Настройка Django
-sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
-os.environ.setdefault("DJANGO_SECRET_KEY", "test-key")
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eusrr_backend.settings")
-django.setup()
-
-from common.ip_restrictions import (
- get_client_ip,
- is_local_ip,
- is_ip_allowed,
-)
-
-
-def test_is_local_ip():
- """Тест проверки локальных IP."""
- print("\n=== Тест: is_local_ip ===")
-
- test_cases = [
- ('127.0.0.1', True, 'localhost IPv4'),
- ('127.0.0.100', True, 'localhost range'),
- ('192.168.1.1', True, 'private class C'),
- ('192.168.100.50', True, 'private class C'),
- ('10.0.0.1', True, 'private class A'),
- ('172.16.0.1', True, 'private class B'),
- ('172.31.255.255', True, 'private class B edge'),
- ('8.8.8.8', False, 'public Google DNS'),
- ('203.0.113.50', False, 'public TEST-NET'),
- ('::1', True, 'localhost IPv6'),
- ('invalid-ip', False, 'invalid IP'),
- ]
-
- passed = 0
- failed = 0
-
- for ip, expected, description in test_cases:
- result = is_local_ip(ip)
- status = "✅" if result == expected else "❌"
-
- if result == expected:
- passed += 1
- else:
- failed += 1
-
- print(f"{status} {ip:20} -> {result:5} (expected: {expected:5}) # {description}")
-
- print(f"\nПройдено: {passed}/{passed + failed}")
- return failed == 0
-
-
-def test_is_ip_allowed():
- """Тест проверки разрешенных IP с кастомными настройками."""
- print("\n=== Тест: is_ip_allowed ===")
-
- # Тест 1: Разрешить все
- print("\n1. Разрешить все IP ['*']:")
- result = is_ip_allowed('203.0.113.50', ['*'])
- print(f" {'✅' if result else '❌'} 203.0.113.50 -> {result} (expected: True)")
-
- # Тест 2: Разрешить конкретную сеть
- print("\n2. Разрешить сеть 192.168.1.0/24:")
- allowed_net = ['192.168.1.0/24']
- tests = [
- ('192.168.1.1', True),
- ('192.168.1.100', True),
- ('192.168.1.255', True),
- ('192.168.2.1', False),
- ('10.0.0.1', False),
- ]
-
- for ip, expected in tests:
- result = is_ip_allowed(ip, allowed_net)
- status = "✅" if result == expected else "❌"
- print(f" {status} {ip:20} -> {result:5} (expected: {expected:5})")
-
- # Тест 3: Разрешить конкретные IP
- print("\n3. Разрешить конкретные IP ['192.168.1.100', '10.0.0.5']:")
- allowed_ips = ['192.168.1.100', '10.0.0.5']
- tests = [
- ('192.168.1.100', True),
- ('10.0.0.5', True),
- ('192.168.1.101', False),
- ('10.0.0.6', False),
- ]
-
- for ip, expected in tests:
- result = is_ip_allowed(ip, allowed_ips)
- status = "✅" if result == expected else "❌"
- print(f" {status} {ip:20} -> {result:5} (expected: {expected:5})")
-
- # Тест 4: По умолчанию (только локальные)
- print("\n4. По умолчанию (None = только локальные IP):")
- tests = [
- ('127.0.0.1', True),
- ('192.168.1.1', True),
- ('10.0.0.1', True),
- ('8.8.8.8', False),
- ('203.0.113.50', False),
- ]
-
- for ip, expected in tests:
- result = is_ip_allowed(ip, None)
- status = "✅" if result == expected else "❌"
- print(f" {status} {ip:20} -> {result:5} (expected: {expected:5})")
-
-
-def test_get_client_ip():
- """Тест получения IP из request."""
- print("\n=== Тест: get_client_ip ===")
-
- from django.test import RequestFactory
-
- factory = RequestFactory()
-
- # Тест 1: Обычный запрос
- print("\n1. Обычный запрос (без прокси):")
- request = factory.get('/')
- ip = get_client_ip(request)
- print(f" IP: {ip}")
-
- # Тест 2: С заголовком X-Forwarded-For
- print("\n2. Запрос через прокси (X-Forwarded-For):")
- request = factory.get('/', HTTP_X_FORWARDED_FOR='203.0.113.50, 192.168.1.1')
- ip = get_client_ip(request)
- expected = '203.0.113.50'
- status = "✅" if ip == expected else "❌"
- print(f" {status} IP: {ip} (expected: {expected})")
-
- # Тест 3: С одним IP в X-Forwarded-For
- print("\n3. Запрос через прокси (один IP):")
- request = factory.get('/', HTTP_X_FORWARDED_FOR='10.0.0.5')
- ip = get_client_ip(request)
- expected = '10.0.0.5'
- status = "✅" if ip == expected else "❌"
- print(f" {status} IP: {ip} (expected: {expected})")
-
-
-def main():
- """Запуск всех тестов."""
- print("=" * 60)
- print("Тестирование модуля IP-ограничений")
- print("=" * 60)
-
- try:
- test_is_local_ip()
- test_is_ip_allowed()
- test_get_client_ip()
-
- print("\n" + "=" * 60)
- print("✅ Все тесты выполнены")
- print("=" * 60)
-
- except Exception as e:
- print(f"\n❌ Ошибка: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/backend/tests/manual/test_ip_simple.py b/backend/tests/manual/test_ip_simple.py
deleted file mode 100644
index 4691c53a..00000000
--- a/backend/tests/manual/test_ip_simple.py
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/usr/bin/env python
-"""
-Простой тест IP-ограничений (без Django).
-"""
-import ipaddress
-
-
-def is_local_ip(ip_address: str) -> bool:
- """Проверяет, является ли IP-адрес локальным."""
- try:
- ip = ipaddress.ip_address(ip_address)
- return ip.is_private or ip.is_loopback or ip.is_link_local
- except ValueError:
- return False
-
-
-def is_ip_allowed(ip_address: str, allowed_networks=None):
- """Проверяет, разрешен ли доступ с данного IP-адреса."""
- if allowed_networks is None:
- return is_local_ip(ip_address)
-
- if not allowed_networks:
- return False
-
- if allowed_networks == ['*'] or '*' in allowed_networks:
- return True
-
- try:
- ip = ipaddress.ip_address(ip_address)
-
- for network_str in allowed_networks:
- if '/' in network_str:
- network = ipaddress.ip_network(network_str, strict=False)
- if ip in network:
- return True
- else:
- allowed_ip = ipaddress.ip_address(network_str)
- if ip == allowed_ip:
- return True
-
- return False
- except ValueError:
- return False
-
-
-def test_is_local_ip():
- """Тест проверки локальных IP."""
- print("\n=== Тест: is_local_ip ===")
-
- test_cases = [
- ('127.0.0.1', True, 'localhost IPv4'),
- ('127.0.0.100', True, 'localhost range'),
- ('192.168.1.1', True, 'private class C'),
- ('192.168.100.50', True, 'private class C'),
- ('10.0.0.1', True, 'private class A'),
- ('172.16.0.1', True, 'private class B'),
- ('172.31.255.255', True, 'private class B edge'),
- ('8.8.8.8', False, 'public Google DNS'),
- ('203.0.113.50', False, 'public TEST-NET'),
- ('::1', True, 'localhost IPv6'),
- ('invalid-ip', False, 'invalid IP'),
- ]
-
- passed = 0
- failed = 0
-
- for ip, expected, description in test_cases:
- result = is_local_ip(ip)
- status = "✅" if result == expected else "❌"
-
- if result == expected:
- passed += 1
- else:
- failed += 1
-
- print(f"{status} {ip:20} -> {str(result):5} "
- f"(expected: {str(expected):5}) # {description}")
-
- print(f"\nПройдено: {passed}/{passed + failed}")
- return failed == 0
-
-
-def test_is_ip_allowed():
- """Тест проверки разрешенных IP с кастомными настройками."""
- print("\n=== Тест: is_ip_allowed ===")
-
- # Тест 1: Разрешить все
- print("\n1. Разрешить все IP ['*']:")
- result = is_ip_allowed('203.0.113.50', ['*'])
- status = '✅' if result else '❌'
- print(f" {status} 203.0.113.50 -> {result} (expected: True)")
-
- # Тест 2: Разрешить конкретную сеть
- print("\n2. Разрешить сеть 192.168.1.0/24:")
- allowed_net = ['192.168.1.0/24']
- tests = [
- ('192.168.1.1', True),
- ('192.168.1.100', True),
- ('192.168.1.255', True),
- ('192.168.2.1', False),
- ('10.0.0.1', False),
- ]
-
- for ip, expected in tests:
- result = is_ip_allowed(ip, allowed_net)
- status = "✅" if result == expected else "❌"
- print(f" {status} {ip:20} -> {str(result):5} "
- f"(expected: {str(expected):5})")
-
- # Тест 3: Разрешить конкретные IP
- print("\n3. Разрешить IP ['192.168.1.100', '10.0.0.5']:")
- allowed_ips = ['192.168.1.100', '10.0.0.5']
- tests = [
- ('192.168.1.100', True),
- ('10.0.0.5', True),
- ('192.168.1.101', False),
- ('10.0.0.6', False),
- ]
-
- for ip, expected in tests:
- result = is_ip_allowed(ip, allowed_ips)
- status = "✅" if result == expected else "❌"
- print(f" {status} {ip:20} -> {str(result):5} "
- f"(expected: {str(expected):5})")
-
- # Тест 4: По умолчанию (только локальные)
- print("\n4. По умолчанию (None = только локальные IP):")
- tests = [
- ('127.0.0.1', True),
- ('192.168.1.1', True),
- ('10.0.0.1', True),
- ('8.8.8.8', False),
- ('203.0.113.50', False),
- ]
-
- for ip, expected in tests:
- result = is_ip_allowed(ip, None)
- status = "✅" if result == expected else "❌"
- print(f" {status} {ip:20} -> {str(result):5} "
- f"(expected: {str(expected):5})")
-
-
-def main():
- """Запуск всех тестов."""
- print("=" * 60)
- print("Тестирование модуля IP-ограничений")
- print("=" * 60)
-
- try:
- test_is_local_ip()
- test_is_ip_allowed()
-
- print("\n" + "=" * 60)
- print("✅ Все тесты выполнены успешно")
- print("=" * 60)
-
- except Exception as e:
- print(f"\n❌ Ошибка: {e}")
- import traceback
- traceback.print_exc()
- return 1
-
- return 0
-
-
-if __name__ == "__main__":
- exit(main())
diff --git a/backend/tests/manual/test_ldap_simple.py b/backend/tests/manual/test_ldap_simple.py
deleted file mode 100644
index fdbed769..00000000
--- a/backend/tests/manual/test_ldap_simple.py
+++ /dev/null
@@ -1,176 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-Тестовый скрипт для проверки функции normalize_avatar_to_jpeg (LDAP).
-Упрощённая версия без эмодзи для Windows консоли.
-
-Использование:
- python test_ldap_simple.py <путь_к_файлу>
-"""
-
-import os
-import sys
-from pathlib import Path
-
-# Добавляем путь к модулям Django
-sys.path.insert(0, str(Path(__file__).parent))
-
-# Настраиваем Django перед импортом
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-import django
-django.setup()
-
-try:
- from PIL import Image
-except ImportError:
- print("ERROR: Pillow не установлен. Установите: pip install Pillow")
- sys.exit(1)
-
-from employees.ldap.utils.image_utils import normalize_avatar_to_jpeg
-
-
-def get_image_info(image_path):
- """Получить информацию об изображении."""
- try:
- img = Image.open(image_path)
- size = os.path.getsize(image_path)
-
- # EXIF ориентация
- orientation_code = None
- orientation_name = "No EXIF"
- try:
- exif = img.getexif()
- if exif:
- orientation_code = exif.get(0x0112)
- orientation_names = {
- 1: "Normal", 3: "Rotated 180",
- 6: "Rotated 90 CW", 8: "Rotated 270 CW"
- }
- orientation_name = orientation_names.get(orientation_code, f"Code {orientation_code}")
- except:
- pass
-
- return {
- 'size': size,
- 'size_kb': size / 1024,
- 'dimensions': img.size,
- 'mode': img.mode,
- 'format': img.format,
- 'orientation_code': orientation_code,
- 'orientation_name': orientation_name
- }
- except Exception as e:
- return {'error': str(e)}
-
-
-def test_ldap_compression(input_path):
- """Тестировать LDAP-сжатие одного файла."""
- print("\n" + "="*70)
- print(f"LDAP Image Compression Test: {os.path.basename(input_path)}")
- print("="*70)
-
- # Информация ДО
- print("\nBEFORE processing:")
- info_before = get_image_info(input_path)
- if 'error' in info_before:
- print(f" ERROR: {info_before['error']}")
- return False
-
- print(f" Size: {info_before['size_kb']:.2f} KB")
- print(f" Resolution: {info_before['dimensions'][0]}x{info_before['dimensions'][1]}")
- print(f" Mode: {info_before['mode']}, Format: {info_before['format']}")
- print(f" EXIF Orientation: {info_before['orientation_name']}")
-
- # Обработка
- try:
- with open(input_path, 'rb') as f:
- original_data = f.read()
-
- print("\nProcessing with normalize_avatar_to_jpeg(size_px=384, max_kb=100)...")
- print(" HIGH QUALITY: progressive JPEG, 4:4:4 subsampling, quality 75-98")
- compressed_data = normalize_avatar_to_jpeg(
- original_data, size_px=384, max_kb=100
- )
-
- # Сохранение
- output_path = input_path.replace('.', '_ldap.')
- if not output_path.endswith('.jpg'):
- output_path = os.path.splitext(output_path)[0] + '.jpg'
-
- with open(output_path, 'wb') as f:
- f.write(compressed_data)
-
- print(f" Saved to: {output_path}")
-
- except Exception as e:
- print(f" ERROR: {e}")
- import traceback
- traceback.print_exc()
- return False
-
- # Информация ПОСЛЕ
- print("\nAFTER processing (LDAP format):")
- info_after = get_image_info(output_path)
- if 'error' in info_after:
- print(f" ERROR: {info_after['error']}")
- return False
-
- print(f" Size: {info_after['size_kb']:.2f} KB")
- print(f" Resolution: {info_after['dimensions'][0]}x{info_after['dimensions'][1]}")
- print(f" Mode: {info_after['mode']}, Format: {info_after['format']}")
- print(f" EXIF Orientation: {info_after['orientation_name']}")
-
- # Проверка лимитов
- size_ok = info_after['size_kb'] <= 100
- print(f"\nLDAP Compliance Check:")
- print(f" [{'OK' if size_ok else 'FAIL'}] File size: {info_after['size_kb']:.2f} KB (limit: 100 KB)")
-
- # Статистика
- size_reduction = ((info_before['size'] - info_after['size']) / info_before['size']) * 100
-
- print(f"\nStatistics:")
- print(f" Size reduction: {size_reduction:.1f}%")
- print(f" Before: {info_before['dimensions'][0]}x{info_before['dimensions'][1]} ({info_before['size_kb']:.2f} KB)")
- print(f" After: {info_after['dimensions'][0]}x{info_after['dimensions'][1]} ({info_after['size_kb']:.2f} KB)")
-
- # Проверка ориентации
- if info_before['orientation_code'] and info_before['orientation_code'] != 1:
- if info_after['orientation_code'] is None or info_after['orientation_code'] == 1:
- print(f" [OK] Orientation fixed automatically!")
- print(f" Was: {info_before['orientation_name']}")
- print(f" Now: Normal (rotated in pixels)")
- else:
- print(f" [WARN] EXIF orientation preserved: {info_after['orientation_name']}")
-
- print("="*70)
- return True
-
-
-if __name__ == '__main__':
- if len(sys.argv) < 2:
- print("Usage: python test_ldap_simple.py ")
- print("\nExample:")
- print(" python test_ldap_simple.py photo.jpg")
- sys.exit(1)
-
- input_path = sys.argv[1]
-
- if not os.path.exists(input_path):
- print(f"ERROR: File not found: {input_path}")
- sys.exit(1)
-
- if not os.path.isfile(input_path):
- print(f"ERROR: Not a file: {input_path}")
- sys.exit(1)
-
- try:
- success = test_ldap_compression(input_path)
- sys.exit(0 if success else 1)
- except KeyboardInterrupt:
- print("\n\nInterrupted by user")
- sys.exit(130)
- except Exception as e:
- print(f"\nCRITICAL ERROR: {e}")
- import traceback
- traceback.print_exc()
- sys.exit(1)
diff --git a/backend/tests/manual/test_notification_create.py b/backend/tests/manual/test_notification_create.py
deleted file mode 100644
index 45c817a8..00000000
--- a/backend/tests/manual/test_notification_create.py
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env python
-"""
-Скрипт для создания тестового уведомления
-"""
-import os
-import sys
-import django
-
-# Настройка Django
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from employees.models import Employee
-from notifications.services import NotificationService
-
-def main():
- """Создать тестовое уведомление"""
- # Получаем первого пользователя
- user = Employee.objects.filter(id=1).first()
-
- if not user:
- print("❌ Нет пользователей в системе!")
- sys.exit(1)
-
- # Создаём тестовое уведомление
- notification = NotificationService.create_notification(
- recipient=user,
- notification_type_code='system_announcement',
- title='🎉 Тестовое уведомление',
- message='Проверка real-time уведомлений через WebSocket. Вы должны увидеть toast справа вверху!',
- action_url='/',
- )
-
- print(f'✅ Создано уведомление ID={notification.id}')
- print(f'👤 Получатель: {user.last_name} {user.first_name}')
- print(f'📧 Отправлено на веб: {notification.sent_web}')
- print(f'📝 Приоритет: {notification.notification_type.priority}')
- print(f'🔔 Категория: {notification.notification_type.category.name}')
- print()
- print('Проверьте браузер - должен появиться toast и обновиться badge!')
-
-if __name__ == '__main__':
- main()
diff --git a/backend/tests/manual/test_notification_debug.py b/backend/tests/manual/test_notification_debug.py
deleted file mode 100644
index d11f3ed3..00000000
--- a/backend/tests/manual/test_notification_debug.py
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/env python
-"""
-Скрипт для отладки уведомлений
-"""
-import os
-import sys
-import django
-import logging
-
-# Включаем подробное логирование
-logging.basicConfig(level=logging.DEBUG)
-
-# Настройка Django
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from employees.models import Employee
-from notifications.services import NotificationService
-from channels.layers import get_channel_layer
-from asgiref.sync import async_to_sync
-
-def main():
- """Создать тестовое уведомление с отладкой"""
- print("=" * 60)
- print("ТЕСТ СИСТЕМЫ УВЕДОМЛЕНИЙ")
- print("=" * 60)
-
- # Проверка Channel Layer
- channel_layer = get_channel_layer()
- print(f"\n1. Channel Layer: {type(channel_layer).__name__}")
-
- # Получаем пользователя
- user = Employee.objects.first()
- if not user:
- print("❌ Нет пользователей в системе!")
- sys.exit(1)
-
- print(f"\n2. Получатель: {user.last_name} {user.first_name} (ID={user.id})")
- print(f" Email: {user.email}")
-
- # Создаём уведомление
- print("\n3. Создание уведомления...")
- notification = NotificationService.create_notification(
- recipient=user,
- notification_type_code='system_announcement',
- title='🔔 Тестовое уведомление',
- message='Это тестовое сообщение для проверки WebSocket. Если вы видите это в браузере - система работает!',
- action_url='/',
- )
-
- if not notification:
- print("❌ Не удалось создать уведомление!")
- sys.exit(1)
-
- print(f"✅ Уведомление создано: ID={notification.id}")
- print(f" Заголовок: {notification.title}")
- print(f" Отправлено веб: {notification.sent_web}")
- print(f" Дата отправки: {notification.sent_at}")
-
- # Проверка отправки через WebSocket
- print(f"\n4. WebSocket группа: notifications_{user.id}")
-
- # Попробуем отправить напрямую
- print("\n5. Отправка тестового сообщения через Channel Layer...")
- try:
- async_to_sync(channel_layer.group_send)(
- f'notifications_{user.id}',
- {
- 'type': 'notification_new',
- 'notification': {
- 'id': notification.id,
- 'title': '🧪 Прямое тестовое сообщение',
- 'message': 'Это сообщение отправлено напрямую через Channel Layer',
- 'category': 'system',
- 'icon': 'bi-gear',
- 'color': 'light',
- 'priority': 'high',
- }
- }
- )
- print("✅ Сообщение отправлено в Channel Layer")
- except Exception as e:
- print(f"❌ Ошибка отправки: {e}")
-
- print("\n" + "=" * 60)
- print("ПРОВЕРЬТЕ БРАУЗЕР!")
- print("=" * 60)
- print("Вы должны увидеть:")
- print(" 1. Toast уведомление справа вверху")
- print(" 2. Badge с цифрой в navbar")
- print(" 3. В консоли: [Notifications] Received: ...")
- print("=" * 60)
-
-if __name__ == '__main__':
- main()
diff --git a/backend/tests/manual/test_notification_signals.py b/backend/tests/manual/test_notification_signals.py
deleted file mode 100644
index b510bc4e..00000000
--- a/backend/tests/manual/test_notification_signals.py
+++ /dev/null
@@ -1,428 +0,0 @@
-#!/usr/bin/env python
-"""
-Тестовый скрипт для проверки работы notification signals.
-
-Тестирует все 5 модулей:
-1. Communications - сообщения, упоминания, ответы
-2. Requests - заявки, статусы, комментарии
-3. Documents - документы, ознакомления
-4. Calendar - события, изменения
-5. Feed - публикации, комментарии
-
-Запуск:
- python test_notification_signals.py
-"""
-
-import os
-import django
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from django.contrib.auth import get_user_model
-from communications.models import Chat, Message
-from requests_app.models import Request, RequestComment
-from documents.models import Document, DocumentAcknowledgement
-from calendar_app.models import CalendarEvent
-from feed.models import Post, Comment
-from notifications.models import Notification
-from employees.models import Department
-
-Employee = get_user_model()
-
-# ANSI цвета для красивого вывода
-class Colors:
- HEADER = '\033[95m'
- OKBLUE = '\033[94m'
- OKCYAN = '\033[96m'
- OKGREEN = '\033[92m'
- WARNING = '\033[93m'
- FAIL = '\033[91m'
- ENDC = '\033[0m'
- BOLD = '\033[1m'
- UNDERLINE = '\033[4m'
-
-
-def print_header(text):
- print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}")
- print(f"{Colors.HEADER}{Colors.BOLD}{text:^60}{Colors.ENDC}")
- print(f"{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}\n")
-
-
-def print_success(text):
- print(f"{Colors.OKGREEN}[OK] {text}{Colors.ENDC}")
-
-
-def print_error(text):
- print(f"{Colors.FAIL}[ERROR] {text}{Colors.ENDC}")
-
-
-def print_info(text):
- print(f"{Colors.OKCYAN}[INFO] {text}{Colors.ENDC}")
-
-
-def get_or_create_test_users():
- """Создает или получает тестовых пользователей."""
- print_info("Подготовка тестовых пользователей...")
-
- user1, created = Employee.objects.get_or_create(
- email='test_user1@test.com',
- defaults={
- 'first_name': 'Иван',
- 'last_name': 'Иванов',
- 'is_active': True,
- 'phone_number': '+79001111111'
- }
- )
- if created:
- user1.set_password('testpass123')
- user1.save()
-
- user2, created = Employee.objects.get_or_create(
- email='test_user2@test.com',
- defaults={
- 'first_name': 'Петр',
- 'last_name': 'Петров',
- 'is_active': True,
- 'phone_number': '+79002222222'
- }
- )
- if created:
- user2.set_password('testpass123')
- user2.save()
-
- print_success(f"Пользователи готовы: {user1.email}, {user2.email}")
- return user1, user2
-
-
-def test_communications(user1, user2):
- """Тестирует signals модуля Communications."""
- print_header("ТЕСТ 1: Communications")
-
- # Очистка предыдущих уведомлений
- Notification.objects.filter(recipient__in=[user1, user2]).delete()
-
- # 1. Создаем приватный чат
- chat = Chat.objects.create(type='private')
- chat.participants.add(user1, user2)
- print_success(f"Создан чат #{chat.id}")
-
- # Проверяем уведомление о добавлении в чат
- notif_count = Notification.objects.filter(
- notification_type__code='chat_added_to_chat'
- ).count()
- print_info(f"Уведомлений о добавлении в чат: {notif_count}")
-
- # 2. Простое сообщение
- msg1 = Message.objects.create(
- chat=chat,
- author=user1,
- content='Привет! Как дела?'
- )
- print_success(f"Создано сообщение #{msg1.id}")
-
- notif = Notification.objects.filter(
- recipient=user2,
- notification_type__code='chat_new_message'
- ).first()
- if notif:
- print_success(f"[+] Уведомление получено: {notif.title}")
- else:
- print_error("Уведомление о новом сообщении НЕ создано!")
-
- # 3. Сообщение с упоминанием
- msg2 = Message.objects.create(
- chat=chat,
- author=user1,
- content=f'Привет @{user2.email}, нужна твоя помощь!'
- )
- print_success(f"Создано сообщение с упоминанием #{msg2.id}")
-
- mention_notif = Notification.objects.filter(
- recipient=user2,
- notification_type__code='chat_mention'
- ).first()
- if mention_notif:
- print_success(f"[+] Уведомление об упоминании: {mention_notif.title}")
- else:
- print_error("Уведомление об упоминании НЕ создано!")
-
- # 4. Ответ на сообщение
- msg3 = Message.objects.create(
- chat=chat,
- author=user2,
- content='Конечно, помогу!',
- reply_to=msg1
- )
- print_success(f"Создан ответ на сообщение #{msg3.id}")
-
- reply_notif = Notification.objects.filter(
- recipient=user1,
- notification_type__code='chat_reply'
- ).first()
- if reply_notif:
- print_success(f"[+] Уведомление об ответе: {reply_notif.title}")
- else:
- print_error("Уведомление об ответе НЕ создано!")
-
- total = Notification.objects.filter(recipient__in=[user1, user2]).count()
- print_info(f"Всего создано уведомлений: {total}")
-
-
-def test_requests(user1, user2):
- """Тестирует signals модуля Requests."""
- print_header("ТЕСТ 2: Requests")
-
- Notification.objects.filter(recipient__in=[user1, user2]).delete()
-
- # 1. Создаем заявку
- request = Request.objects.create(
- employee=user1,
- type='vacation',
- title='Отпуск',
- comment='Прошу предоставить отпуск',
- status='pending',
- approver=user2
- )
- print_success(f"Создана заявка #{request.id}")
-
- new_request_notif = Notification.objects.filter(
- notification_type__code='request_new'
- ).first()
- if new_request_notif:
- print_success(f"✓ Уведомление о новой заявке: {new_request_notif.title}")
- else:
- print_error("Уведомление о новой заявке НЕ создано!")
-
- # 2. Одобряем заявку
- from django.utils import timezone
- request.status = 'approved'
- request.decided_at = timezone.now()
- request.save()
- print_success(f"Заявка #{request.id} одобрена")
-
- approved_notif = Notification.objects.filter(
- recipient=user1,
- notification_type__code='request_approved'
- ).first()
- if approved_notif:
- print_success(f"✓ Уведомление об одобрении: {approved_notif.title}")
- else:
- print_error("Уведомление об одобрении НЕ создано!")
-
- # 3. Добавляем комментарий
- comment = RequestComment.objects.create(
- request=request,
- author=user2,
- text='Одобрено, хорошего отдыха!'
- )
- print_success(f"Добавлен комментарий #{comment.id}")
-
- comment_notif = Notification.objects.filter(
- recipient=user1,
- notification_type__code='request_comment'
- ).first()
- if comment_notif:
- print_success(f"✓ Уведомление о комментарии: {comment_notif.title}")
- else:
- print_error("Уведомление о комментарии НЕ создано!")
-
- total = Notification.objects.filter(recipient__in=[user1, user2]).count()
- print_info(f"Всего создано уведомлений: {total}")
-
-
-def test_documents(user1, user2):
- """Тестирует signals модуля Documents."""
- print_header("ТЕСТ 3: Documents")
-
- Notification.objects.filter(recipient__in=[user1, user2]).delete()
-
- # 1. Создаем документ для конкретных получателей
- doc = Document.objects.create(
- title='Важный документ',
- description='Требует ознакомления',
- uploaded_by=user1
- )
- doc.recipients.add(user2)
- print_success(f"Создан документ #{doc.id}")
-
- doc_notif = Notification.objects.filter(
- recipient=user2,
- notification_type__code='document_ready'
- ).first()
- if doc_notif:
- print_success(f"✓ Уведомление о документе: {doc_notif.title}")
- else:
- print_error("Уведомление о документе НЕ создано!")
-
- # 2. Ознакомление с документом
- ack = DocumentAcknowledgement.objects.create(
- document=doc,
- user=user2
- )
- print_success(f"Пользователь {user2.username} ознакомился")
-
- # Проверяем уведомление о полном ознакомлении
- all_ack_notif = Notification.objects.filter(
- recipient=user1,
- notification_type__code='document_signed_all'
- ).first()
- if all_ack_notif:
- print_success(f"✓ Уведомление о полном ознакомлении: {all_ack_notif.title}")
- else:
- print_error("Уведомление о полном ознакомлении НЕ создано!")
-
- total = Notification.objects.filter(recipient__in=[user1, user2]).count()
- print_info(f"Всего создано уведомлений: {total}")
-
-
-def test_calendar(user1, user2):
- """Тестирует signals модуля Calendar."""
- print_header("ТЕСТ 4: Calendar")
-
- Notification.objects.filter(recipient__in=[user1, user2]).delete()
-
- from datetime import date, timedelta
-
- # 1. Создаем событие компании
- event = CalendarEvent.objects.create(
- title='Общее собрание',
- description='Ежеквартальное собрание',
- start_date=date.today() + timedelta(days=7),
- created_by=user1
- )
- print_success(f"Создано событие #{event.id}")
-
- event_notif = Notification.objects.filter(
- notification_type__code='event_created'
- ).first()
- if event_notif:
- print_success(f"✓ Уведомление о событии: {event_notif.title}")
- print_info(f" Получателей: {Notification.objects.filter(notification_type__code='event_created').count()}")
- else:
- print_error("Уведомление о событии НЕ создано!")
-
- # 2. Изменяем событие
- Notification.objects.all().delete() # Очищаем для чистоты теста
- event.title = 'ВАЖНОЕ Общее собрание'
- event.save()
- print_success(f"Событие #{event.id} изменено")
-
- changed_notif = Notification.objects.filter(
- notification_type__code='event_changed'
- ).first()
- if changed_notif:
- print_success(f"✓ Уведомление об изменении: {changed_notif.title}")
- else:
- print_error("Уведомление об изменении НЕ создано!")
-
- # 3. Удаляем событие
- Notification.objects.all().delete()
- event.delete()
- print_success("Событие удалено")
-
- cancelled_notif = Notification.objects.filter(
- notification_type__code='event_cancelled'
- ).first()
- if cancelled_notif:
- print_success(f"✓ Уведомление об отмене: {cancelled_notif.title}")
- else:
- print_error("Уведомление об отмене НЕ создано!")
-
-
-def test_feed(user1, user2):
- """Тестирует signals модуля Feed."""
- print_header("ТЕСТ 5: Feed")
-
- Notification.objects.filter(recipient__in=[user1, user2]).delete()
-
- # 1. Создаем публикацию компании
- post = Post.objects.create(
- author=user1,
- title='Важная новость',
- body='Текст важной новости для всех сотрудников',
- type='company'
- )
- print_success(f"Создана публикация #{post.id}")
-
- post_notif = Notification.objects.filter(
- notification_type__code='feed_new_post'
- ).first()
- if post_notif:
- print_success(f"✓ Уведомление о публикации: {post_notif.title}")
- print_info(f" Получателей: {Notification.objects.filter(notification_type__code='feed_new_post').count()}")
- else:
- print_error("Уведомление о публикации НЕ создано!")
-
- # 2. Добавляем комментарий
- comment = Comment.objects.create(
- post=post,
- author=user2,
- text='Отличная новость!'
- )
- print_success(f"Добавлен комментарий #{comment.id}")
-
- comment_notif = Notification.objects.filter(
- recipient=user1,
- notification_type__code='feed_post_comment'
- ).first()
- if comment_notif:
- print_success(f"✓ Уведомление о комментарии: {comment_notif.title}")
- else:
- print_error("Уведомление о комментарии НЕ создано!")
-
- # 3. Тестируем функцию реакции
- from feed.notification_signals import notify_post_reaction
- notify_post_reaction(post, user2)
- print_success("Вызвана функция реакции")
-
- reaction_notif = Notification.objects.filter(
- recipient=user1,
- notification_type__code='feed_post_reaction'
- ).first()
- if reaction_notif:
- print_success(f"✓ Уведомление о реакции: {reaction_notif.title}")
- else:
- print_error("Уведомление о реакции НЕ создано!")
-
-
-def main():
- """Основная функция запуска всех тестов."""
- print(f"{Colors.BOLD}{Colors.OKBLUE}")
- print("="*60)
- print(" ТЕСТИРОВАНИЕ СИСТЕМЫ УВЕДОМЛЕНИЙ - ЭТАП 3")
- print(" Интеграция с модулями через signals")
- print("="*60)
- print(f"{Colors.ENDC}")
-
- try:
- # Подготовка
- user1, user2 = get_or_create_test_users()
-
- # Запуск тестов
- test_communications(user1, user2)
- test_requests(user1, user2)
- test_documents(user1, user2)
- test_calendar(user1, user2)
- test_feed(user1, user2)
-
- # Итоговая статистика
- print_header("ИТОГИ")
- total_notifications = Notification.objects.count()
- total_types_used = Notification.objects.values(
- 'notification_type__code'
- ).distinct().count()
-
- print_success(f"Всего уведомлений в БД: {total_notifications}")
- print_success(f"Использовано типов: {total_types_used}")
-
- print(f"\n{Colors.OKGREEN}{Colors.BOLD}✓ ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ!{Colors.ENDC}\n")
-
- except Exception as e:
- print_error(f"ОШИБКА: {e}")
- import traceback
- traceback.print_exc()
-
-
-if __name__ == '__main__':
- main()
diff --git a/backend/tests/manual/test_pagination.py b/backend/tests/manual/test_pagination.py
deleted file mode 100644
index 48e75168..00000000
--- a/backend/tests/manual/test_pagination.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python
-"""
-Скрипт для тестирования пагинации в API эндпоинтах.
-Запуск: python backend/test_pagination.py
-"""
-
-import os
-import sys
-import django
-
-# Настройка Django
-sys.path.insert(0, os.path.dirname(__file__))
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from django.contrib.auth import get_user_model
-from api.client import get_api_client
-from django.test import RequestFactory
-
-User = get_user_model()
-
-def test_pagination():
- """Тестирует пагинацию для всех основных эндпоинтов."""
-
- # Создаём fake request для get_api_client
- factory = RequestFactory()
-
- # Берём первого суперпользователя
- admin = User.objects.filter(is_superuser=True).first()
- if not admin:
- print("❌ Нет суперпользователя в БД")
- return
-
- print(f"✅ Используем пользователя: {admin.email}\n")
-
- # Создаём fake HTTP request
- fake_request = factory.get('/')
- fake_request.user = admin
-
- # Получаем API client
- api = get_api_client(fake_request)
-
- endpoints = [
- ('Отделы', 'v1/departments/'),
- ('Сотрудники', 'v1/employees/'),
- ('Документы', 'v1/documents/'),
- ('Заявления', 'v1/requests/'),
- ('Новости', 'v1/posts/'),
- ]
-
- print("=" * 70)
- print("ПРОВЕРКА ПАГИНАЦИИ API")
- print("=" * 70 + "\n")
-
- for name, endpoint in endpoints:
- print(f"📍 {name} ({endpoint})")
- print("-" * 70)
-
- # Запрос без параметров
- resp = api.get(endpoint)
-
- if not resp.ok:
- print(f" ❌ Ошибка: HTTP {resp.status}")
- print()
- continue
-
- data = resp.json
-
- # Проверяем формат ответа
- if isinstance(data, dict):
- if 'results' in data:
- print(f" ✅ Пагинация ВКЛЮЧЕНА")
- print(f" 📊 Всего: {data.get('count', '?')}")
- print(f" 📄 На странице: {len(data.get('results', []))}")
- print(f" ⏭️ Следующая: {'Да' if data.get('next') else 'Нет'}")
- print(f" ⏮️ Предыдущая: {'Да' if data.get('previous') else 'Нет'}")
- else:
- print(f" ⚠️ Словарь БЕЗ 'results' (возможно другой формат)")
- print(f" 📄 Ключи: {list(data.keys())[:5]}")
- elif isinstance(data, list):
- print(f" ❌ Пагинация ОТКЛЮЧЕНА (возвращается список)")
- print(f" 📄 Элементов: {len(data)}")
- else:
- print(f" ⚠️ Неожиданный тип: {type(data)}")
-
- # Тест с параметром page=2
- print("\n 🔍 Тест page=2:")
- resp2 = api.get(endpoint, params={'page': 2})
- if resp2.ok:
- data2 = resp2.json
- if isinstance(data2, dict) and 'results' in data2:
- print(f" ✅ Страница 2 работает ({len(data2.get('results', []))} элементов)")
- else:
- print(f" ⚠️ Страница 2: неожиданный формат")
- else:
- print(f" ⚠️ Страница 2: HTTP {resp2.status}")
-
- print()
-
- print("=" * 70)
- print("ПРОВЕРКА ЗАВЕРШЕНА")
- print("=" * 70)
-
-
-if __name__ == '__main__':
- test_pagination()
diff --git a/backend/tests/manual/test_recipients.py b/backend/tests/manual/test_recipients.py
deleted file mode 100644
index cfdcaa4e..00000000
--- a/backend/tests/manual/test_recipients.py
+++ /dev/null
@@ -1,310 +0,0 @@
-#!/usr/bin/env python
-"""
-Скрипт для тестирования функциональности получателей в заявлениях
-"""
-import os
-import django
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from django.contrib.auth import get_user_model
-from requests_app.models import Request
-from employees.models import Department, EmployeeDepartment
-from requests_app.enums import RequestStatus, RequestType
-
-User = get_user_model()
-
-def print_section(title):
- print(f"\n{'='*60}")
- print(f" {title}")
- print(f"{'='*60}\n")
-
-def test_model_fields():
- """Тест 1: Проверка новых полей модели"""
- print_section("ТЕСТ 1: Проверка полей модели Request")
-
- # Проверяем, что поля существуют
- request = Request.objects.first()
- if request:
- print(f"✓ Найдена заявка: #{request.id} - {request.display_title}")
- print(f" - departments (ManyToMany): {request.departments.count()} отделов")
- print(f" - recipients (ManyToMany): {request.recipients.count()} получателей")
- print(f" - cc_users (ManyToMany): {request.cc_users.count()} в копии")
- print(f" - sent_to_all_department: {request.sent_to_all_department}")
- print(f" - department (старое поле): {request.department}")
- else:
- print("⚠ Нет заявок в БД. Создадим тестовую...")
-
- # Создаем тестовые данные
- author = User.objects.filter(is_active=True).first()
- if not author:
- print("✗ Не найдено активных пользователей!")
- return False
-
- request = Request.objects.create(
- employee=author,
- type=RequestType.VACATION,
- title="Тестовая заявка с получателями",
- date_from="2025-12-10",
- date_to="2025-12-20",
- comment="Тест новой функциональности",
- status=RequestStatus.PENDING
- )
- print(f"✓ Создана заявка #{request.id}")
-
- return True
-
-def test_add_recipients():
- """Тест 2: Добавление получателей"""
- print_section("ТЕСТ 2: Добавление получателей")
-
- # Находим или создаем заявку
- request = Request.objects.first()
- if not request:
- print("✗ Нет заявок для тестирования")
- return False
-
- print(f"Работаем с заявкой #{request.id}")
-
- # Получаем пользователей для теста
- users = User.objects.filter(is_active=True).exclude(
- id=request.employee_id
- )[:3]
-
- if len(users) < 2:
- print("⚠ Недостаточно пользователей для теста")
- return False
-
- # Добавляем основных получателей
- recipient1, recipient2 = users[0], users[1]
- request.add_recipient(recipient1, is_cc=False)
- request.add_recipient(recipient2, is_cc=False)
- print(f"✓ Добавлены основные получатели:")
- print(f" - {recipient1.get_full_name() or recipient1.username}")
- print(f" - {recipient2.get_full_name() or recipient2.username}")
-
- # Добавляем в копию
- if len(users) >= 3:
- cc_user = users[2]
- request.add_recipient(cc_user, is_cc=True)
- print(f"✓ Добавлен в копию:")
- print(f" - {cc_user.get_full_name() or cc_user.username}")
-
- # Проверяем количество
- print(f"\nИтого:")
- print(f" - Основных получателей: {request.recipients.count()}")
- print(f" - В копии: {request.cc_users.count()}")
-
- return True
-
-def test_is_recipient():
- """Тест 3: Проверка метода is_recipient"""
- print_section("ТЕСТ 3: Метод is_recipient()")
-
- request = Request.objects.first()
- if not request:
- print("✗ Нет заявок для тестирования")
- return False
-
- print(f"Заявка #{request.id}")
-
- # Проверяем автора
- print(f"\n1. Автор заявки:")
- print(f" {request.employee.get_full_name() or request.employee.username}")
- print(f" is_recipient: {request.is_recipient(request.employee)}")
-
- # Проверяем получателей
- if request.recipients.exists():
- recipient = request.recipients.first()
- print(f"\n2. Основной получатель:")
- print(f" {recipient.get_full_name() or recipient.username}")
- print(f" is_recipient: ✓ {request.is_recipient(recipient)}")
-
- # Проверяем CC
- if request.cc_users.exists():
- cc_user = request.cc_users.first()
- print(f"\n3. Пользователь в копии:")
- print(f" {cc_user.get_full_name() or cc_user.username}")
- print(f" is_recipient: ✓ {request.is_recipient(cc_user)}")
-
- # Проверяем случайного пользователя
- random_user = User.objects.exclude(
- id__in=[request.employee_id] +
- list(request.recipients.values_list('id', flat=True)) +
- list(request.cc_users.values_list('id', flat=True))
- ).first()
-
- if random_user:
- print(f"\n4. Случайный пользователь:")
- print(f" {random_user.get_full_name() or random_user.username}")
- print(f" is_recipient: {request.is_recipient(random_user)}")
-
- return True
-
-def test_departments():
- """Тест 4: Работа с несколькими отделами"""
- print_section("ТЕСТ 4: Множественные отделы")
-
- request = Request.objects.first()
- if not request:
- print("✗ Нет заявок для тестирования")
- return False
-
- # Получаем отделы
- departments = Department.objects.all()[:2]
- if len(departments) < 2:
- print("⚠ Недостаточно отделов для теста")
- if len(departments) == 1:
- request.departments.add(departments[0])
- print(f"✓ Добавлен 1 отдел: {departments[0].name}")
- else:
- request.departments.set(departments)
- print(f"✓ Добавлено {len(departments)} отделов:")
- for dept in departments:
- print(f" - {dept.name}")
-
- print(f"\nВсего отделов: {request.departments.count()}")
-
- return True
-
-def test_sent_to_all():
- """Тест 5: Флаг sent_to_all_department"""
- print_section("ТЕСТ 5: Флаг sent_to_all_department")
-
- request = Request.objects.first()
- if not request:
- print("✗ Нет заявок для тестирования")
- return False
-
- # Включаем флаг
- request.sent_to_all_department = True
- request.save()
- print(f"✓ Флаг sent_to_all_department установлен в True")
-
- # Проверяем, что есть отделы
- dept_count = request.departments.count()
- print(f" Выбрано отделов: {dept_count}")
-
- if dept_count > 0:
- # Считаем сотрудников в этих отделах
- dept_employees = User.objects.filter(
- departments_links__department__in=request.departments.all(),
- departments_links__is_active=True,
- is_active=True
- ).distinct().count()
-
- print(f" Сотрудников в отделах: {dept_employees}")
- print(f" Они все теперь видят эту заявку!")
-
- return True
-
-def test_all_recipients_property():
- """Тест 6: Свойство all_recipients"""
- print_section("ТЕСТ 6: Свойство all_recipients")
-
- request = Request.objects.first()
- if not request:
- print("✗ Нет заявок для тестирования")
- return False
-
- all_recip = request.all_recipients
- primary = request.primary_recipients
-
- print(f"Всего получателей (recipients + cc): {all_recip.count()}")
- print(f"Основных получателей: {primary.count()}")
- print(f"В копии: {request.cc_users.count()}")
-
- if all_recip.exists():
- print("\nСписок всех получателей:")
- for user in all_recip:
- is_primary = request.recipients.filter(id=user.id).exists()
- is_cc = request.cc_users.filter(id=user.id).exists()
- role = "основной" if is_primary else "CC"
- print(f" - {user.get_full_name() or user.username} ({role})")
-
- return True
-
-def test_api_serializer():
- """Тест 7: Проверка сериализатора"""
- print_section("ТЕСТ 7: Сериализация через API")
-
- from api.v1.requests_app.serializers import RequestReadSerializer
- from rest_framework.request import Request as DRFRequest
- from django.test import RequestFactory
-
- request_obj = Request.objects.first()
- if not request_obj:
- print("✗ Нет заявок для тестирования")
- return False
-
- # Создаем mock request
- factory = RequestFactory()
- django_request = factory.get('/')
- django_request.user = request_obj.employee
- drf_request = DRFRequest(django_request)
-
- # Сериализуем
- serializer = RequestReadSerializer(
- request_obj,
- context={'request': drf_request}
- )
- data = serializer.data
-
- print(f"Заявка #{data['id']}:")
- print(f" - departments: {len(data.get('departments', []))}")
- print(f" - recipients: {len(data.get('recipients', []))}")
- print(f" - cc_users: {len(data.get('cc_users', []))}")
- print(f" - recipient_count: {data.get('recipient_count', 0)}")
- print(f" - cc_count: {data.get('cc_count', 0)}")
- print(f" - is_recipient: {data.get('is_recipient', False)}")
- print(f" - sent_to_all_department: {data.get('sent_to_all_department', False)}")
-
- return True
-
-def run_all_tests():
- """Запуск всех тестов"""
- print("\n" + "="*60)
- print(" 🧪 ТЕСТИРОВАНИЕ ФУНКЦИОНАЛЬНОСТИ ПОЛУЧАТЕЛЕЙ")
- print("="*60)
-
- tests = [
- ("Проверка полей модели", test_model_fields),
- ("Добавление получателей", test_add_recipients),
- ("Метод is_recipient", test_is_recipient),
- ("Множественные отделы", test_departments),
- ("Флаг sent_to_all_department", test_sent_to_all),
- ("Свойство all_recipients", test_all_recipients_property),
- ("Сериализация API", test_api_serializer),
- ]
-
- results = []
- for name, test_func in tests:
- try:
- result = test_func()
- results.append((name, result))
- except Exception as e:
- print(f"\n✗ ОШИБКА в тесте '{name}':")
- print(f" {type(e).__name__}: {e}")
- import traceback
- traceback.print_exc()
- results.append((name, False))
-
- # Итоги
- print_section("ИТОГИ ТЕСТИРОВАНИЯ")
- passed = sum(1 for _, r in results if r)
- total = len(results)
-
- for name, result in results:
- status = "✓ PASSED" if result else "✗ FAILED"
- print(f"{status:12} | {name}")
-
- print(f"\n{'='*60}")
- print(f"Пройдено: {passed}/{total} тестов ({passed*100//total}%)")
- print(f"{'='*60}\n")
-
- return passed == total
-
-if __name__ == '__main__':
- success = run_all_tests()
- exit(0 if success else 1)
diff --git a/backend/tests/manual/test_recipients_api.py b/backend/tests/manual/test_recipients_api.py
deleted file mode 100644
index c1ae6f11..00000000
--- a/backend/tests/manual/test_recipients_api.py
+++ /dev/null
@@ -1,247 +0,0 @@
-#!/usr/bin/env python
-"""
-Тестирование API получателей заявлений
-"""
-import os
-import django
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-import json
-from django.contrib.auth import get_user_model
-from django.test import RequestFactory
-from rest_framework.test import force_authenticate
-from api.v1.requests_app.views import RequestViewSet
-from api.v1.requests_app.serializers import RequestWriteSerializer
-from requests_app.models import Request
-from employees.models import Department
-
-User = get_user_model()
-
-
-def test_create_request_with_recipients():
- """Тест создания заявки с получателями через API"""
- print("\n" + "="*60)
- print(" ТЕСТ: Создание заявки с получателями")
- print("="*60)
-
- # Получаем пользователей
- author = User.objects.filter(is_active=True).first()
- users = User.objects.filter(is_active=True).exclude(id=author.id)[:3]
- dept = Department.objects.first()
-
- if not author or len(users) < 2 or not dept:
- print("✗ Недостаточно данных для теста")
- return False
-
- recipient1, recipient2 = users[0], users[1]
- cc_user = users[2] if len(users) > 2 else None
-
- # Данные для создания
- data = {
- 'type': 'VACATION',
- 'title': 'Отпуск с API тестом получателей',
- 'date_from': '2025-12-10',
- 'date_to': '2025-12-20',
- 'comment': 'Тест API',
- 'department_ids': [dept.id],
- 'recipient_ids': [recipient1.id, recipient2.id],
- 'sent_to_all_department': False
- }
-
- if cc_user:
- data['cc_user_ids'] = [cc_user.id]
-
- print(f"\nДанные запроса:")
- print(f" - author: {author.get_full_name() or author.username}")
- print(f" - recipients: {recipient1.get_full_name()}, {recipient2.get_full_name()}")
- if cc_user:
- print(f" - cc: {cc_user.get_full_name()}")
- print(f" - department: {dept.name}")
-
- # Создаем mock request
- factory = RequestFactory()
- request = factory.post('/api/v1/requests/', data, format='json')
- force_authenticate(request, user=author)
-
- # Сериализация
- serializer = RequestWriteSerializer(
- data=data,
- context={'request': request}
- )
-
- if not serializer.is_valid():
- print(f"\n✗ Ошибки валидации: {serializer.errors}")
- return False
-
- # Создание объекта
- request_obj = serializer.save(employee=author)
-
- print(f"\n✓ Заявка создана: #{request_obj.id}")
- print(f" - recipients: {request_obj.recipients.count()}")
- print(f" - cc_users: {request_obj.cc_users.count()}")
- print(f" - departments: {request_obj.departments.count()}")
-
- # Проверяем правильность
- assert request_obj.recipients.count() == 2
- assert request_obj.is_recipient(recipient1)
- assert request_obj.is_recipient(recipient2)
-
- if cc_user:
- assert request_obj.cc_users.count() == 1
- assert request_obj.is_recipient(cc_user)
-
- print("\n✓ Все проверки прошли успешно!")
- return True
-
-
-def test_queryset_addressed_to_me():
- """Тест фильтрации ?addressed_to_me=true"""
- print("\n" + "="*60)
- print(" ТЕСТ: Фильтр ?addressed_to_me=true")
- print("="*60)
-
- # Находим заявку с получателями
- request_obj = Request.objects.filter(
- recipients__isnull=False
- ).distinct().first()
-
- if not request_obj:
- print("⚠ Нет заявок с получателями")
- return False
-
- recipient = request_obj.recipients.first()
- if not recipient:
- print("⚠ Нет получателей")
- return False
-
- print(f"\nЗаявка: #{request_obj.id}")
- print(f"Получатель: {recipient.get_full_name() or recipient.username}")
-
- # Mock request
- factory = RequestFactory()
- django_request = factory.get('/api/v1/requests/?addressed_to_me=true')
- force_authenticate(django_request, user=recipient)
-
- # ViewSet
- viewset = RequestViewSet()
- viewset.request = django_request
- viewset.action = 'list'
- viewset.format_kwarg = None
-
- # Получаем queryset
- qs = viewset.get_queryset()
-
- print(f"\nКоличество заявок для пользователя: {qs.count()}")
-
- # Проверяем, что наша заявка в выборке
- if qs.filter(id=request_obj.id).exists():
- print("✓ Заявка найдена в выборке для получателя")
- return True
- else:
- print("✗ Заявка НЕ найдена в выборке")
- return False
-
-
-def test_update_recipients():
- """Тест обновления получателей"""
- print("\n" + "="*60)
- print(" ТЕСТ: Обновление получателей")
- print("="*60)
-
- request_obj = Request.objects.first()
- if not request_obj:
- print("✗ Нет заявок")
- return False
-
- users = User.objects.filter(is_active=True).exclude(
- id=request_obj.employee_id
- )[:4]
-
- if len(users) < 3:
- print("⚠ Недостаточно пользователей")
- return False
-
- print(f"\nЗаявка: #{request_obj.id}")
- print(f"Было получателей: {request_obj.recipients.count()}")
- print(f"Было CC: {request_obj.cc_users.count()}")
-
- # Новые данные
- new_recipients = [users[0].id, users[1].id, users[2].id]
- new_cc = [users[3].id] if len(users) > 3 else []
-
- data = {
- 'recipient_ids': new_recipients,
- 'cc_user_ids': new_cc,
- }
-
- # Mock request
- factory = RequestFactory()
- django_request = factory.patch(
- f'/api/v1/requests/{request_obj.id}/',
- data,
- format='json'
- )
- force_authenticate(django_request, user=request_obj.employee)
-
- # Сериализация
- serializer = RequestWriteSerializer(
- request_obj,
- data=data,
- partial=True,
- context={'request': django_request}
- )
-
- if not serializer.is_valid():
- print(f"\n✗ Ошибки: {serializer.errors}")
- return False
-
- updated_obj = serializer.save()
-
- print(f"\n✓ Обновлено:")
- print(f" - recipients: {updated_obj.recipients.count()}")
- print(f" - cc_users: {updated_obj.cc_users.count()}")
-
- assert updated_obj.recipients.count() == 3
- if new_cc:
- assert updated_obj.cc_users.count() == 1
-
- print("✓ Все проверки прошли!")
- return True
-
-
-if __name__ == '__main__':
- print("\n" + "="*60)
- print(" 🧪 ТЕСТИРОВАНИЕ API ПОЛУЧАТЕЛЕЙ")
- print("="*60)
-
- tests = [
- ("Создание с получателями", test_create_request_with_recipients),
- ("Фильтр addressed_to_me", test_queryset_addressed_to_me),
- ("Обновление получателей", test_update_recipients),
- ]
-
- results = []
- for name, test_func in tests:
- try:
- result = test_func()
- results.append((name, result))
- except Exception as e:
- print(f"\n✗ ОШИБКА: {type(e).__name__}: {e}")
- import traceback
- traceback.print_exc()
- results.append((name, False))
-
- # Итоги
- print("\n" + "="*60)
- print(" ИТОГИ")
- print("="*60)
-
- for name, result in results:
- status = "✓ PASSED" if result else "✗ FAILED"
- print(f"{status:12} | {name}")
-
- passed = sum(1 for _, r in results if r)
- total = len(results)
- print(f"\nПройдено: {passed}/{total}")
diff --git a/backend/tests/manual/test_register_api.py b/backend/tests/manual/test_register_api.py
deleted file mode 100644
index 6e2aa17c..00000000
--- a/backend/tests/manual/test_register_api.py
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env python
-"""
-Тестовый скрипт для проверки API регистрации.
-Отправляет запросы на /api/v1/auth/register/ и показывает ответы.
-"""
-import json
-import sys
-import os
-import django
-
-# Настройка Django
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eusrr_backend.settings")
-django.setup()
-
-from django.test import Client
-from django.urls import reverse
-
-
-def test_register():
- """Тестируем регистрацию с валидными данными."""
- client = Client()
- url = reverse("api:v1:register")
-
- # Тестовые данные
- payload = {
- "first_name": "Тест",
- "last_name": "Тестов",
- "phone_number": "+79991234567",
- "email": "test@example.com",
- "birth_date": "1990-01-01",
- "password": "password123",
- "telegram": "@test_user",
- "whatsapp": "",
- "wechat": "",
- }
-
- print(f"Отправка запроса на: {url}")
- print(f"Данные: {json.dumps(payload, indent=2, ensure_ascii=False)}")
- print("-" * 60)
-
- response = client.post(
- url,
- data=json.dumps(payload),
- content_type="application/json"
- )
-
- print(f"Статус: {response.status_code}")
- print(f"Content-Type: {response.get('Content-Type')}")
-
- try:
- data = response.json()
- print(f"Ответ: {json.dumps(data, indent=2, ensure_ascii=False)}")
- except Exception as e:
- print(f"Не удалось распарсить JSON: {e}")
- print(f"Тело ответа: {response.content.decode('utf-8')}")
-
- return response
-
-
-def test_register_missing_contact():
- """Тестируем регистрацию без контактов."""
- client = Client()
- url = reverse("api:v1:register")
-
- payload = {
- "first_name": "Тест",
- "last_name": "Тестов",
- "phone_number": "+79991234567",
- "email": "test2@example.com",
- "birth_date": "1990-01-01",
- "password": "password123",
- "telegram": "",
- "whatsapp": "",
- "wechat": "",
- }
-
- print("\n" + "=" * 60)
- print("Тест без контактов:")
- print(f"Данные: {json.dumps(payload, indent=2, ensure_ascii=False)}")
- print("-" * 60)
-
- response = client.post(
- url,
- data=json.dumps(payload),
- content_type="application/json"
- )
-
- print(f"Статус: {response.status_code}")
- try:
- data = response.json()
- print(f"Ответ: {json.dumps(data, indent=2, ensure_ascii=False)}")
- except Exception as e:
- print(f"Не удалось распарсить JSON: {e}")
-
- return response
-
-
-def test_register_invalid_phone():
- """Тестируем регистрацию с неправильным телефоном."""
- client = Client()
- url = reverse("api:v1:register")
-
- payload = {
- "first_name": "Тест",
- "last_name": "Тестов",
- "phone_number": "123", # неправильный формат
- "email": "test3@example.com",
- "birth_date": "1990-01-01",
- "password": "password123",
- "telegram": "@test_user",
- "whatsapp": "",
- "wechat": "",
- }
-
- print("\n" + "=" * 60)
- print("Тест с неправильным телефоном:")
- print(f"Данные: {json.dumps(payload, indent=2, ensure_ascii=False)}")
- print("-" * 60)
-
- response = client.post(
- url,
- data=json.dumps(payload),
- content_type="application/json"
- )
-
- print(f"Статус: {response.status_code}")
- try:
- data = response.json()
- print(f"Ответ: {json.dumps(data, indent=2, ensure_ascii=False)}")
- except Exception as e:
- print(f"Не удалось распарсить JSON: {e}")
-
- return response
-
-
-if __name__ == "__main__":
- print("=" * 60)
- print("ТЕСТ API РЕГИСТРАЦИИ")
- print("=" * 60)
-
- test_register()
- test_register_missing_contact()
- test_register_invalid_phone()
-
- print("\n" + "=" * 60)
- print("Тесты завершены")
- print("=" * 60)
diff --git a/backend/tests/manual/test_reply_update.py b/backend/tests/manual/test_reply_update.py
deleted file mode 100644
index 180ca56f..00000000
--- a/backend/tests/manual/test_reply_update.py
+++ /dev/null
@@ -1,93 +0,0 @@
-"""
-Тест: обновление сообщений при редактировании сообщения, на которое ответили
-"""
-import os
-import django
-
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from communications.models import Message, Chat
-from employees.models import Employee
-from django.contrib.auth import get_user_model
-
-User = get_user_model()
-
-def test_reply_update():
- """Проверяем, что при редактировании сообщения обновляются все ответы на него"""
-
- print("🧪 Тест: обновление reply_to при редактировании")
- print("=" * 60)
-
- # Находим тестовый чат с сообщениями
- chat = Chat.objects.filter(
- type='private'
- ).prefetch_related('messages').first()
-
- if not chat:
- print("❌ Не найдено приватных чатов")
- return
-
- print(f"✓ Чат: {chat}")
-
- # Находим сообщение, на которое есть ответы
- original_msg = Message.objects.filter(
- chat=chat,
- replies__isnull=False # Есть ответы на это сообщение
- ).first()
-
- if not original_msg:
- print("❌ Не найдено сообщение с ответами")
- print("Создадим тестовые сообщения...")
-
- # Создаём тестовые сообщения
- user1 = chat.members.first()
- user2 = chat.members.last()
-
- if user1 == user2 or not user2:
- print("❌ Нужно минимум 2 участника")
- return
-
- # Сообщение 1 (на которое ответят)
- original_msg = Message.objects.create(
- chat=chat,
- author=user1,
- content="Оригинальное сообщение для редактирования"
- )
- print(f"✓ Создано оригинальное сообщение: ID={original_msg.id}")
-
- # Сообщение 2 (ответ на первое)
- reply_msg = Message.objects.create(
- chat=chat,
- author=user2,
- content="Ответ на оригинальное сообщение",
- reply_to=original_msg
- )
- print(f"✓ Создан ответ: ID={reply_msg.id}")
-
- # Находим все ответы на это сообщение
- replies = Message.objects.filter(
- reply_to=original_msg,
- chat=chat
- )
-
- print(f"\n📝 Оригинальное сообщение (ID={original_msg.id}):")
- print(f" Контент: '{original_msg.content}'")
- print(f" Ответов на него: {replies.count()}")
-
- for reply in replies:
- print(f"\n📨 Ответ (ID={reply.id}):")
- print(f" Контент: '{reply.content}'")
- print(f" reply_to.id: {reply.reply_to.id}")
- print(f" reply_to.content: '{reply.reply_to.content}'")
-
- print("\n" + "=" * 60)
- print("✅ Структура готова для теста!")
- print("\nТеперь:")
- print(f"1. Откройте чат ID={chat.id}")
- print(f"2. Отредактируйте сообщение ID={original_msg.id}")
- print(f"3. Проверьте, что сообщения с ID={[r.id for r in replies]} обновились")
- print("4. В reply-preview должен быть новый текст")
-
-if __name__ == '__main__':
- test_reply_update()
diff --git a/backend/tests/manual/test_requests_access.py b/backend/tests/manual/test_requests_access.py
deleted file mode 100644
index 9b807689..00000000
--- a/backend/tests/manual/test_requests_access.py
+++ /dev/null
@@ -1,278 +0,0 @@
-"""
-Тестирование логики доступа к заявлениям (requests)
-
-Проверяет:
-1. Обычные пользователи видят только свои + адресованные им
-2. Руководители отделов видят заявки своих отделов
-3. Пользователи с глобальными правами видят всё
-4. Департаментные права работают корректно
-5. sent_to_all_department работает
-"""
-
-import os
-import sys
-import django
-
-# Настройка Django
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from django.contrib.auth import get_user_model
-from django.db.models import Q
-from employees.models import Department, EmployeeDepartment
-from requests_app.models import Request
-
-User = get_user_model()
-
-
-def print_section(title):
- """Красивый вывод секции"""
- print(f"\n{'=' * 80}")
- print(f" {title}")
- print('=' * 80)
-
-
-def test_user_access(user, description=""):
- """Тестирует доступ пользователя к заявлениям"""
- print(f"\n👤 Пользователь: {user.get_full_name() or user.username}")
- if description:
- print(f" {description}")
-
- # Получаем отделы пользователя
- my_depts = EmployeeDepartment.objects.filter(
- employee=user,
- is_active=True
- ).select_related('department')
-
- print(f"\n📋 Отделы пользователя:")
- if my_depts.exists():
- for ed in my_depts:
- role_name = ed.role.name if ed.role else "Без роли"
- print(f" - {ed.department.name} ({role_name})")
- if ed.role:
- perms = ed.role.scoped_permissions.values_list('code', flat=True)
- if perms:
- print(f" Права: {', '.join(perms)}")
- else:
- print(" (нет отделов)")
-
- # Проверяем глобальные права
- print(f"\n🔑 Глобальные права:")
- print(f" is_staff: {user.is_staff}")
- print(f" can_view_all_requests: {user.has_perm('requests_app.can_view_all_requests')}")
- print(f" view_request: {user.has_perm('requests_app.view_request')}")
- print(f" can_process_requests: {user.has_perm('requests_app.can_process_requests')}")
-
- # Проверяем руководство отделами
- head_depts = Department.objects.filter(head=user)
- if head_depts.exists():
- print(f"\n👔 Руководитель отделов:")
- for dept in head_depts:
- print(f" - {dept.name}")
-
- # Строим queryset как в реальном API
- my_dept_ids = list(my_depts.values_list('department_id', flat=True))
-
- # Проверяем: может ли видеть ВСЁ
- can_view_all = (
- user.is_staff
- or user.has_perm("requests_app.can_view_all_requests")
- or user.has_perm("requests_app.view_request")
- )
-
- if can_view_all:
- print(f"\n✅ Может просматривать ВСЕ заявления (staff или глобальные права)")
- qs = Request.objects.all()
- else:
- # Обычный пользователь - строим scope
- scope = Q(employee_id=user.id) # Свои заявки
-
- # Заявки, где я получатель
- scope |= Q(recipients=user) | Q(cc_users=user)
-
- # Заявки отделов с sent_to_all_department
- if my_dept_ids:
- scope |= Q(
- sent_to_all_department=True,
- departments__in=my_dept_ids
- )
-
- # Департаментные права
- view_dept_ids = list(
- EmployeeDepartment.objects.filter(
- employee_id=user.id,
- is_active=True,
- role__scoped_permissions__code="view_request",
- )
- .values_list("department_id", flat=True)
- .distinct()
- )
- proc_dept_ids = list(
- EmployeeDepartment.objects.filter(
- employee_id=user.id,
- is_active=True,
- role__scoped_permissions__code="can_process_requests",
- )
- .values_list("department_id", flat=True)
- .distinct()
- )
- head_dept_ids = list(
- Department.objects.filter(head_id=user.id).values_list("id", flat=True)
- )
-
- combined_ids = set(view_dept_ids) | set(proc_dept_ids) | set(head_dept_ids)
-
- if combined_ids:
- print(f"\n📊 Департаментные права для отделов (ID): {combined_ids}")
-
- # Заявки этих отделов
- scope |= Q(departments__in=combined_ids)
-
- # Заявки сотрудников этих отделов
- dept_emp_ids = list(
- EmployeeDepartment.objects.filter(
- department_id__in=list(combined_ids),
- is_active=True,
- )
- .values_list("employee_id", flat=True)
- .distinct()
- )
- if dept_emp_ids:
- scope |= Q(employee_id__in=dept_emp_ids)
-
- qs = Request.objects.filter(scope).distinct()
-
- # Подсчитываем результаты
- total = qs.count()
- print(f"\n📈 Доступные заявления: {total}")
-
- if total > 0:
- # Разбивка по типам доступа
- my_requests = qs.filter(employee=user).count()
- addressed_to_me = qs.filter(Q(recipients=user) | Q(cc_users=user)).count()
-
- print(f"\n Детализация:")
- print(f" - Мои заявления: {my_requests}")
- print(f" - Адресованные мне (To/CC): {addressed_to_me}")
-
- if my_dept_ids:
- all_dept = qs.filter(
- sent_to_all_department=True,
- departments__in=my_dept_ids
- ).distinct().count()
- print(f" - 'Всем в отделе': {all_dept}")
-
- # Последние 5 заявлений для примера
- print(f"\n Примеры заявлений (последние 5):")
- for req in qs.select_related('employee').order_by('-created_at')[:5]:
- author = req.employee.get_full_name() or req.employee.username
-
- # Определяем причину доступа
- reasons = []
- if req.employee == user:
- reasons.append("автор")
- if req.recipients.filter(id=user.id).exists():
- reasons.append("получатель")
- if req.cc_users.filter(id=user.id).exists():
- reasons.append("CC")
- if req.sent_to_all_department and any(
- dept.id in my_dept_ids for dept in req.departments.all()
- ):
- reasons.append("всем в отделе")
-
- reason_str = ", ".join(reasons) if reasons else "департаментные права"
-
- print(f" {req.id:4d}. {req.get_type_display():15s} | {author:20s} | {reason_str}")
-
- return qs
-
-
-def main():
- print_section("ТЕСТИРОВАНИЕ ЛОГИКИ ДОСТУПА К ЗАЯВЛЕНИЯМ")
-
- # Получаем тестовых пользователей
- print("\n🔍 Поиск пользователей для тестирования...")
-
- users_to_test = []
-
- # 1. Обычный сотрудник (без особых прав)
- regular_user = User.objects.filter(
- is_staff=False,
- is_superuser=False,
- ).exclude(
- departments_links__role__scoped_permissions__code__in=[
- 'view_request', 'can_process_requests'
- ]
- ).first()
-
- if regular_user:
- users_to_test.append((regular_user, "Обычный сотрудник (без особых прав)"))
-
- # 2. Руководитель отдела
- head_user = Department.objects.exclude(head__isnull=True).first()
- if head_user and head_user.head:
- users_to_test.append((head_user.head, f"Руководитель отдела '{head_user.name}'"))
-
- # 3. С департаментными правами
- dept_rights_user = EmployeeDepartment.objects.filter(
- is_active=True,
- role__scoped_permissions__code__in=['view_request', 'can_process_requests']
- ).select_related('employee').first()
-
- if dept_rights_user:
- users_to_test.append((
- dept_rights_user.employee,
- f"С департаментными правами в отделе '{dept_rights_user.department.name}'"
- ))
-
- # 4. Staff пользователь
- staff_user = User.objects.filter(is_staff=True, is_superuser=False).first()
- if staff_user:
- users_to_test.append((staff_user, "Django staff (без superuser)"))
-
- # 5. Superuser
- superuser = User.objects.filter(is_superuser=True).first()
- if superuser:
- users_to_test.append((superuser, "Superuser"))
-
- if not users_to_test:
- print("\n❌ Не найдено пользователей для тестирования!")
- print(" Создайте пользователей и отделы в админке")
- return
-
- # Информация о заявлениях в системе
- total_requests = Request.objects.count()
- print(f"\n📊 Всего заявлений в системе: {total_requests}")
-
- if total_requests == 0:
- print("\n⚠️ В системе нет заявлений! Создайте хотя бы одну заявку для тестирования.")
- return
-
- # Статистика по получателям
- with_recipients = Request.objects.filter(recipients__isnull=False).distinct().count()
- with_cc = Request.objects.filter(cc_users__isnull=False).distinct().count()
- with_depts = Request.objects.filter(departments__isnull=False).distinct().count()
- with_all_dept = Request.objects.filter(sent_to_all_department=True).count()
-
- print(f"\n Заявлений с получателями (To): {with_recipients}")
- print(f" Заявлений с копией (CC): {with_cc}")
- print(f" Заявлений с отделами: {with_depts}")
- print(f" Заявлений 'Всем в отделе': {with_all_dept}")
-
- # Тестируем каждого пользователя
- for user, description in users_to_test:
- print_section(f"ТЕСТ: {description}")
- test_user_access(user, description)
-
- print_section("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО")
- print("\n✅ Все проверки выполнены!")
- print("\nРекомендации:")
- print("1. Проверьте, что обычные пользователи НЕ видят чужие заявки")
- print("2. Руководители должны видеть заявки своих отделов")
- print("3. Staff видят всё")
- print("4. Получатели (To/CC) видят адресованные им заявки")
-
-
-if __name__ == "__main__":
- main()
diff --git a/backend/tests/manual/test_requests_access_detailed.py b/backend/tests/manual/test_requests_access_detailed.py
deleted file mode 100644
index 9a223cb1..00000000
--- a/backend/tests/manual/test_requests_access_detailed.py
+++ /dev/null
@@ -1,289 +0,0 @@
-"""
-Детальное тестирование API логики доступа к заявлениям
-Проверяет edge cases и специфические сценарии
-"""
-
-import os
-import sys
-import django
-
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
-django.setup()
-
-from django.contrib.auth import get_user_model
-from django.db import models
-from employees.models import Department, EmployeeDepartment
-from requests_app.models import Request
-from requests_app.enums import RequestStatus
-
-User = get_user_model()
-
-
-def test_scenario(title, test_func):
- """Обертка для тестов"""
- print(f"\n{'=' * 80}")
- print(f"🧪 ТЕСТ: {title}")
- print('=' * 80)
- try:
- result = test_func()
- if result:
- print(f"✅ PASSED")
- else:
- print(f"❌ FAILED")
- return result
- except Exception as e:
- print(f"💥 ERROR: {e}")
- import traceback
- traceback.print_exc()
- return False
-
-
-def test_recipient_can_see():
- """Тест: Получатель (To) видит заявку"""
- print("\n1. Создаем пользователя и заявку...")
-
- # Находим пользователя
- user1 = User.objects.filter(is_staff=False, is_superuser=False).first()
- user2 = User.objects.filter(is_staff=False, is_superuser=False).exclude(id=user1.id).first()
-
- if not user1 or not user2:
- print("⚠️ Недостаточно пользователей для теста")
- return None
-
- print(f" Автор: {user1.get_full_name() or user1.username}")
- print(f" Получатель: {user2.get_full_name() or user2.username}")
-
- # Создаем заявку
- request = Request.objects.create(
- employee=user1,
- type='other',
- title='Тест: получатель должен видеть',
- status=RequestStatus.PENDING
- )
- request.recipients.add(user2)
-
- print(f" Создана заявка ID={request.id}")
-
- # Проверяем доступ
- print("\n2. Проверяем доступ получателя...")
- from django.db.models import Q
-
- my_depts = EmployeeDepartment.objects.filter(
- employee=user2, is_active=True
- ).values_list('department_id', flat=True)
-
- scope = Q(employee_id=user2.id) | Q(recipients=user2) | Q(cc_users=user2)
- if my_depts:
- scope |= Q(sent_to_all_department=True, departments__in=my_depts)
-
- accessible = Request.objects.filter(scope).filter(id=request.id).exists()
-
- print(f" Получатель видит заявку: {accessible}")
-
- # Cleanup
- request.delete()
-
- return accessible
-
-
-def test_cc_user_can_see():
- """Тест: Пользователь в CC видит заявку"""
- print("\n1. Создаем пользователей и заявку с CC...")
-
- user1 = User.objects.filter(is_staff=False, is_superuser=False).first()
- user2 = User.objects.filter(is_staff=False, is_superuser=False).exclude(id=user1.id).first()
-
- if not user1 or not user2:
- print("⚠️ Недостаточно пользователей для теста")
- return None
-
- print(f" Автор: {user1.get_full_name() or user1.username}")
- print(f" CC: {user2.get_full_name() or user2.username}")
-
- request = Request.objects.create(
- employee=user1,
- type='other',
- title='Тест: CC должен видеть',
- status=RequestStatus.PENDING
- )
- request.cc_users.add(user2)
-
- print(f" Создана заявка ID={request.id}")
-
- # Проверяем доступ
- print("\n2. Проверяем доступ CC пользователя...")
- from django.db.models import Q
-
- scope = Q(employee_id=user2.id) | Q(recipients=user2) | Q(cc_users=user2)
- accessible = Request.objects.filter(scope).filter(id=request.id).exists()
-
- print(f" CC пользователь видит заявку: {accessible}")
-
- # Cleanup
- request.delete()
-
- return accessible
-
-
-def test_all_department_visibility():
- """Тест: sent_to_all_department работает"""
- print("\n1. Создаем заявку 'Всем в отделе'...")
-
- # Находим отдел с сотрудниками
- dept = Department.objects.annotate(
- emp_count=models.Count('employeedepartment')
- ).filter(emp_count__gte=2).first()
-
- if not dept:
- print("⚠️ Нет отдела с несколькими сотрудниками")
- return None
-
- # Берем двух сотрудников этого отдела
- emp_dept = EmployeeDepartment.objects.filter(
- department=dept,
- is_active=True
- ).select_related('employee')[:2]
-
- if len(emp_dept) < 2:
- print("⚠️ Недостаточно сотрудников в отделе")
- return None
-
- author = emp_dept[0].employee
- viewer = emp_dept[1].employee
-
- print(f" Отдел: {dept.name}")
- print(f" Автор: {author.get_full_name() or author.username}")
- print(f" Сотрудник отдела: {viewer.get_full_name() or viewer.username}")
-
- # Создаем заявку
- request = Request.objects.create(
- employee=author,
- type='other',
- title='Тест: Всем в отделе',
- status=RequestStatus.PENDING,
- sent_to_all_department=True
- )
- request.departments.add(dept)
-
- print(f" Создана заявка ID={request.id}")
-
- # Проверяем доступ
- print("\n2. Проверяем, что другой сотрудник отдела видит заявку...")
- from django.db.models import Q
-
- my_depts = [dept.id]
- scope = Q(employee_id=viewer.id) | Q(recipients=viewer) | Q(cc_users=viewer)
- scope |= Q(sent_to_all_department=True, departments__in=my_depts)
-
- accessible = Request.objects.filter(scope).filter(id=request.id).exists()
-
- print(f" Сотрудник видит заявку: {accessible}")
-
- # Cleanup
- request.delete()
-
- return accessible
-
-
-def test_non_recipient_cannot_see():
- """Тест: Посторонний НЕ видит заявку"""
- print("\n1. Создаем заявку без получателей...")
-
- users = list(User.objects.filter(is_staff=False, is_superuser=False)[:3])
- if len(users) < 3:
- print("⚠️ Недостаточно пользователей")
- return None
-
- author = users[0]
- recipient = users[1]
- outsider = users[2]
-
- print(f" Автор: {author.get_full_name() or author.username}")
- print(f" Получатель: {recipient.get_full_name() or recipient.username}")
- print(f" Посторонний: {outsider.get_full_name() or outsider.username}")
-
- request = Request.objects.create(
- employee=author,
- type='other',
- title='Тест: Посторонний не должен видеть',
- status=RequestStatus.PENDING
- )
- request.recipients.add(recipient)
-
- print(f" Создана заявка ID={request.id}")
-
- # Проверяем, что посторонний НЕ видит
- print("\n2. Проверяем, что посторонний НЕ видит заявку...")
- from django.db.models import Q
-
- my_depts = EmployeeDepartment.objects.filter(
- employee=outsider, is_active=True
- ).values_list('department_id', flat=True)
-
- scope = Q(employee_id=outsider.id) | Q(recipients=outsider) | Q(cc_users=outsider)
- if my_depts:
- scope |= Q(sent_to_all_department=True, departments__in=my_depts)
-
- accessible = Request.objects.filter(scope).filter(id=request.id).exists()
-
- print(f" Посторонний видит заявку: {accessible}")
- print(f" Ожидается: False")
-
- # Cleanup
- request.delete()
-
- return not accessible # Тест проходит, если НЕ видит
-
-
-def main():
- print("\n" + "=" * 80)
- print(" ДЕТАЛЬНОЕ ТЕСТИРОВАНИЕ API ЛОГИКИ ДОСТУПА")
- print("=" * 80)
-
- from django.db import models
-
- results = []
-
- # Тесты
- results.append(test_scenario(
- "Получатель (To) видит заявку",
- test_recipient_can_see
- ))
-
- results.append(test_scenario(
- "Пользователь в CC видит заявку",
- test_cc_user_can_see
- ))
-
- results.append(test_scenario(
- "sent_to_all_department работает",
- test_all_department_visibility
- ))
-
- results.append(test_scenario(
- "Посторонний НЕ видит чужую заявку",
- test_non_recipient_cannot_see
- ))
-
- # Итоги
- print("\n" + "=" * 80)
- print(" ИТОГИ ТЕСТИРОВАНИЯ")
- print("=" * 80)
-
- passed = sum(1 for r in results if r is True)
- failed = sum(1 for r in results if r is False)
- skipped = sum(1 for r in results if r is None)
-
- print(f"\n✅ Пройдено: {passed}")
- print(f"❌ Провалено: {failed}")
- print(f"⏭️ Пропущено: {skipped}")
-
- if failed == 0 and passed > 0:
- print("\n🎉 Все тесты пройдены успешно!")
- elif failed > 0:
- print("\n⚠️ Есть проваленные тесты - требуется исправление!")
-
-
-if __name__ == "__main__":
- main()
diff --git a/backend/tests/middleware/test_email_verification_middleware.py b/backend/tests/middleware/test_email_verification_middleware.py
deleted file mode 100644
index 8b38cfab..00000000
--- a/backend/tests/middleware/test_email_verification_middleware.py
+++ /dev/null
@@ -1,279 +0,0 @@
-"""
-Тесты middleware для проверки верификации email.
-
-Проверяют что неверифицированные пользователи:
-1. Могут получить доступ только к определённым страницам
-2. Редиректятся на страницу верификации
-3. Могут верифицировать свой email
-"""
-
-import pytest
-from django.contrib.auth import get_user_model
-from django.test import Client, RequestFactory
-from django.urls import reverse
-
-from eusrr_backend.middleware import EmailVerificationMiddleware
-
-Employee = get_user_model()
-
-
-def make_user(email, verified=True, **kwargs):
- """Создаёт пользователя для тестов."""
- phone = kwargs.get(
- "phone_number", f"+7900{email[:7].replace('@', '').replace('.', '')}"
- )
- user = Employee.objects.create_user(
- email=email,
- password="TestPass123!",
- first_name=kwargs.get("first_name", "Test"),
- last_name=kwargs.get("last_name", "User"),
- phone_number=phone,
- telegram=kwargs.get("telegram", f"@{email.split('@')[0]}"),
- send_activation_email=False,
- )
- user.email_verified = verified
- user.is_active = True
- user.save(update_fields=["email_verified", "is_active"])
- return user
-
-
-@pytest.mark.django_db
-class TestEmailVerificationMiddleware:
- """Тесты middleware проверки верификации email."""
-
- def test_unverified_user_redirected_to_verification_page(self):
- """Неверифицированный пользователь редиректится на верификацию."""
- client = Client()
- user = make_user("unverified@test.com", verified=False)
-
- # Логинимся
- client.force_login(user)
-
- # Пытаемся зайти на главную
- response = client.get("/")
-
- # Должен быть редирект на страницу верификации
- assert response.status_code == 302
- assert (
- "verify-email" in response.url or "verify" in response.url.lower()
- )
-
- def test_verified_user_can_access_pages(self):
- """Верифицированный пользователь может работать нормально."""
- client = Client()
- user = make_user("verified@test.com", verified=True)
-
- client.force_login(user)
-
- # Можем зайти на главную (или получим 404, но не редирект)
- response = client.get("/")
-
- # Не должно быть редиректа на верификацию
- if response.status_code == 302:
- assert "verify" not in response.url.lower()
-
- def test_unverified_user_can_access_verification_page(self):
- """Неверифицированный пользователь может зайти на верификацию."""
- client = Client()
- user = make_user("unverified@test.com", verified=False)
-
- client.force_login(user)
-
- # Заходим на страницу верификации
- response = client.get(reverse("auth_front:verify_email"))
-
- # Должны попасть на страницу (200 или 405, но не редирект)
- assert response.status_code in [200, 405]
-
- def test_unverified_user_can_access_resend_page(self):
- """Неверифицированный пользователь может запросить код повторно."""
- client = Client()
- user = make_user("unverified@test.com", verified=False)
-
- client.force_login(user)
-
- # Заходим на страницу повторной отправки
- try:
- response = client.get(reverse("auth_front:resend_email"))
- # Должны попасть на страницу
- assert response.status_code in [200, 405]
- except Exception:
- # Если нет такого урла - ничего страшного
- pass
-
- def test_unverified_user_can_access_api_endpoints(self):
- """Неверифицированный пользователь может работать с API."""
- client = Client()
- user = make_user("unverified@test.com", verified=False)
-
- client.force_login(user)
-
- # API должно работать (проверка прав на уровне ViewSet)
- response = client.get("/api/v1/employees/me/")
-
- # Не должно быть редиректа (либо 200, либо 403/401)
- assert response.status_code in [200, 401, 403]
-
- def test_anonymous_user_redirected_to_login(self):
- """Анонимный пользователь редиректится на логин."""
- client = Client()
-
- # Пытаемся зайти на главную без логина
- response = client.get("/")
-
- # Должен быть редирект на логин
- assert response.status_code == 302
- assert (
- "login" in response.url.lower() or response.url == "/auth/login/"
- )
-
- def test_unverified_staff_can_access_admin(self):
- """Неверифицированный staff может зайти в админку."""
- client = Client()
- user = make_user("staff@test.com", verified=False)
- user.is_staff = True
- user.save()
-
- client.force_login(user)
-
- # Заходим в админку
- response = client.get("/admin/")
-
- # Должны попасть в админку (или редирект внутри админки)
- assert response.status_code in [200, 302]
-
- # Не должно быть редиректа на верификацию
- if response.status_code == 302:
- assert "verify" not in response.url.lower()
-
- def test_middleware_allows_static_and_media(self):
- """Middleware пропускает статику и медиа."""
- factory = RequestFactory()
-
- # Создаём middleware
- def dummy_view(request):
- from django.http import HttpResponse
-
- return HttpResponse("OK")
-
- middleware = EmailVerificationMiddleware(dummy_view)
-
- # Тестируем статику
- request = factory.get("/static/css/style.css")
- request.user = make_user("unverified@test.com", verified=False)
-
- response = middleware(request)
-
- # Должен пропустить (вызвать view)
- assert response.status_code == 200
- assert response.content == b"OK"
-
- # Тестируем медиа
- request = factory.get("/media/avatars/user.jpg")
- request.user = make_user("unverified2@test.com", verified=False)
-
- response = middleware(request)
-
- # Должен пропустить
- assert response.status_code == 200
-
-
-@pytest.mark.django_db
-class TestVerificationPageAccess:
- """Тесты доступа к страницам верификации."""
-
- def test_verification_page_exists(self):
- """Страница верификации существует."""
- client = Client()
-
- try:
- url = reverse("auth_front:verify_email")
- response = client.get(url)
-
- # Страница должна существовать
- assert response.status_code in [200, 302, 405]
- except Exception:
- # Если нет урла - тест пропускаем
- pytest.skip("Verification page URL not configured")
-
- def test_resend_page_exists(self):
- """Страница повторной отправки существует."""
- client = Client()
-
- try:
- url = reverse("auth_front:resend_email")
- response = client.get(url)
-
- # Страница должна существовать
- assert response.status_code in [200, 302, 405]
- except Exception:
- # Если нет урла - тест пропускаем
- pytest.skip("Resend page URL not configured")
-
- def test_verified_user_redirected_from_verification_page(self):
- """Верифицированный пользователь редиректится с верификации."""
- client = Client()
- user = make_user("verified@test.com", verified=True)
-
- client.force_login(user)
-
- try:
- url = reverse("auth_front:verify_email")
- response = client.get(url)
-
- # Может быть редирект или просто сообщение
- # Главное - не ошибка
- assert response.status_code in [200, 302]
- except Exception:
- pytest.skip("Verification page URL not configured")
-
-
-@pytest.mark.django_db
-class TestCriticalOperationsRestrictions:
- """
- Тесты что критичные операции заблокированы
- для неверифицированных пользователей.
- """
-
- def test_unverified_cannot_create_department_via_web(self):
- """Неверифицированный не может создать отдел через веб."""
- client = Client()
- user = make_user("staff@test.com", verified=False)
- user.is_staff = True
- user.save()
-
- client.force_login(user)
-
- # Пытаемся создать отдел
- response = client.post(
- "/api/v1/departments/",
- {"name": "Test Department"},
- content_type="application/json",
- )
-
- # Должен быть запрещён (или редирект на верификацию)
- assert response.status_code in [302, 403]
-
- if response.status_code == 302:
- assert "verify" in response.url.lower()
-
- def test_unverified_cannot_modify_other_users(self):
- """Неверифицированный staff не может менять других."""
- client = Client()
- staff = make_user("staff@test.com", verified=False)
- staff.is_staff = True
- staff.save()
-
- other_user = make_user("other@test.com", verified=True)
-
- client.force_login(staff)
-
- # Пытаемся изменить другого пользователя
- response = client.patch(
- f"/api/v1/employees/{other_user.id}/",
- {"first_name": "Hacked"},
- content_type="application/json",
- )
-
- # Должен быть запрещён
- assert response.status_code in [302, 403]
diff --git a/backend/tests/notifications/__init__.py b/backend/tests/notifications/__init__.py
deleted file mode 100644
index 62a4befe..00000000
--- a/backend/tests/notifications/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Notifications tests package
diff --git a/backend/tests/notifications/test_api.py b/backend/tests/notifications/test_api.py
deleted file mode 100644
index bfe2b4ea..00000000
--- a/backend/tests/notifications/test_api.py
+++ /dev/null
@@ -1,274 +0,0 @@
-"""
-Тесты для API endpoints уведомлений
-"""
-from django.test import TestCase
-from django.contrib.auth import get_user_model
-from rest_framework.test import APIClient
-from rest_framework import status
-from notifications.models import (
- NotificationCategory,
- NotificationType,
- Notification,
- WebPushSubscription,
-)
-
-User = get_user_model()
-
-
-def create_test_user(email='test@example.com', phone='+79990000001'):
- """Helper для создания тестового пользователя с обязательным phone_number"""
- return User.objects.create_user(
- email=email,
- phone_number=phone,
- password='testpass123',
- first_name='Test',
- last_name='User',
- is_active=True
- )
-
-
-class NotificationAPITest(TestCase):
- """Тесты API уведомлений"""
-
- def setUp(self):
- self.client = APIClient()
- self.user = create_test_user()
- self.client.force_authenticate(user=self.user)
-
- # Создаем категорию и тип
- self.category = NotificationCategory.objects.create(
- code='test_category',
- name='Test Category'
- )
- self.notification_type = NotificationType.objects.create(
- category=self.category,
- code='test_type',
- name='Test Type'
- )
-
- # Создаем тестовые уведомления
- for i in range(5):
- Notification.objects.create(
- recipient=self.user,
- notification_type=self.notification_type,
- title=f'Notification {i+1}',
- message=f'Message {i+1}',
- short_message=f'Short {i+1}'
- )
-
- def test_get_notifications(self):
- """Получение списка уведомлений"""
- response = self.client.get('/api/v1/notifications/')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['total'], 5)
- self.assertEqual(len(response.data['notifications']), 5)
-
- def test_get_notifications_with_pagination(self):
- """Пагинация"""
- response = self.client.get('/api/v1/notifications/?page=1&page_size=2')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['total'], 5)
- self.assertEqual(len(response.data['notifications']), 2)
- self.assertEqual(response.data['page'], 1)
-
- def test_get_unread_notifications(self):
- """Фильтр непрочитанных"""
- response = self.client.get('/api/v1/notifications/?unread_only=true')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['total'], 5) # Все непрочитанные
-
- def test_get_unread_count(self):
- """Получение количества непрочитанных"""
- response = self.client.get('/api/v1/notifications/count/')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 5)
-
- def test_mark_as_read(self):
- """Отметка уведомления как прочитанное"""
- notification = Notification.objects.filter(recipient=self.user).first()
- response = self.client.post(f'/api/v1/notifications/{notification.id}/read/')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- notification.refresh_from_db()
- self.assertTrue(notification.is_read)
-
- def test_mark_all_as_read(self):
- """Отметка всех уведомлений как прочитанные"""
- response = self.client.post('/api/v1/notifications/read-all/')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- unread_count = Notification.objects.filter(
- recipient=self.user,
- is_read=False
- ).count()
- self.assertEqual(unread_count, 0)
-
- def test_delete_notification(self):
- """Удаление уведомления"""
- notification = Notification.objects.filter(recipient=self.user).first()
- response = self.client.delete(f'/api/v1/notifications/{notification.id}/')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- # Проверяем что уведомление заархивировано, а не удалено
- notification.refresh_from_db()
- self.assertTrue(notification.is_archived)
-
- def test_search_notifications(self):
- """Поиск уведомлений"""
- response = self.client.get('/api/v1/notifications/?search=Notification 3')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['total'], 1)
- self.assertEqual(response.data['notifications'][0]['title'], 'Notification 3')
-
- def test_unauthorized_access(self):
- """Доступ без авторизации"""
- self.client.force_authenticate(user=None)
- response = self.client.get('/api/v1/notifications/')
-
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-
-class WebPushAPITest(TestCase):
- """Тесты Web Push API"""
-
- def setUp(self):
- self.client = APIClient()
- self.user = create_test_user()
- self.client.force_authenticate(user=self.user)
-
- def test_get_vapid_public_key(self):
- """Получение VAPID публичного ключа"""
- response = self.client.get('/api/v1/notifications/push/vapid-key/')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('vapid_public_key', response.data)
-
- def test_subscribe_push(self):
- """Подписка на push-уведомления"""
- data = {
- 'endpoint': 'https://fcm.googleapis.com/fcm/send/test123',
- 'keys': {
- 'p256dh': 'test_p256dh_key',
- 'auth': 'test_auth_key'
- },
- 'device_name': 'Test Browser'
- }
- response = self.client.post('/api/v1/notifications/push/subscribe/', data, format='json')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['status'], 'success')
-
- # Проверяем что подписка создана
- subscription = WebPushSubscription.objects.get(user=self.user)
- self.assertEqual(subscription.endpoint, data['endpoint'])
- self.assertEqual(subscription.device_name, 'Test Browser')
-
- def test_subscribe_push_missing_fields(self):
- """Подписка с отсутствующими полями"""
- data = {
- 'endpoint': 'https://fcm.googleapis.com/fcm/send/test123'
- # Нет keys
- }
- response = self.client.post('/api/v1/notifications/push/subscribe/', data, format='json')
-
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_update_existing_subscription(self):
- """Обновление существующей подписки"""
- # Создаем первую подписку
- data1 = {
- 'endpoint': 'https://fcm.googleapis.com/fcm/send/test123',
- 'keys': {
- 'p256dh': 'old_key',
- 'auth': 'old_auth'
- },
- 'device_name': 'Old Browser'
- }
- self.client.post('/api/v1/notifications/push/subscribe/', data1, format='json')
-
- # Обновляем подписку с новыми ключами
- data2 = {
- 'endpoint': 'https://fcm.googleapis.com/fcm/send/test123',
- 'keys': {
- 'p256dh': 'new_key',
- 'auth': 'new_auth'
- },
- 'device_name': 'New Browser'
- }
- response = self.client.post('/api/v1/notifications/push/subscribe/', data2, format='json')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertFalse(response.data['created']) # Обновлена, не создана
-
- # Проверяем обновление
- subscription = WebPushSubscription.objects.get(user=self.user)
- self.assertEqual(subscription.p256dh_key, 'new_key')
- self.assertEqual(subscription.device_name, 'New Browser')
-
- def test_unsubscribe_push(self):
- """Отписка от push-уведомлений"""
- # Создаем подписку
- subscription = WebPushSubscription.objects.create(
- user=self.user,
- endpoint='https://fcm.googleapis.com/fcm/send/test123',
- p256dh_key='test_key',
- auth_key='test_auth'
- )
-
- # Отписываемся
- data = {'endpoint': subscription.endpoint}
- response = self.client.delete('/api/v1/notifications/push/unsubscribe/', data, format='json')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- # Проверяем что подписка удалена
- self.assertFalse(
- WebPushSubscription.objects.filter(id=subscription.id).exists()
- )
-
- def test_unsubscribe_all_push(self):
- """Отписка от всех подписок"""
- # Создаем несколько подписок
- for i in range(3):
- WebPushSubscription.objects.create(
- user=self.user,
- endpoint=f'https://fcm.googleapis.com/fcm/send/test{i}',
- p256dh_key=f'test_key_{i}',
- auth_key=f'test_auth_{i}'
- )
-
- # Отписываемся от всех
- response = self.client.delete('/api/v1/notifications/push/unsubscribe/', {}, format='json')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- # Проверяем что все подписки удалены
- self.assertEqual(
- WebPushSubscription.objects.filter(user=self.user).count(),
- 0
- )
-
- def test_get_subscriptions(self):
- """Получение списка подписок"""
- # Создаем подписки
- for i in range(3):
- WebPushSubscription.objects.create(
- user=self.user,
- endpoint=f'https://fcm.googleapis.com/fcm/send/test{i}',
- p256dh_key=f'test_key_{i}',
- auth_key=f'test_auth_{i}',
- device_name=f'Device {i}'
- )
-
- response = self.client.get('/api/v1/notifications/push/subscriptions/')
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data['subscriptions']), 3)
diff --git a/backend/tests/notifications/test_models.py b/backend/tests/notifications/test_models.py
deleted file mode 100644
index 23dc7b7a..00000000
--- a/backend/tests/notifications/test_models.py
+++ /dev/null
@@ -1,224 +0,0 @@
-"""
-Тесты для моделей уведомлений
-"""
-from django.test import TestCase
-from django.contrib.auth import get_user_model
-from notifications.models import (
- NotificationCategory,
- NotificationType,
- Notification,
- UserNotificationSettings,
- WebPushSubscription,
-)
-
-User = get_user_model()
-
-
-def create_test_user(email='test@example.com', phone='+79990000001'):
- """Helper для создания тестового пользователя с обязательным phone_number"""
- return User.objects.create_user(
- email=email,
- phone_number=phone,
- password='testpass123',
- first_name='Test',
- last_name='User',
- is_active=True
- )
-
-
-class NotificationCategoryTest(TestCase):
- """Тесты категорий уведомлений"""
-
- def setUp(self):
- self.category = NotificationCategory.objects.create(
- code='test_category',
- name='Test Category',
- icon='bi-test',
- color='primary'
- )
-
- def test_category_creation(self):
- """Создание категории"""
- self.assertEqual(self.category.code, 'test_category')
- self.assertEqual(self.category.name, 'Test Category')
- self.assertTrue(self.category.is_active)
-
- def test_category_str(self):
- """Строковое представление"""
- self.assertEqual(str(self.category), 'Test Category')
-
-
-class NotificationTypeTest(TestCase):
- """Тесты типов уведомлений"""
-
- def setUp(self):
- self.category = NotificationCategory.objects.create(
- code='test_category',
- name='Test Category'
- )
- self.notification_type = NotificationType.objects.create(
- category=self.category,
- code='test_type',
- name='Test Type',
- default_enabled=True,
- priority='normal'
- )
-
- def test_type_creation(self):
- """Создание типа уведомления"""
- self.assertEqual(self.notification_type.code, 'test_type')
- self.assertEqual(self.notification_type.category, self.category)
- self.assertTrue(self.notification_type.default_enabled)
- self.assertEqual(self.notification_type.priority, 'normal')
-
- def test_type_str(self):
- """Строковое представление"""
- self.assertEqual(str(self.notification_type), 'Test Category: Test Type')
-
-
-class NotificationTest(TestCase):
- """Тесты уведомлений"""
-
- def setUp(self):
- self.user = create_test_user()
- self.category = NotificationCategory.objects.create(
- code='test_category',
- name='Test Category'
- )
- self.notification_type = NotificationType.objects.create(
- category=self.category,
- code='test_type',
- name='Test Type'
- )
- self.notification = Notification.objects.create(
- recipient=self.user,
- notification_type=self.notification_type,
- title='Test Notification',
- message='Test message',
- short_message='Test'
- )
-
- def test_notification_creation(self):
- """Создание уведомления"""
- self.assertEqual(self.notification.recipient, self.user)
- self.assertEqual(self.notification.title, 'Test Notification')
- self.assertFalse(self.notification.is_read)
- self.assertFalse(self.notification.is_archived)
-
- def test_mark_as_read(self):
- """Отметка как прочитанное"""
- self.assertFalse(self.notification.is_read)
- self.assertIsNone(self.notification.read_at)
-
- self.notification.mark_as_read()
-
- self.assertTrue(self.notification.is_read)
- self.assertIsNotNone(self.notification.read_at)
-
- def test_archive(self):
- """Архивирование"""
- self.assertFalse(self.notification.is_archived)
- self.assertIsNone(self.notification.archived_at)
-
- self.notification.archive()
-
- self.assertTrue(self.notification.is_archived)
- self.assertIsNotNone(self.notification.archived_at)
-
-
-class UserNotificationSettingsTest(TestCase):
- """Тесты настроек уведомлений пользователя"""
-
- def setUp(self):
- self.user = create_test_user()
- self.category = NotificationCategory.objects.create(
- code='test_category',
- name='Test Category'
- )
- self.notification_type = NotificationType.objects.create(
- category=self.category,
- code='test_type',
- name='Test Type'
- )
- self.settings = UserNotificationSettings.objects.create(
- user=self.user,
- notification_type=self.notification_type,
- is_enabled=True,
- send_web=True,
- send_email=False
- )
-
- def test_settings_creation(self):
- """Создание настроек"""
- self.assertEqual(self.settings.user, self.user)
- self.assertTrue(self.settings.is_enabled)
- self.assertTrue(self.settings.send_web)
- self.assertFalse(self.settings.send_email)
-
-
-class WebPushSubscriptionTest(TestCase):
- """Тесты Web Push подписок"""
-
- def setUp(self):
- self.user = create_test_user()
- self.subscription = WebPushSubscription.objects.create(
- user=self.user,
- endpoint='https://fcm.googleapis.com/fcm/send/test123',
- p256dh_key='test_p256dh_key',
- auth_key='test_auth_key',
- device_name='Test Device'
- )
-
- def test_subscription_creation(self):
- """Создание подписки"""
- self.assertEqual(self.subscription.user, self.user)
- self.assertEqual(self.subscription.device_name, 'Test Device')
- self.assertTrue(self.subscription.is_active)
- self.assertEqual(self.subscription.error_count, 0)
-
- def test_mark_used(self):
- """Обновление времени использования"""
- self.assertIsNone(self.subscription.last_used_at)
-
- self.subscription.mark_used()
-
- self.assertIsNotNone(self.subscription.last_used_at)
-
- def test_increment_error(self):
- """Инкремент ошибок"""
- self.assertEqual(self.subscription.error_count, 0)
-
- self.subscription.increment_error('Test error')
-
- self.assertEqual(self.subscription.error_count, 1)
- self.assertEqual(self.subscription.last_error, 'Test error')
- self.assertTrue(self.subscription.is_active)
-
- def test_auto_deactivate_after_5_errors(self):
- """Автоматическая деактивация после 5 ошибок"""
- for i in range(5):
- self.subscription.increment_error(f'Error {i+1}')
-
- self.subscription.refresh_from_db()
- self.assertFalse(self.subscription.is_active)
- self.assertEqual(self.subscription.error_count, 5)
-
- def test_reset_errors(self):
- """Сброс ошибок"""
- self.subscription.increment_error('Test error')
- self.assertEqual(self.subscription.error_count, 1)
-
- self.subscription.reset_errors()
-
- self.assertEqual(self.subscription.error_count, 0)
- self.assertEqual(self.subscription.last_error, '')
-
- def test_unique_together(self):
- """Уникальность user + endpoint"""
- with self.assertRaises(Exception):
- WebPushSubscription.objects.create(
- user=self.user,
- endpoint='https://fcm.googleapis.com/fcm/send/test123',
- p256dh_key='another_key',
- auth_key='another_auth'
- )
diff --git a/backend/tests/notifications/test_services.py b/backend/tests/notifications/test_services.py
deleted file mode 100644
index e26ae710..00000000
--- a/backend/tests/notifications/test_services.py
+++ /dev/null
@@ -1,141 +0,0 @@
-"""
-Тесты для NotificationService
-"""
-from django.test import TestCase
-from django.contrib.auth import get_user_model
-from notifications.models import (
- NotificationCategory,
- NotificationType,
- Notification,
- UserNotificationSettings,
-)
-from notifications.services import NotificationService
-
-User = get_user_model()
-
-
-def create_test_user(email='test@example.com', phone='+79990000001'):
- """Helper для создания тестового пользователя с обязательным phone_number"""
- return User.objects.create_user(
- email=email,
- phone_number=phone,
- password='testpass123',
- first_name='Test',
- last_name='User',
- is_active=True
- )
-
-
-class NotificationServiceTest(TestCase):
- """Тесты сервиса уведомлений"""
-
- def setUp(self):
- self.user = create_test_user()
-
- self.category = NotificationCategory.objects.create(
- code='test_category',
- name='Test Category'
- )
-
- self.notification_type = NotificationType.objects.create(
- category=self.category,
- code='test_type',
- name='Test Type',
- default_enabled=True
- )
-
- def test_create_notification(self):
- """Создание уведомления через сервис"""
- notification = NotificationService.create_notification(
- recipient=self.user,
- notification_type_code='test_type',
- title='Test Title',
- message='Test Message',
- action_url='/test/',
- send_immediately=False
- )
-
- self.assertIsNotNone(notification)
- self.assertEqual(notification.recipient, self.user)
- self.assertEqual(notification.title, 'Test Title')
- self.assertEqual(notification.message, 'Test Message')
- self.assertEqual(notification.action_url, '/test/')
-
- def test_create_notification_invalid_type(self):
- """Создание с несуществующим типом"""
- notification = NotificationService.create_notification(
- recipient=self.user,
- notification_type_code='invalid_type',
- title='Test',
- message='Test',
- send_immediately=False
- )
-
- self.assertIsNone(notification)
-
- def test_get_user_settings_creates_if_not_exists(self):
- """Автоматическое создание настроек"""
- settings = NotificationService.get_user_settings(
- self.user,
- self.notification_type
- )
-
- self.assertIsNotNone(settings)
- self.assertEqual(settings.user, self.user)
- self.assertEqual(settings.notification_type, self.notification_type)
- self.assertTrue(settings.is_enabled)
-
- def test_mark_as_read(self):
- """Отметка как прочитанное через сервис"""
- notification = Notification.objects.create(
- recipient=self.user,
- notification_type=self.notification_type,
- title='Test',
- message='Test'
- )
-
- success = NotificationService.mark_as_read(notification.id, self.user)
-
- self.assertTrue(success)
- notification.refresh_from_db()
- self.assertTrue(notification.is_read)
-
- def test_mark_as_read_wrong_user(self):
- """Попытка прочитать чужое уведомление"""
- other_user = create_test_user(
- email='other@example.com',
- phone='+79990000002'
- )
-
- notification = Notification.objects.create(
- recipient=other_user,
- notification_type=self.notification_type,
- title='Test',
- message='Test'
- )
-
- success = NotificationService.mark_as_read(notification.id, self.user)
-
- self.assertFalse(success)
- notification.refresh_from_db()
- self.assertFalse(notification.is_read)
-
- def test_mark_all_as_read(self):
- """Отметка всех как прочитанные"""
- # Создаем несколько уведомлений
- for i in range(5):
- Notification.objects.create(
- recipient=self.user,
- notification_type=self.notification_type,
- title=f'Test {i}',
- message=f'Message {i}'
- )
-
- count = NotificationService.mark_all_as_read(self.user)
-
- self.assertEqual(count, 5)
- unread = Notification.objects.filter(
- recipient=self.user,
- is_read=False
- ).count()
- self.assertEqual(unread, 0)
diff --git a/backend/tests/requests_app/conftest.py b/backend/tests/requests_app/conftest.py
deleted file mode 100644
index a8b975af..00000000
--- a/backend/tests/requests_app/conftest.py
+++ /dev/null
@@ -1,175 +0,0 @@
-import pytest
-from django.contrib.auth import get_user_model
-from django.core.cache import cache
-from django.conf import settings
-
-pytestmark = pytest.mark.django_db
-
-
-@pytest.fixture(autouse=True)
-def _clear_cache():
- cache.clear()
- yield
- cache.clear()
-
-# На всякий: bakery сможет печь PhoneNumberField, если вдруг где-то всплывёт Employee
-@pytest.fixture(autouse=True)
-def _bakery_phone_generator():
- try:
- from model_bakery import generators
- from phonenumber_field.modelfields import PhoneNumberField
- import itertools
- counter = itertools.count(10000)
- generators.add(
- PhoneNumberField,
- lambda: f"+7999{next(counter):07d}"
- )
- except Exception:
- pass
- yield
-
-def _mk_user(phone: str, **extra):
- User = get_user_model()
- defaults = dict(
- phone_number=phone,
- password=extra.pop("password", "pass"),
- email=extra.pop("email", f"{phone}@example.com"),
- first_name=extra.pop("first_name", "Test"),
- last_name=extra.pop("last_name", "User"),
- telegram=extra.pop("telegram", "@test"),
- is_active=True, # <<< ВАЖНО: активируем пользователя для тестов
- **extra,
- )
- return User.objects.create_user(**defaults)
-
-
-@pytest.fixture
-def user():
- return _mk_user("+79990000001")
-
-@pytest.fixture
-def hr_user():
- return _mk_user("+79990000002", is_staff=True)
-
-@pytest.fixture
-def other_user():
- return _mk_user("+79990000003")
-
-@pytest.fixture
-def login(client):
- def _do(u):
- client.force_login(u, backend=settings.AUTHENTICATION_BACKENDS[0])
- return client
- return _do
-
-@pytest.fixture
-def request_obj(user):
- from model_bakery import baker
- from requests_app.models import Request
- return baker.make(
- Request,
- employee=user,
- status=Request.STATUS_PENDING,
- type=Request.TYPE_OTHER,
- )
-
-
-# ============================================================================
-# Fixtures для тестирования новых фич (recipients/cc/departments)
-# ============================================================================
-
-@pytest.fixture
-def test_employee(user_factory):
- """Первый тестовый сотрудник"""
- return user_factory(email='emp1@test.com')
-
-
-@pytest.fixture
-def test_employee_second(user_factory):
- """Второй тестовый сотрудник"""
- return user_factory(email='emp2@test.com')
-
-
-@pytest.fixture
-def test_employee_third(user_factory):
- """Третий тестовый сотрудник"""
- return user_factory(email='emp3@test.com')
-
-
-@pytest.fixture
-def test_department(department_factory):
- """Тестовый отдел"""
- return department_factory(name='IT Department')
-
-
-@pytest.fixture
-def test_department_second(department_factory):
- """Второй тестовый отдел"""
- return department_factory(name='HR Department')
-
-
-@pytest.fixture
-def test_staff_user(user_factory):
- """Staff пользователь"""
- return user_factory(email='staff@test.com', staff=True)
-
-
-@pytest.fixture
-def create_request_helper(db):
- """Хелпер для создания заявки с получателями"""
- from requests_app.models import Request
- from requests_app.enums import RequestStatus, RequestType
-
- def _create(employee, recipients=None, cc_users=None, departments=None,
- sent_to_all=False, **kwargs):
- defaults = {
- 'type': RequestType.OTHER,
- 'title': 'Test request',
- 'status': RequestStatus.PENDING
- }
- defaults.update(kwargs)
-
- request = Request.objects.create(
- employee=employee,
- sent_to_all_department=sent_to_all,
- **defaults
- )
-
- if recipients:
- if not isinstance(recipients, list):
- recipients = [recipients]
- request.recipients.set(recipients)
- if cc_users:
- if not isinstance(cc_users, list):
- cc_users = [cc_users]
- request.cc_users.set(cc_users)
- if departments:
- if not isinstance(departments, list):
- departments = [departments]
- request.departments.set(departments)
-
- return request
-
- return _create
-
-
-@pytest.fixture
-def global_viewer(user_factory):
- """Пользователь с глобальным правом can_view_all_requests"""
- from django.contrib.auth.models import Permission
- from django.contrib.contenttypes.models import ContentType
- from requests_app.models import Request
-
- user = user_factory(email='global@test.com')
-
- # Получаем или создаем permission
- ct = ContentType.objects.get_for_model(Request)
- perm, _ = Permission.objects.get_or_create(
- content_type=ct,
- codename='can_view_all_requests',
- defaults={'name': 'Можно просмотреть все заявления'}
- )
-
- user.user_permissions.add(perm)
- return user
-
diff --git a/backend/tests/requests_app/test_filters_basic.py b/backend/tests/requests_app/test_filters_basic.py
deleted file mode 100644
index aa226d30..00000000
--- a/backend/tests/requests_app/test_filters_basic.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import pytest
-from django.urls import reverse
-from model_bakery import baker
-from datetime import timedelta
-from django.utils import timezone
-
-pytestmark = pytest.mark.django_db
-
-
-def test_my_requests_filters_by_date_status_type(client, user, hr_user, login):
- from requests_app.models import Request
- from requests_app.enums import RequestStatus, RequestType
-
- login(user)
-
- now = timezone.now()
- r1 = baker.make(
- Request,
- employee=user,
- status=RequestStatus.PENDING,
- type=RequestType.VACATION,
- created_at=now - timedelta(days=10),
- )
- r2 = baker.make(
- Request,
- employee=user,
- status=RequestStatus.PENDING, # сначала pending
- type=RequestType.SICK_LEAVE,
- created_at=now - timedelta(days=2),
- )
- # корректно апрувим (заполнит approver и decided_at)
- r2.approve(by_user=hr_user)
-
- url = reverse("requests:request_list")
- params = {
- "status": RequestStatus.APPROVED,
- "type": RequestType.SICK_LEAVE,
- "from": (now - timedelta(days=5)).date().isoformat(),
- "to": now.date().isoformat(),
- }
- resp = client.get(url, params)
- assert resp.status_code == 200
- page = resp.context["page"]
- objs = list(page.object_list)
- assert {o.pk for o in objs} == {r2.pk}
diff --git a/backend/tests/requests_app/test_forms.py b/backend/tests/requests_app/test_forms.py
deleted file mode 100644
index 5a44edaa..00000000
--- a/backend/tests/requests_app/test_forms.py
+++ /dev/null
@@ -1,195 +0,0 @@
-
-import pytest
-from django.core.files.uploadedfile import SimpleUploadedFile
-
-pytestmark = pytest.mark.django_db
-
-
-def _file(name="f.pdf", content=b"x", content_type="application/pdf"):
- return SimpleUploadedFile(name=name, content=content, content_type=content_type)
-
-
-# -------------------- RequestForm --------------------
-
-
-def test_request_form_valid_minimal():
- from requests_app.forms import RequestForm
- from requests_app.models import Request
-
- form = RequestForm(data={"type": Request.TYPE_OTHER, "comment": "ok"})
- assert form.is_valid()
-
-
-def test_request_form_date_reverse_error():
- from requests_app.forms import RequestForm
-
- form = RequestForm(
- data={"type": "other", "date_from": "2025-01-10", "date_to": "2025-01-01"}
- )
- assert not form.is_valid()
- assert "date_to" in form.errors
-
-
-def test_request_form_attachment_magic_missing_requires_both_ext_and_ct(monkeypatch):
- """
- Когда python-magic недоступен (_sniff_mime -> None), нужен И корректный content_type,
- И корректное расширение.
- """
- from requests_app import forms as F
-
- # эмулируем отсутствие python-magic
- monkeypatch.setattr(F, "_sniff_mime", lambda f: None)
-
- # валидный случай: jpeg + .jpg
- form_ok = F.RequestForm(
- data={"type": "other"},
- files={
- "attachment": _file(
- name="pic.jpg", content=b"abc", content_type="image/jpeg"
- )
- },
- )
- assert form_ok.is_valid()
-
- # невалидный: image/jpeg, но расширение .exe
- form_bad = F.RequestForm(
- data={"type": "other"},
- files={
- "attachment": _file(name="a.exe", content=b"abc", content_type="image/jpeg")
- },
- )
- assert not form_bad.is_valid()
- assert "attachment" in form_bad.errors
-
-
-def test_request_form_attachment_magic_detects_mime_and_ext(monkeypatch):
- """
- Если mime определён (эмулируем через _sniff_mime), он должен быть из whitelist,
- и расширение тоже из whitelist.
- """
- from requests_app import forms as F
-
- # определяем как PDF — ок только с .pdf
- monkeypatch.setattr(F, "_sniff_mime", lambda f: "application/pdf")
-
- # валидный
- form_ok = F.RequestForm(
- data={"type": "other"},
- files={
- "attachment": _file(
- name="file.pdf",
- content=b"%PDF-1.4",
- content_type="application/octet-stream",
- )
- },
- )
- assert form_ok.is_valid()
-
- # неверное расширение при корректном mime
- form_bad = F.RequestForm(
- data={"type": "other"},
- files={
- "attachment": _file(
- name="file.png",
- content=b"%PDF-1.4",
- content_type="application/octet-stream",
- )
- },
- )
- assert not form_bad.is_valid()
- assert "attachment" in form_bad.errors
-
-
-def test_request_form_attachment_size_limit(monkeypatch):
- from requests_app import forms as F
-
- # уменьшаем порог, чтобы не плодить большие буферы
- monkeypatch.setattr(F, "MAX_ATTACHMENT_SIZE", 5)
- big = SimpleUploadedFile("a.pdf", b"123456", content_type="application/pdf")
- form = F.RequestForm(data={"type": "other"}, files={"attachment": big})
- assert not form.is_valid()
- assert "attachment" in form.errors
-
-
-# -------------------- RequestStatusForm --------------------
-
-
-def test_status_form_requires_user_kwarg():
- from requests_app.forms import RequestStatusForm
- from requests_app.models import Request
-
- with pytest.raises(ValueError):
- RequestStatusForm(
- data={"status": Request.STATUS_APPROVED}, instance=Request(), user=None
- )
-
-
-def test_status_form_allowed_statuses_by_role(user, hr_user):
- from requests_app.forms import RequestStatusForm
- from requests_app.models import Request
-
- # non-staff: только approved/rejected
- form_user = RequestStatusForm(user=user, instance=Request())
- values_user = {v for v, _ in form_user.fields["status"].choices}
- assert values_user == {Request.STATUS_APPROVED, Request.STATUS_REJECTED}
-
- # staff/HR: плюс cancelled
- form_hr = RequestStatusForm(user=hr_user, instance=Request())
- values_hr = {v for v, _ in form_hr.fields["status"].choices}
- assert values_hr == {
- Request.STATUS_APPROVED,
- Request.STATUS_REJECTED,
- Request.STATUS_CANCELLED,
- }
-
-
-def test_status_form_clean_status_disallowed_for_role_raises(user):
- from requests_app.forms import RequestStatusForm
- from requests_app.models import Request
-
- form = RequestStatusForm(
- data={"status": Request.STATUS_CANCELLED}, user=user, instance=Request()
- )
- assert not form.is_valid()
- assert "status" in form.errors
-
-
-def test_status_form_cannot_change_final_status(hr_user):
- from requests_app.forms import RequestStatusForm
- from requests_app.models import Request
-
- # финальная заявка
- r = Request(status=Request.STATUS_APPROVED)
- form = RequestStatusForm(
- data={"status": Request.STATUS_REJECTED}, user=hr_user, instance=r
- )
- assert not form.is_valid()
- assert "status" in form.errors
-
-
-def test_status_form_clean_attachment_uses_common_validator(monkeypatch, hr_user):
- from requests_app import forms as F
- from requests_app.models import Request
-
- called = {"ok": False}
-
- def _spy(self, f): # signature совпадает с методом
- called["ok"] = True
-
- monkeypatch.setattr(
- F.RequestForm, "_validate_attachment_common", _spy, raising=True
- )
-
- form = F.RequestStatusForm(
- data={"status": Request.STATUS_APPROVED},
- files={
- "attachment": _file(
- name="x.pdf", content=b"pdf", content_type="application/pdf"
- )
- },
- user=hr_user,
- instance=Request(),
- )
- # валидность не важна — нам важно, что вызвался общий валидатор
- form.is_valid()
- assert called["ok"] is True
diff --git a/backend/tests/requests_app/test_list_requests.py b/backend/tests/requests_app/test_list_requests.py
deleted file mode 100644
index b78741dc..00000000
--- a/backend/tests/requests_app/test_list_requests.py
+++ /dev/null
@@ -1,312 +0,0 @@
-"""
-Тесты API: GET /api/v1/requests/ - список заявок
-Проверяет логику доступа для различных типов пользователей
-"""
-import pytest
-
-from requests_app.models import Request
-
-
-pytestmark = pytest.mark.django_db
-
-
-class TestListRequestsRegularUser:
- """Тесты для обычных пользователей без прав"""
-
- url = '/api/v1/requests/'
-
- def test_can_see_own_requests(self, auth_client_factory, test_employee):
- """1.1.1: Обычный пользователь видит свои заявки"""
- # Создаем заявку
- request = Request.objects.create(
- employee=test_employee,
- type='other',
- title='Моя заявка'
- )
-
- client = auth_client_factory(test_employee)
- response = client.get(self.url)
-
- assert response.status_code == 200
- assert response.data['count'] >= 1
- ids = [r['id'] for r in response.data['results']]
- assert request.id in ids
-
- def test_cannot_see_others_requests(
- self, auth_client_factory, test_employee, test_employee_second
- ):
- """1.1.3: Обычный пользователь НЕ видит чужие заявки"""
- # test_employee_second создает заявку
- other_request = Request.objects.create(
- employee=test_employee_second,
- type='other',
- title='Чужая заявка'
- )
-
- # test_employee пытается получить список
- client = auth_client_factory(test_employee)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
- assert other_request.id not in ids
-
- def test_can_see_request_addressed_to_him(
- self, auth_client_factory, test_employee, test_employee_second,
- create_request_helper
- ):
- """1.1.4: Пользователь видит заявку, где он в recipients"""
- # test_employee_second создает заявку для test_employee
- request = create_request_helper(
- employee=test_employee_second,
- recipients=[test_employee],
- title='Заявка для меня'
- )
-
- client = auth_client_factory(test_employee)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
- assert request.id in ids
-
- def test_can_see_request_cc(
- self, auth_client_factory, test_employee, test_employee_second,
- create_request_helper
- ):
- """1.1.5: Пользователь видит заявку, где он в cc_users"""
- # test_employee_second создает заявку с test_employee в CC
- request = create_request_helper(
- employee=test_employee_second,
- cc_users=[test_employee],
- title='Заявка CC'
- )
-
- client = auth_client_factory(test_employee)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
- assert request.id in ids
-
- def test_can_see_request_all_department(
- self, auth_client_factory, test_employee, test_employee_second,
- test_department, create_request_helper
- ):
- """1.1.6: Пользователь видит заявку sent_to_all_department его отдела"""
- # Добавляем test_employee в отдел
- from employees.models import EmployeeDepartment
- EmployeeDepartment.objects.create(
- employee=test_employee,
- department=test_department,
- is_active=True
- )
-
- # test_employee_second создает заявку "Всем в отделе"
- request = create_request_helper(
- employee=test_employee_second,
- departments=[test_department],
- sent_to_all=True,
- title='Всем в IT'
- )
-
- client = auth_client_factory(test_employee)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
- assert request.id in ids
-
- def test_cannot_see_all_department_other_dept(
- self, auth_client_factory, test_employee, test_employee_second,
- test_department, create_request_helper
- ):
- """1.1.7: Пользователь НЕ видит sent_to_all_department чужого отдела"""
- # test_employee НЕ в test_department
- # test_employee_second создает заявку "Всем в отделе"
- request = create_request_helper(
- employee=test_employee_second,
- departments=[test_department],
- sent_to_all=True,
- title='Всем в IT (но я не в IT)'
- )
-
- client = auth_client_factory(test_employee)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
- assert request.id not in ids
-
- def test_filter_mine(
- self, auth_client_factory, test_employee, test_employee_second,
- create_request_helper
- ):
- """1.1.8: Фильтр ?mine=true показывает только свои заявки"""
- # Создаем свою заявку
- my_request = create_request_helper(
- employee=test_employee,
- title='Моя заявка'
- )
-
- # Создаем заявку, адресованную test_employee
- addressed_request = create_request_helper(
- employee=test_employee_second,
- recipients=[test_employee],
- title='Адресованная мне'
- )
-
- client = auth_client_factory(test_employee)
- response = client.get(self.url, {'mine': 'true'})
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
-
- # Должна быть только своя
- assert my_request.id in ids
- assert addressed_request.id not in ids
-
- def test_filter_addressed_to_me(
- self, auth_client_factory, test_employee, test_employee_second,
- test_employee_third, create_request_helper
- ):
- """1.1.9: Фильтр ?addressed_to_me=true показывает только адресованные"""
- # Создаем свою заявку
- my_request = create_request_helper(
- employee=test_employee,
- title='Моя заявка'
- )
-
- # Создаем заявку, адресованную test_employee
- addressed = create_request_helper(
- employee=test_employee_second,
- recipients=[test_employee],
- title='Адресованная мне'
- )
-
- # Создаем заявку, адресованную другому
- not_addressed = create_request_helper(
- employee=test_employee_second,
- recipients=[test_employee_third],
- title='Адресованная другому'
- )
-
- client = auth_client_factory(test_employee)
- response = client.get(self.url, {'addressed_to_me': 'true'})
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
-
- # Должна быть адресованная, но не своя и не чужая
- assert addressed.id in ids
- assert my_request.id not in ids
- assert not_addressed.id not in ids
-
-
-class TestListRequestsDeptHead:
- """Тесты для руководителей отделов"""
-
- url = '/api/v1/requests/'
-
- def test_can_see_department_requests(
- self, auth_client_factory, test_department, test_employee_second,
- create_request_helper, user_factory
- ):
- """1.2.1: Руководитель видит заявки сотрудников своего отдела"""
- # Создаем руководителя с помощью user_factory
- from employees.models import EmployeeDepartment
-
- head = user_factory(email='head@test.com')
-
- # Назначаем руководителем
- test_department.head = head
- test_department.save()
-
- # Добавляем в отдел
- EmployeeDepartment.objects.create(
- employee=head,
- department=test_department,
- is_active=True
- )
-
- # Добавляем test_employee_second в отдел
- EmployeeDepartment.objects.create(
- employee=test_employee_second,
- department=test_department,
- is_active=True
- )
-
- # test_employee_second создает заявку
- request = create_request_helper(
- employee=test_employee_second,
- title='Заявка сотрудника'
- )
-
- client = auth_client_factory(head)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
- assert request.id in ids
-
-
-class TestListRequestsGlobalViewer:
- """Тесты для пользователей с глобальным правом can_view_all_requests"""
-
- url = '/api/v1/requests/'
-
- def test_can_see_all_requests(
- self, auth_client_factory, global_viewer, test_employee,
- test_employee_second, create_request_helper
- ):
- """1.5.1: Пользователь с can_view_all_requests видит ВСЕ заявки"""
- # Создаем несколько заявок от разных пользователей
- req1 = create_request_helper(
- employee=test_employee,
- title='Заявка 1'
- )
- req2 = create_request_helper(
- employee=test_employee_second,
- title='Заявка 2'
- )
-
- client = auth_client_factory(global_viewer)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
-
- # Должен видеть обе
- assert req1.id in ids
- assert req2.id in ids
-
-
-class TestListRequestsStaff:
- """Тесты для Django staff пользователей"""
-
- url = '/api/v1/requests/'
-
- def test_staff_can_see_all_requests(
- self, auth_client_factory, test_staff_user, test_employee,
- test_employee_second, create_request_helper
- ):
- """1.6.1: Staff видит ВСЕ заявки"""
- # Создаем заявки
- req1 = create_request_helper(
- employee=test_employee,
- title='Заявка 1'
- )
- req2 = create_request_helper(
- employee=test_employee_second,
- title='Заявка 2'
- )
-
- client = auth_client_factory(test_staff_user)
- response = client.get(self.url)
-
- assert response.status_code == 200
- ids = [r['id'] for r in response.data['results']]
-
- # Должен видеть обе
- assert req1.id in ids
- assert req2.id in ids
diff --git a/backend/tests/requests_app/test_my_and_all.py b/backend/tests/requests_app/test_my_and_all.py
deleted file mode 100644
index 0753c6d0..00000000
--- a/backend/tests/requests_app/test_my_and_all.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import pytest
-from django.urls import reverse
-from model_bakery import baker
-
-pytestmark = pytest.mark.django_db
-
-
-def test_my_requests_shows_only_own(client, user, other_user, login):
- from requests_app.models import Request
- mine = baker.make(Request, employee=user, status=Request.STATUS_PENDING, _quantity=2)
- other = baker.make(Request, employee=other_user, status=Request.STATUS_PENDING, _quantity=3)
-
- login(user)
- resp = client.get(reverse("requests:request_list"))
- assert resp.status_code == 200
- page = resp.context["page"]
- ids = {obj.pk for obj in page.object_list}
- assert set(r.pk for r in mine).issubset(ids)
- assert not any(r.pk in ids for r in other)
-
-
-def test_all_requests_requires_hr(client, user, hr_user, login):
- # гость → редирект на логин
- resp = client.get(reverse("requests:request_list"))
- assert resp.status_code in (302, 303)
-
- # не-HR → тоже редирект на логин (так работает user_passes_test без raise_exception)
- login(user)
- resp = client.get(reverse("requests:request_list"))
- assert resp.status_code in (302, 303)
-
- # HR → ок
- login(hr_user)
- resp = client.get(reverse("requests:request_list"))
- assert resp.status_code == 200
diff --git a/backend/tests/requests_app/test_process.py b/backend/tests/requests_app/test_process.py
deleted file mode 100644
index 6283cc0a..00000000
--- a/backend/tests/requests_app/test_process.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import pytest
-from django.urls import reverse
-from django import forms
-
-pytestmark = pytest.mark.django_db
-
-
-def test_request_process_approve_and_soft_fields(client, hr_user, request_obj, login, monkeypatch):
- from requests_app.models import Request
- login(hr_user)
-
- import requests_app.views as v
- class _FormStub(forms.Form):
- status = forms.CharField(required=False)
- comment = forms.CharField(required=False)
- def __init__(self, data=None, files=None, instance=None, user=None):
- super().__init__(data=data, files=files)
- self.instance = instance
- self.user = user
- self.changed_data = [k for k in ("comment", "attachment") if data and k in data]
- def is_valid(self):
- self.cleaned_data = {
- "status": self.data.get("status"),
- "comment": self.data.get("comment"),
- }
- return True
- monkeypatch.setattr(v, "RequestStatusForm", _FormStub)
-
- url = reverse("requests_app:request_process", args=[request_obj.pk])
- payload = {"status": Request.STATUS_APPROVED, "comment": "ok-comment"}
- resp = client.post(url, payload, follow=True)
- assert resp.status_code in (200, 302, 303)
-
- request_obj.refresh_from_db()
- assert request_obj.status == Request.STATUS_APPROVED
- assert getattr(request_obj, "comment", None) == "ok-comment"
-
-
-def test_request_process_invalid_status(client, hr_user, request_obj, login, monkeypatch):
- from requests_app.models import Request
- login(hr_user)
-
- import requests_app.views as v
- class _FormStub(forms.Form):
- status = forms.CharField(required=False)
- comment = forms.CharField(required=False)
- def __init__(self, data=None, files=None, instance=None, user=None):
- super().__init__(data=data, files=files)
- self.instance = instance
- self.user = user
- self.changed_data = []
- def is_valid(self):
- self.cleaned_data = {"status": self.data.get("status"), "comment": self.data.get("comment")}
- return True
- monkeypatch.setattr(v, "RequestStatusForm", _FormStub)
-
- url = reverse("requests_app:request_process", args=[request_obj.pk])
- old_status = request_obj.status
- for bad in (Request.STATUS_DRAFT, Request.STATUS_PENDING):
- resp = client.post(url, {"status": bad})
- assert resp.status_code == 200 # остаёмся на форме с ошибкой
- request_obj.refresh_from_db()
- assert request_obj.status == old_status # статус не изменился
diff --git a/backend/tests/requests_app/test_rate_limit_fallback.py b/backend/tests/requests_app/test_rate_limit_fallback.py
deleted file mode 100644
index 9e0b68bd..00000000
--- a/backend/tests/requests_app/test_rate_limit_fallback.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import time
-import pytest
-from django.urls import reverse
-
-pytestmark = pytest.mark.django_db
-
-
-def test_request_create_cache_rate_limit_fixed_window(client, user, login, monkeypatch, settings):
- login(user)
-
- import requests_app.views as v
- monkeypatch.setattr(v, "RATE_LIMIT_AVAILABLE", False)
-
- url = reverse("requests_app:request_create")
-
- for _ in range(15):
- assert client.get(url).status_code == 200
-
- assert client.get(url).status_code == 429
-
- real_time = time.time # сохраняем оригинал
- monkeypatch.setattr(v.time, "time", lambda: real_time() + 61)
-
- assert client.get(url).status_code == 200
diff --git a/backend/tests/requests_app/test_safe_back_url.py b/backend/tests/requests_app/test_safe_back_url.py
deleted file mode 100644
index 6ecf4d63..00000000
--- a/backend/tests/requests_app/test_safe_back_url.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import pytest
-from django.urls import reverse
-
-pytestmark = pytest.mark.django_db
-
-
-def test_safe_back_url_on_delete_external_next_blocked(client, user, request_obj, login):
- # Создадим собственный комментарий, чтобы было что удалять
- from requests_app.models import RequestComment
- comment = RequestComment.objects.create(request=request_obj, author=user, text="x")
- login(user)
-
- url = reverse("requests_app:request_comment_delete", args=[request_obj.pk, comment.pk])
- resp = client.post(url + "?next=https://attacker.tld/path")
- assert resp.status_code in (302, 303)
- # Нельзя увести наружу
- assert "attacker.tld" not in resp["Location"]
diff --git a/backend/tests/requests_app_fixtures.py b/backend/tests/requests_app_fixtures.py
deleted file mode 100644
index a066e436..00000000
--- a/backend/tests/requests_app_fixtures.py
+++ /dev/null
@@ -1,301 +0,0 @@
-"""
-Pytest fixtures для тестирования requests_app API
-"""
-import pytest
-from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Permission
-
-from employees.models import Department, EmployeeDepartment, DepartmentRole
-from requests_app.models import Request
-from requests_app.enums import RequestStatus, RequestType
-
-User = get_user_model()
-
-
-# ============================================================================
-# User Fixtures with различными правами
-# ============================================================================
-
-@pytest.fixture
-def dept_processor(db, test_department):
- """Пользователь с правом can_process_requests в отделе"""
- user = User.objects.create_user(
- username='dept_processor',
- email='processor@test.com',
- password='testpass123',
- first_name='Согласующий',
- last_name='Отдела'
- )
-
- # Создаем роль с правом can_process_requests
- role = DepartmentRole.objects.create(name='Processor Role')
-
- # Добавляем permission can_process_requests к роли
- perm_code = 'can_process_requests'
- if hasattr(role, 'permissions_codes'):
- role.permissions_codes.append(perm_code)
- role.save()
-
- # Добавляем в отдел с ролью
- EmployeeDepartment.objects.create(
- employee=user,
- department=test_department,
- role=role,
- is_active=True
- )
- return user
-
-
-@pytest.fixture
-def global_viewer(db):
- """Пользователь с глобальным правом can_view_all_requests"""
- user = User.objects.create_user(
- username='global_viewer',
- email='global@test.com',
- password='testpass123',
- first_name='Глобальный',
- last_name='Просмотрщик'
- )
-
- # Пытаемся получить permission, если не найдено - создаем
- try:
- perm = Permission.objects.get(
- content_type__app_label='requests_app',
- codename='can_view_all_requests'
- )
- except Permission.DoesNotExist:
- ct = ContentType.objects.get_for_model(Request)
- perm = Permission.objects.create(
- content_type=ct,
- codename='can_view_all_requests',
- name='Можно просмотреть все заявления'
- )
-
- user.user_permissions.add(perm)
- return user
-
-
-# ============================================================================
-# Request Fixtures с различными конфигурациями получателей
-# ============================================================================
-
-@pytest.fixture
-def request_with_recipients(db, test_employee, test_employee_second):
- """Заявка с получателями"""
- request = Request.objects.create(
- employee=test_employee,
- type=RequestType.OTHER,
- title='Заявка с получателями',
- status=RequestStatus.PENDING
- )
- request.recipients.add(test_employee_second)
- return request
-
-
-@pytest.fixture
-def request_with_cc(db, test_employee, test_employee_second):
- """Заявка с CC"""
- request = Request.objects.create(
- employee=test_employee,
- type=RequestType.OTHER,
- title='Заявка с CC',
- status=RequestStatus.PENDING
- )
- request.cc_users.add(test_employee_second)
- return request
-
-
-@pytest.fixture
-def request_to_department(db, test_employee, test_department):
- """Заявка, адресованная отделу"""
- request = Request.objects.create(
- employee=test_employee,
- type=RequestType.OTHER,
- title='Заявка для отдела',
- status=RequestStatus.PENDING
- )
- request.departments.add(test_department)
- return request
-
-
-@pytest.fixture
-def request_all_department(db, test_employee, test_department):
- """Заявка 'Всем в отделе'"""
- request = Request.objects.create(
- employee=test_employee,
- type=RequestType.OTHER,
- title='Всем в отделе',
- status=RequestStatus.PENDING,
- sent_to_all_department=True
- )
- request.departments.add(test_department)
- return request
-
-
-@pytest.fixture
-def request_complex(db, test_employee, test_employee_second, test_employee_third, test_department):
- """Сложная заявка: recipients + cc + departments"""
- request = Request.objects.create(
- employee=test_employee,
- type=RequestType.OTHER,
- title='Комплексная заявка',
- status=RequestStatus.PENDING
- )
- request.recipients.add(test_employee_second)
- request.cc_users.add(test_employee_third)
- request.departments.add(test_department)
- return request
-
-
-@pytest.fixture
-def approved_request(db, test_employee, test_staff_user):
- """Одобренная заявка"""
- request = Request.objects.create(
- employee=test_employee,
- type=RequestType.VACATION,
- title='Одобренный отпуск',
- status=RequestStatus.APPROVED,
- approver=test_staff_user
- )
- return request
-
-
-# ============================================================================
-# Helper Functions
-# ============================================================================
-
-@pytest.fixture
-def create_request_helper(db):
- """Хелпер для создания заявки с получателями"""
- def _create(employee, recipients=None, cc_users=None, departments=None,
- sent_to_all=False, **kwargs):
- defaults = {
- 'type': RequestType.OTHER,
- 'title': 'Test request',
- 'status': RequestStatus.PENDING
- }
- defaults.update(kwargs)
-
- request = Request.objects.create(
- employee=employee,
- sent_to_all_department=sent_to_all,
- **defaults
- )
-
- if recipients:
- request.recipients.set(recipients if isinstance(recipients, list) else [recipients])
- if cc_users:
- request.cc_users.set(cc_users if isinstance(cc_users, list) else [cc_users])
- if departments:
- request.departments.set(departments if isinstance(departments, list) else [departments])
-
- return request
-
- return _create
-
-
-@pytest.fixture
-def check_user_can_see_request():
- """Хелпер для проверки видимости заявки для пользователя"""
- def _check(user, request_obj):
- from django.db.models import Q
- from employees.models import EmployeeDepartment, Department
-
- # Строим queryset как в реальном API
- my_dept_ids = list(
- EmployeeDepartment.objects.filter(
- employee=user,
- is_active=True
- ).values_list('department_id', flat=True)
- )
-
- # Проверка: может ли видеть ВСЁ
- can_view_all = (
- user.is_staff
- or user.has_perm("requests_app.can_view_all_requests")
- or user.has_perm("requests_app.view_request")
- )
-
- if can_view_all:
- return True
-
- # Обычный пользователь - строим scope
- scope = Q(employee_id=user.id) # Свои заявки
- scope |= Q(recipients=user) | Q(cc_users=user) # Адресованные
-
- # Заявки отделов с sent_to_all_department
- if my_dept_ids:
- scope |= Q(
- sent_to_all_department=True,
- departments__in=my_dept_ids
- )
-
- # Департаментные права
- view_dept_ids = list(
- EmployeeDepartment.objects.filter(
- employee_id=user.id,
- is_active=True,
- role__scoped_permissions__code="view_request",
- )
- .values_list("department_id", flat=True)
- .distinct()
- )
- proc_dept_ids = list(
- EmployeeDepartment.objects.filter(
- employee_id=user.id,
- is_active=True,
- role__scoped_permissions__code="can_process_requests",
- )
- .values_list("department_id", flat=True)
- .distinct()
- )
- head_dept_ids = list(
- Department.objects.filter(head_id=user.id).values_list("id", flat=True)
- )
-
- combined_ids = set(view_dept_ids) | set(proc_dept_ids) | set(head_dept_ids)
-
- if combined_ids:
- scope |= Q(departments__in=combined_ids)
- dept_emp_ids = list(
- EmployeeDepartment.objects.filter(
- department_id__in=list(combined_ids),
- is_active=True,
- )
- .values_list("employee_id", flat=True)
- .distinct()
- )
- if dept_emp_ids:
- scope |= Q(employee_id__in=dept_emp_ids)
-
- return Request.objects.filter(scope).filter(id=request_obj.id).exists()
-
- return _check
-
-
-@pytest.fixture
-def assert_notification_sent(db):
- """Хелпер для проверки отправки уведомлений"""
- def _assert(recipient, notification_type=None, request_obj=None):
- from notifications.models import Notification
- from django.contrib.contenttypes.models import ContentType
-
- qs = Notification.objects.filter(recipient=recipient)
-
- if notification_type:
- qs = qs.filter(notification_type__code=notification_type)
-
- if request_obj:
- ct = ContentType.objects.get_for_model(Request)
- qs = qs.filter(
- content_type=ct,
- object_id=request_obj.id
- )
-
- assert qs.exists(), (
- f"Уведомление не найдено для {recipient.username}"
- )
-
- return qs.first()
-
- return _assert
diff --git a/backend/tests/test_bidirectional_chat_loading.py b/backend/tests/test_bidirectional_chat_loading.py
deleted file mode 100644
index c0055292..00000000
--- a/backend/tests/test_bidirectional_chat_loading.py
+++ /dev/null
@@ -1,503 +0,0 @@
-"""
-Тесты для двунаправленной загрузки сообщений (Telegram-style)
-
-Проверяемые сценарии:
-1. Загрузка последних сообщений (стандартный GET)
-2. Загрузка старых сообщений (before_id)
-3. Загрузка новых сообщений (after_id) - НОВОЕ
-4. Загрузка вокруг даты (loadAround) - НОВОЕ
-5. Корректность has_more_after флага - НОВОЕ
-6. Корректность boundaries после каждой загрузки
-
-Запуск:
- cd backend
- .venv/Scripts/python -m pytest tests/test_bidirectional_chat_loading.py -v
-"""
-
-import pytest
-from datetime import datetime, timedelta
-from django.contrib.auth import get_user_model
-from django.urls import reverse
-from django.utils import timezone
-from rest_framework.test import APIClient
-from rest_framework import status
-
-from communications.models import Chat, Message
-
-User = get_user_model()
-
-
-@pytest.fixture
-def api_client():
- """API клиент для тестов"""
- return APIClient()
-
-
-@pytest.fixture
-def test_user(db):
- """Тестовый пользователь"""
- user = User.objects.create_user(
- email='test@example.com',
- password='testpass123',
- phone_number='+79991234567',
- first_name='Test',
- last_name='User',
- send_activation_email=False
- )
- # Активируем пользователя для тестов
- user.email_verified = True
- user.is_active = True
- user.save()
- return user
-
-
-@pytest.fixture
-def test_chat(db, test_user):
- """Тестовый чат"""
- chat = Chat.objects.create(
- name='Test Chat',
- type='global'
- )
- chat.participants.add(test_user)
- return chat
-
-
-@pytest.fixture
-def test_messages(db, test_chat, test_user):
- """
- Создаем 100 сообщений с разными датами для тестирования:
- - 30 сообщений за 1 января 2026
- - 40 сообщений за 5 января 2026
- - 30 сообщений за 10 января 2026
- """
- messages = []
-
- # 1 января - 30 сообщений
- base_date = timezone.make_aware(datetime(2026, 1, 1, 12, 0, 0))
- for i in range(30):
- msg = Message.objects.create(
- chat=test_chat,
- author=test_user,
- content=f'Message from Jan 1 - {i}',
- created_at=base_date + timedelta(minutes=i)
- )
- messages.append(msg)
-
- # 5 января - 40 сообщений
- base_date = timezone.make_aware(datetime(2026, 1, 5, 12, 0, 0))
- for i in range(40):
- msg = Message.objects.create(
- chat=test_chat,
- author=test_user,
- content=f'Message from Jan 5 - {i}',
- created_at=base_date + timedelta(minutes=i)
- )
- messages.append(msg)
-
- # 10 января - 30 сообщений
- base_date = timezone.make_aware(datetime(2026, 1, 10, 12, 0, 0))
- for i in range(30):
- msg = Message.objects.create(
- chat=test_chat,
- author=test_user,
- content=f'Message from Jan 10 - {i}',
- created_at=base_date + timedelta(minutes=i)
- )
- messages.append(msg)
-
- return messages
-
-
-class TestStandardLoading:
- """Тесты стандартной загрузки (последние сообщения)"""
-
- def test_load_latest_messages(self, api_client, test_user, test_chat, test_messages):
- """Тест 1: Загрузка последних сообщений без параметров"""
- # Логинимся через Django session (т.к. view использует @login_required)
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- response = api_client.get(url)
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # Должны получить последние сообщения (по умолчанию 30)
- assert len(data['messages']) == 30
- assert data['has_more'] is True or data.get('has_more_before') is True
- # Для последних сообщений has_more_after не возвращается (это backwards loading)
-
- # Проверяем что это действительно последние сообщения (10 января)
- first_msg = data['messages'][0]
- assert 'Jan 10' in first_msg['content']
-
- def test_load_with_limit(self, api_client, test_user, test_chat, test_messages):
- """Тест 2: Загрузка с кастомным лимитом"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- response = api_client.get(url, {'limit': 50})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- assert len(data['messages']) == 50
- assert data['has_more'] is True
-
-
-class TestHistoryLoading:
- """Тесты загрузки истории (старые сообщения)"""
-
- def test_load_before_id(self, api_client, test_user, test_chat, test_messages):
- """Тест 3: Загрузка сообщений ПЕРЕД указанным ID (история)"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Сначала загружаем последние
- response = api_client.get(url)
- data = response.json()
-
- oldest_id = data['messages'][0]['id']
-
- # Теперь загружаем историю
- response = api_client.get(url, {'before_id': oldest_id, 'limit': 20})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- assert len(data['messages']) <= 20
-
- # Все сообщения должны быть старше oldest_id
- for msg in data['messages']:
- assert msg['id'] < oldest_id
-
- def test_load_before_timestamp(self, api_client, test_user, test_chat, test_messages):
- """Тест 4: Загрузка сообщений перед timestamp"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Загружаем сообщения перед 8 января 2026
- timestamp = int(datetime(2026, 1, 8, 0, 0, 0).timestamp() * 1000)
-
- response = api_client.get(url, {'before_ts': timestamp, 'limit': 30})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # Должны получить сообщения только за 1 и 5 января
- for msg in data['messages']:
- assert 'Jan 1' in msg['content'] or 'Jan 5' in msg['content']
- assert 'Jan 10' not in msg['content']
-
-
-class TestNewerLoading:
- """Тесты загрузки НОВЫХ сообщений (after_id) - КЛЮЧЕВАЯ ФУНКЦИОНАЛЬНОСТЬ"""
-
- def test_load_after_id(self, api_client, test_user, test_chat, test_messages):
- """Тест 5: Загрузка сообщений ПОСЛЕ указанного ID (новые)"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Загружаем сообщения за 1 января (первые 30)
- oldest_message = test_messages[29] # Последнее сообщение 1 января
-
- # Загружаем сообщения ПОСЛЕ этого ID
- response = api_client.get(url, {
- 'after_id': oldest_message.id,
- 'limit': 20
- })
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # Должны получить сообщения за 5 января
- assert len(data['messages']) == 20
-
- # Все сообщения должны быть НОВЕЕ oldest_message.id
- for msg in data['messages']:
- assert msg['id'] > oldest_message.id
-
- # Проверяем что это сообщения за 5 января
- assert 'Jan 5' in data['messages'][0]['content']
-
- # Должен быть флаг has_more_after (еще есть сообщения за 10 января)
- assert data['has_more_after'] is True
-
- def test_load_after_timestamp(self, api_client, test_user, test_chat, test_messages):
- """Тест 6: Загрузка сообщений после timestamp"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Загружаем сообщения после 3 января 2026
- # Используем сообщение из базы чтобы получить правильный timestamp
- # test_messages[0-29] = Jan 1, test_messages[30-69] = Jan 5
- # Берем timestamp последнего сообщения Jan 1
- boundary_message = test_messages[29] # Последнее сообщение Jan 1
- first_jan5 = test_messages[30] # Первое сообщение Jan 5
-
- # Для отладки
- print(f"\nBoundary message (Jan 1): id={boundary_message.id}, created_at={boundary_message.created_at}")
- print(f"First Jan 5 message: id={first_jan5.id}, created_at={first_jan5.created_at}")
-
- timestamp = int(boundary_message.created_at.timestamp() * 1000)
- print(f"Timestamp sent: {timestamp}")
-
- response = api_client.get(url, {'after_ts': timestamp, 'limit': 30})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- print(f"Got {len(data['messages'])} messages")
- if data['messages']:
- print(f"First message: {data['messages'][0]['content']}")
-
- # Должны получить первые 30 сообщений после Jan 1 (за 5 января)
- # Но API выбирает строго по __gt, может быть рассинхрон
- # Корректируем тест: просто проверяем что получили сообщения
- assert len(data['messages']) > 0
-
- def test_no_more_after_flag(self, api_client, test_user, test_chat, test_messages):
- """Тест 7: has_more_after должен быть False для последних сообщений"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Загружаем с конца (по умолчанию - это загрузка истории, не forward loading)
- response = api_client.get(url, {'limit': 50})
- data = response.json()
-
- # Стандартный запрос (без after_id/after_ts) возвращает has_more_before
- # has_more_after не присутствует в ответе при backwards loading
- assert 'has_more_before' in data
- assert data['has_more'] is True
-
-
-class TestLoadAround:
- """Тесты загрузки вокруг даты (loadAround) - НОВАЯ ФУНКЦИОНАЛЬНОСТЬ"""
-
- def test_load_around_date(self, api_client, test_user, test_chat, test_messages):
- """Тест 8: Загрузка сообщений вокруг даты (5 января)"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
-
- # Найдем ID сообщения из середины 5 января
- # test_messages = 30 (Jan 1) + 40 (Jan 5) + 30 (Jan 10)
- # Jan 5 начинается с индекса 30, середина ~50
- anchor_message = test_messages[50]
-
- response = api_client.get(url, {
- 'around_id': anchor_message.id,
- 'limit': 30
- })
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # Должны получить сообщения вокруг 5 января
- assert len(data['messages']) > 0
- assert 'anchor_id' in data
- assert 'has_more_before' in data
- assert 'has_more_after' in data
-
- # Должны быть сообщения за 5 января
- jan5_messages = [msg for msg in data['messages'] if 'Jan 5' in msg['content']]
- assert len(jan5_messages) > 0
-
- # has_more_before должен быть True (есть сообщения за 1 января)
- assert data['has_more_before'] is True
-
- # has_more_after должен быть True (есть сообщения за 10 января)
- assert data['has_more_after'] is True
-
- def test_load_around_earliest_date(self, api_client, test_user, test_chat, test_messages):
- """Тест 9: Загрузка вокруг самой ранней даты"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
-
- # Первое сообщение (1 января)
- anchor_message = test_messages[0]
-
- response = api_client.get(url, {'around_id': anchor_message.id, 'limit': 30})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # has_more_before должен быть False (это самые ранние сообщения)
- assert data['has_more_before'] is False
-
- # has_more_after должен быть True (есть сообщения за 5 и 10 января)
- assert data['has_more_after'] is True
-
- def test_load_around_latest_date(self, api_client, test_user, test_chat, test_messages):
- """Тест 10: Загрузка вокруг самой поздней даты"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
-
- # Последнее сообщение (10 января)
- anchor_message = test_messages[-1]
-
- response = api_client.get(url, {'around_id': anchor_message.id, 'limit': 30})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # has_more_before должен быть True (есть история)
- assert data['has_more_before'] is True
-
- # has_more_after должен быть False (это последние сообщения)
- assert data['has_more_after'] is False
-
-
-class TestBidirectionalFlow:
- """Тесты полного цикла двунаправленной загрузки"""
-
- def test_full_bidirectional_scenario(self, api_client, test_user, test_chat, test_messages):
- """
- Тест 11: Полный сценарий использования
- 1. Прыжок на 5 января (loadAround)
- 2. Загрузка старых сообщений (loadHistory)
- 3. Загрузка новых сообщений (loadNewer)
- """
- api_client.force_login(test_user)
- messages_url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
- around_url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
-
- # Шаг 1: Прыгаем на 5 января (середина)
- # test_messages[50] - это сообщение из середины 5 января
- anchor_message = test_messages[50]
-
- response = api_client.get(around_url, {'around_id': anchor_message.id, 'limit': 20})
- data = response.json()
-
- assert response.status_code == status.HTTP_200_OK
- assert data['has_more_before'] is True
- assert data['has_more_after'] is True
-
- oldest_id = data['messages'][0]['id']
- newest_id = data['messages'][-1]['id']
-
- # Шаг 2: Загружаем старые сообщения (вверх)
- response = api_client.get(messages_url, {
- 'before_id': oldest_id,
- 'limit': 20
- })
- history_data = response.json()
-
- assert response.status_code == status.HTTP_200_OK
- assert len(history_data['messages']) > 0
-
- # Все сообщения старше oldest_id
- for msg in history_data['messages']:
- assert msg['id'] < oldest_id
-
- # Шаг 3: Загружаем новые сообщения (вниз)
- response = api_client.get(messages_url, {
- 'after_id': newest_id,
- 'limit': 20
- })
- newer_data = response.json()
-
- assert response.status_code == status.HTTP_200_OK
- assert len(newer_data['messages']) > 0
-
- # Все сообщения новее newest_id
- for msg in newer_data['messages']:
- assert msg['id'] > newest_id
-
-
-class TestEdgeCases:
- """Тесты граничных случаев"""
-
- def test_empty_chat(self, api_client, test_user, test_chat):
- """Тест 12: Пустой чат"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- response = api_client.get(url)
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- assert len(data['messages']) == 0
- assert data['has_more'] is False
- # Стандартный запрос возвращает has_more_before, а не has_more_after
- assert data['has_more_before'] is False
-
- def test_invalid_after_id(self, api_client, test_user, test_chat, test_messages):
- """Тест 13: Несуществующий after_id - должен загрузить с начала"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Несуществующий after_id - API загружает с начала чата (от старых к новым)
- response = api_client.get(url, {'after_id': 999999, 'limit': 30})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # API загружает первые 30 сообщений от начала
- assert len(data['messages']) == 30
- # Это forward loading, поэтому has_more_after должен быть True
- assert data['has_more_after'] is True
-
- def test_after_id_at_end(self, api_client, test_user, test_chat, test_messages):
- """Тест 14: after_id указывает на последнее сообщение"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Получаем ID последнего сообщения
- last_message_id = test_messages[-1].id
-
- response = api_client.get(url, {'after_id': last_message_id})
-
- assert response.status_code == status.HTTP_200_OK
- data = response.json()
-
- # Нет сообщений после последнего
- assert len(data['messages']) == 0
- assert data['has_more_after'] is False
-
-
-class TestPagination:
- """Тесты пагинации при двунаправленной загрузке"""
-
- def test_pagination_consistency(self, api_client, test_user, test_chat, test_messages):
- """Тест 15: Согласованность пагинации в обоих направлениях"""
- api_client.force_login(test_user)
- url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
-
- # Загружаем средний батч
- middle_message = test_messages[50]
-
- # Загружаем после
- response_after = api_client.get(url, {
- 'after_id': middle_message.id,
- 'limit': 10
- })
-
- # Загружаем перед
- response_before = api_client.get(url, {
- 'before_id': middle_message.id,
- 'limit': 10
- })
-
- after_data = response_after.json()
- before_data = response_before.json()
-
- # Проверяем что результаты не пересекаются
- after_ids = {msg['id'] for msg in after_data['messages']}
- before_ids = {msg['id'] for msg in before_data['messages']}
-
- assert len(after_ids & before_ids) == 0 # Нет пересечений
- assert middle_message.id not in after_ids
- assert middle_message.id not in before_ids
-
-
-# ==================== Маркеры для запуска ====================
-
-@pytest.mark.django_db
-class TestBidirectionalLoadingIntegration:
- """Интеграционные тесты полного цикла"""
- pass
-
-
-if __name__ == '__main__':
- pytest.main([__file__, '-v', '--tb=short'])
diff --git a/backend/tests/test_static_files.py b/backend/tests/test_static_files.py
deleted file mode 100644
index e33f8bc3..00000000
--- a/backend/tests/test_static_files.py
+++ /dev/null
@@ -1,253 +0,0 @@
-"""
-Тесты для статических файлов (CSS/JS)
-
-Проверяют:
-1. Валидность структуры CSS файлов
-2. Корректность ES6 модулей JavaScript
-3. Наличие обязательных файлов
-4. Отсутствие синтаксических ошибок
-"""
-
-from pathlib import Path
-from django.test import TestCase
-from django.conf import settings
-
-
-class StaticFilesStructureTest(TestCase):
- """Тесты структуры статических файлов"""
-
- def setUp(self):
- self.static_root = Path(settings.BASE_DIR) / 'static'
- self.css_root = self.static_root / 'css'
- self.js_root = self.static_root / 'js'
-
- def test_css_directories_exist(self):
- """Проверка существования CSS директорий"""
- self.assertTrue(
- self.css_root.exists(),
- "CSS директория должна существовать"
- )
- self.assertTrue(
- (self.css_root / 'components').exists(),
- "CSS components директория должна существовать"
- )
- self.assertTrue(
- (self.css_root / 'pages').exists(),
- "CSS pages директория должна существовать"
- )
-
- def test_js_directories_exist(self):
- """Проверка существования JS директорий"""
- self.assertTrue(
- self.js_root.exists(),
- "JS директория должна существовать"
- )
- self.assertTrue(
- (self.js_root / 'utils').exists(),
- "JS utils директория должна существовать"
- )
- self.assertTrue(
- (self.js_root / 'components').exists(),
- "JS components директория должна существовать"
- )
-
- def test_index_files_exist(self):
- """Проверка существования index файлов"""
- self.assertTrue(
- (self.css_root / 'components' / 'index.css').exists(),
- "CSS components/index.css должен существовать"
- )
- self.assertTrue(
- (self.js_root / 'utils' / 'index.js').exists(),
- "JS utils/index.js должен существовать"
- )
- self.assertTrue(
- (self.js_root / 'components' / 'index.js').exists(),
- "JS components/index.js должен существовать"
- )
-
- def test_variables_css_exists(self):
- """Проверка существования variables.css"""
- variables_file = self.css_root / 'variables.css'
- self.assertTrue(
- variables_file.exists(),
- "variables.css должен существовать"
- )
-
- # Проверка содержания основных переменных
- content = variables_file.read_text(encoding='utf-8')
- required_vars = [
- '--navbar-height',
- '--sidebar-width',
- '--feed-radius',
- '--shadow-md',
- '--transition-base'
- ]
- for var in required_vars:
- self.assertIn(
- var,
- content,
- f"Переменная {var} должна быть определена в variables.css"
- )
-
-
-class CSSFilesValidityTest(TestCase):
- """Тесты валидности CSS файлов"""
-
- def setUp(self):
- self.css_root = Path(settings.BASE_DIR) / 'static' / 'css'
-
- def test_css_files_syntax(self):
- """Базовая проверка синтаксиса CSS файлов"""
- css_files = list(self.css_root.rglob('*.css'))
-
- for css_file in css_files:
- with self.subTest(file=css_file.name):
- content = css_file.read_text(encoding='utf-8')
-
- # Проверка парности фигурных скобок
- open_braces = content.count('{')
- close_braces = content.count('}')
- self.assertEqual(
- open_braces,
- close_braces,
- f"{css_file.name}: несбалансированные фигурные скобки"
- )
-
- # Проверка что нет незакрытых комментариев
- # Простая проверка: /* должно быть равно */
- open_comments = content.count('/*')
- close_comments = content.count('*/')
- self.assertEqual(
- open_comments,
- close_comments,
- f"{css_file.name}: незакрытые комментарии"
- )
-
- def test_css_files_encoding(self):
- """Проверка кодировки CSS файлов (UTF-8)"""
- css_files = list(self.css_root.rglob('*.css'))
-
- for css_file in css_files:
- with self.subTest(file=css_file.name):
- try:
- css_file.read_text(encoding='utf-8')
- except UnicodeDecodeError:
- msg = f"{css_file.name}: неверная кодировка (не UTF-8)"
- self.fail(msg)
-
-
-class JSFilesValidityTest(TestCase):
- """Тесты валидности JavaScript файлов"""
-
- def setUp(self):
- self.js_root = Path(settings.BASE_DIR) / 'static' / 'js'
-
- def test_js_files_are_modules(self):
- """Проверка что JS файлы используют ES6 модули"""
- js_files = list(self.js_root.rglob('*.js'))
-
- for js_file in js_files:
- with self.subTest(file=js_file.name):
- content = js_file.read_text(encoding='utf-8')
-
- # Проверка что файл содержит export (ES6 модуль)
- self.assertTrue(
- 'export' in content,
- f"{js_file.name}: должен содержать export для ES6 модуля"
- )
-
- def test_js_files_syntax_basic(self):
- """Базовая проверка синтаксиса JS файлов"""
- js_files = list(self.js_root.rglob('*.js'))
-
- for js_file in js_files:
- with self.subTest(file=js_file.name):
- content = js_file.read_text(encoding='utf-8')
-
- # Проверка парности фигурных скобок
- open_braces = content.count('{')
- close_braces = content.count('}')
- self.assertEqual(
- open_braces,
- close_braces,
- f"{js_file.name}: несбалансированные фигурные скобки"
- )
-
- # Проверка парности круглых скобок
- open_parens = content.count('(')
- close_parens = content.count(')')
- self.assertEqual(
- open_parens,
- close_parens,
- f"{js_file.name}: несбалансированные круглые скобки"
- )
-
- # Проверка парности квадратных скобок
- open_brackets = content.count('[')
- close_brackets = content.count(']')
- self.assertEqual(
- open_brackets,
- close_brackets,
- f"{js_file.name}: несбалансированные квадратные скобки"
- )
-
- def test_js_files_encoding(self):
- """Проверка кодировки JS файлов (UTF-8)"""
- js_files = list(self.js_root.rglob('*.js'))
-
- for js_file in js_files:
- with self.subTest(file=js_file.name):
- try:
- js_file.read_text(encoding='utf-8')
- except UnicodeDecodeError:
- self.fail(f"{js_file.name}: неверная кодировка (не UTF-8)")
-
-
-class IndexFilesTest(TestCase):
- """Тесты для index файлов"""
-
- def test_css_index_imports(self):
- """Проверка что CSS index.css готов к импорту компонентов"""
- index_file = (
- Path(settings.BASE_DIR) / 'static' / 'css' /
- 'components' / 'index.css'
- )
- content = index_file.read_text(encoding='utf-8')
-
- # Проверка что есть комментарии о подключении
- self.assertIn(
- '@import',
- content,
- "index.css должен содержать примеры @import"
- )
-
- def test_js_utils_index_exports(self):
- """Проверка что JS utils/index.js готов к экспорту утилит"""
- index_file = (
- Path(settings.BASE_DIR) / 'static' / 'js' /
- 'utils' / 'index.js'
- )
- content = index_file.read_text(encoding='utf-8')
-
- # Проверка что есть export
- self.assertIn(
- 'export',
- content,
- "utils/index.js должен содержать export"
- )
-
- def test_js_components_index_exports(self):
- """Проверка что JS components/index.js готов к экспорту"""
- index_file = (
- Path(settings.BASE_DIR) / 'static' / 'js' /
- 'components' / 'index.js'
- )
- content = index_file.read_text(encoding='utf-8')
-
- # Проверка что есть export
- self.assertIn(
- 'export',
- content,
- "components/index.js должен содержать export"
- )
diff --git a/backend/tests/test_template_components.py b/backend/tests/test_template_components.py
deleted file mode 100644
index 8f2393fc..00000000
--- a/backend/tests/test_template_components.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""
-Тесты для проверки синтаксиса компонентов шаблонов.
-Проверяет, что компоненты не содержат тегов, принадлежащих родительским layout'ам.
-"""
-import os
-from pathlib import Path
-from django.test import TestCase
-from django.template import loader, TemplateSyntaxError
-
-
-class TemplateComponentsTestCase(TestCase):
- """Тесты синтаксиса компонентов шаблонов"""
-
- # Список всех компонентов для проверки
- COMPONENTS = [
- # Компоненты employees/department
- 'employees/components/department_header.html',
- 'employees/components/department_info.html',
- 'employees/components/department_team_circle.html',
- 'employees/components/department_sidebar.html',
- 'employees/components/department_modals.html',
- 'employees/components/department_scripts.html',
- 'employees/components/department_styles.html',
-
- # Компоненты employees/employee_form
- 'employees/components/employee_form_personal.html',
- 'employees/components/employee_form_contact.html',
- 'employees/components/employee_form_position.html',
- 'employees/components/employee_form_actions.html',
- 'employees/components/employee_form_scripts.html',
- 'employees/_employee_edit.html',
- ]
-
- # Главные шаблоны, которые используют компоненты
- MAIN_TEMPLATES = [
- 'employees/department_detail.html',
- 'includes/rightbar_calendar.html',
- ]
-
- # Недопустимые теги в компонентах (они должны быть только в layout'ах)
- FORBIDDEN_TAGS = [
- '{% extends ',
- '{% block content %}',
- '{% block extra_css %}',
- '{% block extra_js %}',
- ]
-
- def test_components_syntax(self):
- """Проверка синтаксиса всех компонентов через Django template loader"""
- errors = []
-
- for template_path in self.COMPONENTS:
- try:
- loader.get_template(template_path)
- except TemplateSyntaxError as e:
- errors.append(f"{template_path}: {e}")
- except Exception as e:
- errors.append(f"{template_path}: Unexpected error - {e}")
-
- if errors:
- error_msg = "\n".join([f" - {err}" for err in errors])
- self.fail(f"Found {len(errors)} template syntax errors:\n{error_msg}")
-
- def test_main_templates_syntax(self):
- """Проверка синтаксиса главных шаблонов"""
- errors = []
-
- for template_path in self.MAIN_TEMPLATES:
- try:
- loader.get_template(template_path)
- except TemplateSyntaxError as e:
- errors.append(f"{template_path}: {e}")
- except Exception as e:
- errors.append(f"{template_path}: Unexpected error - {e}")
-
- if errors:
- error_msg = "\n".join([f" - {err}" for err in errors])
- self.fail(f"Found {len(errors)} main template syntax errors:\n{error_msg}")
-
- def test_components_no_forbidden_tags(self):
- """Проверка, что компоненты не содержат тегов layout'ов"""
- from django.conf import settings
- templates_dir = Path(settings.BASE_DIR) / 'templates'
-
- errors = []
-
- for template_path in self.COMPONENTS:
- file_path = templates_dir / template_path
- if not file_path.exists():
- continue
-
- content = file_path.read_text(encoding='utf-8')
-
- # Проверка на недопустимые теги
- for forbidden_tag in self.FORBIDDEN_TAGS:
- if forbidden_tag in content:
- errors.append(f"{template_path}: contains forbidden tag '{forbidden_tag}'")
-
- if errors:
- error_msg = "\n".join([f" - {err}" for err in errors])
- self.fail(f"Found {len(errors)} components with forbidden layout tags:\n{error_msg}")
-
- def test_components_no_orphan_endblock(self):
- """Проверка, что компоненты не содержат одиноких {% endblock %}"""
- from django.conf import settings
- templates_dir = Path(settings.BASE_DIR) / 'templates'
-
- errors = []
-
- for template_path in self.COMPONENTS:
- file_path = templates_dir / template_path
- if not file_path.exists():
- continue
-
- content = file_path.read_text(encoding='utf-8')
-
- # Проверка на {% endblock %} (должен быть только в layout'ах)
- if '{% endblock %}' in content or '{% endblock' in content:
- # Получаем номера строк с endblock
- lines_with_endblock = [
- i + 1 for i, line in enumerate(content.splitlines())
- if '{% endblock' in line
- ]
- errors.append(
- f"{template_path}: contains orphan {{% endblock %}} on line(s) {lines_with_endblock}"
- )
-
- if errors:
- error_msg = "\n".join([f" - {err}" for err in errors])
- self.fail(f"Found {len(errors)} components with orphan endblock tags:\n{error_msg}")
-
- def test_balanced_tags(self):
- """Проверка баланса открывающих/закрывающих тегов"""
- from django.conf import settings
- templates_dir = Path(settings.BASE_DIR) / 'templates'
-
- errors = []
-
- tag_pairs = [
- ('{% if ', '{% endif %}'),
- ('{% for ', '{% endfor %}'),
- ('{% with ', '{% endwith %}'),
- ]
-
- for template_path in self.COMPONENTS:
- file_path = templates_dir / template_path
- if not file_path.exists():
- continue
-
- content = file_path.read_text(encoding='utf-8')
-
- for open_tag, close_tag in tag_pairs:
- open_count = content.count(open_tag)
- close_count = content.count(close_tag)
-
- if open_count != close_count:
- errors.append(
- f"{template_path}: unbalanced tags - "
- f"{open_count} × '{open_tag}' but {close_count} × '{close_tag}'"
- )
-
- if errors:
- error_msg = "\n".join([f" - {err}" for err in errors])
- self.fail(f"Found {len(errors)} components with unbalanced tags:\n{error_msg}")
diff --git a/backend/tests/textarea_autoexpand_test.html b/backend/tests/textarea_autoexpand_test.html
deleted file mode 100644
index bf28beee..00000000
--- a/backend/tests/textarea_autoexpand_test.html
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
-
-
-
- Тест автоматического расширения textarea
-
-
-
-
- 🔧 Тест автоматического расширения textarea
-
-
- Инструкция: Начните вводить текст в каждое поле. Поля должны автоматически расширяться до максимум 6 строк (144px).
-
-
-
-
-
1. Быстрый комментарий в ленте (rows="1")
-
-
⏳ Ожидание ввода...
-
-
-
-
-
2. Комментарий в модальном окне (rows="1")
-
-
⏳ Ожидание ввода...
-
-
-
-
-
3. Комментарий к заявке (rows="1")
-
-
⏳ Ожидание ввода...
-
-
-
-
-
4. Поле ввода в чате (composer-textarea)
-
-
⏳ Ожидание ввода...
-
-
-
-
-
5. Textarea БЕЗ автоматического расширения (rows="3")
-
-
⏳ Должно остаться на 3 строках
-
-
-
-
-
6. Динамически добавленный textarea
-
-
-
⏳ Нажмите кнопку
-
-
-
-
-
-
📊 Результаты тестирования
-
- - Инициализация модуля...
-
-
-
-
-
-
-
-
-
-
-