- Replaced feed-* inline styles in base.html with feed-cards.css component (~60 lines) - Replaced ios-search inline styles in 3 templates with ios-search.css component * employees/employees_list.html * search/results.html * employees/department_list.html - Eliminated ~150 lines of duplicate CSS across templates - All 17 tests passing
296 lines
8.8 KiB
Python
296 lines
8.8 KiB
Python
"""
|
||
Pytest конфигурация и фикстуры для тестирования LDAP модуля.
|
||
"""
|
||
|
||
import pytest
|
||
from unittest.mock import Mock, patch
|
||
from django.contrib.auth.models import Group as DjangoGroup
|
||
|
||
from employees.models import Employee, Department, Position, LdapSyncState
|
||
from employees.ldap.domain.dtos import DirectoryUserDTO, DirectoryDepartmentDTO
|
||
|
||
|
||
# ==================== Mock LDAP Connection ====================
|
||
|
||
@pytest.fixture
|
||
def mock_ldap_connection():
|
||
"""Mock LDAP соединения для изоляции от реального AD."""
|
||
conn = Mock()
|
||
conn.result = {'description': 'success'}
|
||
conn.entries = []
|
||
|
||
# Mock базовых операций
|
||
conn.add = Mock(return_value=True)
|
||
conn.delete = Mock(return_value=True)
|
||
conn.modify = Mock(return_value=True)
|
||
conn.modify_dn = Mock(return_value=True)
|
||
conn.search = Mock(return_value=True)
|
||
|
||
# Mock extend для паролей
|
||
conn.extend = Mock()
|
||
conn.extend.microsoft = Mock()
|
||
conn.extend.microsoft.modify_password = Mock(return_value=True)
|
||
|
||
return conn
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_ldap_context(mock_ldap_connection):
|
||
"""Mock контекстного менеджера _ldap()."""
|
||
with patch('employees.ldap.infrastructure.connections._ldap') as mock:
|
||
mock.return_value.__enter__.return_value = mock_ldap_connection
|
||
mock.return_value.__exit__.return_value = None
|
||
yield mock
|
||
|
||
|
||
# ==================== Django Models Fixtures ====================
|
||
|
||
@pytest.fixture
|
||
def sample_employee(db):
|
||
"""Создает тестового сотрудника."""
|
||
return Employee.objects.create(
|
||
first_name="Иван",
|
||
last_name="Иванов",
|
||
email="ivanov@example.com",
|
||
is_active=True
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_department(db):
|
||
"""Создает тестовый отдел."""
|
||
return Department.objects.create(
|
||
name="IT Отдел",
|
||
description="IT Department"
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_position(db):
|
||
"""Создает тестовую должность."""
|
||
return Position.objects.create(
|
||
name="Разработчик",
|
||
description="Backend разработчик"
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_django_group(db):
|
||
"""Создает тестовую Django группу."""
|
||
return DjangoGroup.objects.create(name="Developers")
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_ldap_sync_state(db, sample_employee):
|
||
"""Создает тестовую запись синхронизации."""
|
||
return LdapSyncState.objects.create(
|
||
model='employee',
|
||
object_pk=str(sample_employee.id),
|
||
ldap_dn='CN=Иван Иванов,OU=Users,DC=example,DC=com'
|
||
)
|
||
|
||
|
||
# ==================== DTO Fixtures ====================
|
||
|
||
@pytest.fixture
|
||
def sample_user_dto():
|
||
"""Создает тестовый DTO для пользователя."""
|
||
return DirectoryUserDTO(
|
||
first_name="Иван",
|
||
last_name="Иванов",
|
||
email="ivanov@example.com",
|
||
phone_e164="+79001234567",
|
||
department_dn="OU=IT,DC=example,DC=com",
|
||
group_cns=["Developers", "Users"],
|
||
initial_password="Test123456!",
|
||
avatar_bytes=None,
|
||
is_active=True
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_department_dto():
|
||
"""Создает тестовый DTO для отдела."""
|
||
return DirectoryDepartmentDTO(
|
||
name="IT Отдел",
|
||
description="IT Department",
|
||
head=None
|
||
)
|
||
|
||
|
||
# ==================== Mock Repositories ====================
|
||
|
||
@pytest.fixture
|
||
def mock_ldap_repository():
|
||
"""Mock LdapRepository."""
|
||
repo = Mock()
|
||
repo.read_attrs = Mock(return_value={'cn': ['Test User']})
|
||
repo.is_taken = Mock(return_value=False)
|
||
repo.modify_attrs = Mock(return_value=True)
|
||
repo.ensure_container_exists = Mock(return_value=True)
|
||
repo.modify_or_ignore = Mock(return_value=True)
|
||
return repo
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_employee_repository():
|
||
"""Mock EmployeeRepository."""
|
||
repo = Mock()
|
||
repo.load_users_index = Mock(return_value=({}, {}))
|
||
repo.find_user_for_dto = Mock(return_value=None)
|
||
repo.bind_user_department = Mock(return_value=True)
|
||
repo.cleanup_absent_users = Mock(return_value=0)
|
||
return repo
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_sync_state_repository():
|
||
"""Mock SyncStateRepository."""
|
||
repo = Mock()
|
||
repo.get_or_create = Mock()
|
||
repo.touch = Mock()
|
||
repo.get_employees_with_dn = Mock(return_value=Employee.objects.none())
|
||
repo.delete_for_employee = Mock()
|
||
return repo
|
||
|
||
|
||
# ==================== Mock Services ====================
|
||
|
||
@pytest.fixture
|
||
def mock_user_service():
|
||
"""Mock UserService для тестирования других сервисов."""
|
||
service = Mock()
|
||
service.create_user = Mock()
|
||
service.update_user = Mock()
|
||
service.delete_user = Mock()
|
||
service._get_employee_dn = Mock(
|
||
return_value='CN=Test User,OU=Users,DC=example,DC=com'
|
||
)
|
||
service._move_user_to_base = Mock(
|
||
return_value='CN=Test User,OU=NewBase,DC=example,DC=com'
|
||
)
|
||
return service
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_group_service():
|
||
"""Mock GroupService для тестирования других сервисов."""
|
||
service = Mock()
|
||
dn = 'CN=Test Group,OU=Groups,DC=example,DC=com'
|
||
service.create = Mock(return_value=dn)
|
||
service.delete = Mock()
|
||
service.rename = Mock()
|
||
service.set_description = Mock()
|
||
service.add_members = Mock()
|
||
service.remove_members = Mock()
|
||
service.replace_members = Mock()
|
||
service.list_members = Mock(return_value=[])
|
||
service.find_dn = Mock(return_value=dn)
|
||
service.groups_with_member = Mock(return_value=set())
|
||
return service
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_department_service():
|
||
"""Mock DepartmentService для тестирования."""
|
||
service = Mock()
|
||
service.create_department = Mock()
|
||
service.update_department = Mock()
|
||
service.delete_department = Mock()
|
||
service.add_member = Mock()
|
||
service.remove_member = Mock()
|
||
service.set_head = Mock()
|
||
service._get_department_dn = Mock(
|
||
return_value='OU=Test Department,DC=example,DC=com'
|
||
)
|
||
return service
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_position_service():
|
||
"""Mock PositionService для тестирования."""
|
||
service = Mock()
|
||
service.reconcile_position = Mock()
|
||
service.assign_position = Mock()
|
||
service.unassign_position = Mock()
|
||
service.delete_position_group = Mock()
|
||
return service
|
||
|
||
|
||
# ==================== LDAP Entry Mocks ====================
|
||
|
||
@pytest.fixture
|
||
def mock_ldap_entry():
|
||
"""Mock LDAP entry объекта."""
|
||
entry = Mock()
|
||
entry.entry_dn = 'CN=Test User,OU=Users,DC=example,DC=com'
|
||
entry.cn = Mock()
|
||
entry.cn.value = 'Test User'
|
||
entry.mail = Mock()
|
||
entry.mail.value = 'test@example.com'
|
||
entry.objectGUID = Mock()
|
||
# Mock GUID (16 bytes)
|
||
guid_bytes = (
|
||
b'\x01\x02\x03\x04\x05\x06\x07\x08'
|
||
b'\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10'
|
||
)
|
||
entry.objectGUID.value = guid_bytes
|
||
return entry
|
||
|
||
|
||
# ==================== Settings Overrides ====================
|
||
|
||
@pytest.fixture
|
||
def ldap_test_settings(settings):
|
||
"""Тестовые настройки LDAP."""
|
||
settings.LDAP_HOST = 'ldaps://test.example.com:636'
|
||
settings.LDAP_BASE_DN = 'DC=example,DC=com'
|
||
settings.LDAP_BIND_DN = 'CN=TestUser,DC=example,DC=com'
|
||
settings.LDAP_BIND_PASSWORD = 'test_password'
|
||
settings.LDAP_GROUPS_BASE = 'OU=Groups,DC=example,DC=com'
|
||
settings.LDAP_POSITIONS_BASE = 'OU=Positions,DC=example,DC=com'
|
||
settings.LDAP_UPN_SUFFIX = '@example.com'
|
||
return settings
|
||
|
||
|
||
# ==================== Helper Functions ====================
|
||
|
||
def create_mock_ldap_entry(dn, attributes=None):
|
||
"""
|
||
Создает mock LDAP entry с заданными атрибутами.
|
||
|
||
Args:
|
||
dn: Distinguished Name
|
||
attributes: Словарь атрибутов {attr_name: value}
|
||
|
||
Returns:
|
||
Mock объект LDAP entry
|
||
"""
|
||
entry = Mock()
|
||
entry.entry_dn = dn
|
||
|
||
if attributes:
|
||
for attr_name, attr_value in attributes.items():
|
||
attr_mock = Mock()
|
||
attr_mock.value = attr_value
|
||
setattr(entry, attr_name, attr_mock)
|
||
|
||
return entry
|
||
|
||
|
||
def assert_ldap_operation_called(mock_conn, operation, dn=None):
|
||
"""
|
||
Проверяет, что LDAP операция была вызвана.
|
||
|
||
Args:
|
||
mock_conn: Mock LDAP соединения
|
||
operation: Название операции ('add', 'delete', 'modify', etc.)
|
||
dn: Ожидаемый DN (опционально)
|
||
"""
|
||
op_mock = getattr(mock_conn, operation)
|
||
assert op_mock.called, f"LDAP операция '{operation}' не была вызвана"
|
||
|
||
if dn and op_mock.call_args:
|
||
called_dn = op_mock.call_args[0][0]
|
||
assert called_dn == dn, f"Ожидался DN '{dn}', получен '{called_dn}'"
|