diff --git a/backend/api/v1/employees/serializers.py b/backend/api/v1/employees/serializers.py index 542ff6f8..2965ce02 100644 --- a/backend/api/v1/employees/serializers.py +++ b/backend/api/v1/employees/serializers.py @@ -54,14 +54,14 @@ class RegisterSerializer(serializers.Serializer): whatsapp = serializers.CharField(required=False, allow_blank=True, default="") wechat = serializers.CharField(required=False, allow_blank=True, default="") - avatar = Base64ImageField(required=True) + avatar = Base64ImageField(required=False, allow_null=True) patronymic = serializers.CharField(required=False, allow_blank=True, default="") gender = serializers.ChoiceField( - required=True, + required=False, + allow_null=True, choices=((1, "Мужской"), (2, "Женский")), error_messages={ - 'required': 'Поле "Пол" обязательно для заполнения.', 'invalid_choice': 'Укажите пол: 1 - Мужской, 2 - Женский.' } ) diff --git a/backend/api/v1/employees/views/employees.py b/backend/api/v1/employees/views/employees.py index ca445183..09ec437a 100644 --- a/backend/api/v1/employees/views/employees.py +++ b/backend/api/v1/employees/views/employees.py @@ -150,7 +150,8 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet): # Отдел (для перемещения) - резолвим DN здесь в ViewSet move_to_department_dn = None if 'department' in request_data or 'department_id' in request_data: - dept_val = request_data.get('department') or request_data.get('department_id') + dept_val = request_data.get( + 'department') or request_data.get('department_id') if dept_val: # Если передан ID - получаем инстанс из БД if isinstance(dept_val, int): @@ -162,11 +163,14 @@ class EmployeeViewSet(ExternalSystemSyncMixin, viewsets.ModelViewSet): # Резолвим DN отдела if dept: try: - from employees.ldap.services.department_service import DepartmentService + from employees.ldap.services.department_service import \ + DepartmentService dept_svc = DepartmentService() - move_to_department_dn = dept_svc._get_department_dn(dept) + move_to_department_dn = dept_svc._get_department_dn( + dept) except Exception as e: - logger.warning(f"Failed to resolve department DN: {e}") + logger.warning( + f"Failed to resolve department DN: {e}") # Группы - берем готовый список CN если передан group_cns = request_data.get('group_cns') diff --git a/backend/docs/testing/LDAP_TESTING.md b/backend/docs/testing/LDAP_TESTING.md index f75e0fb4..d181e875 100644 --- a/backend/docs/testing/LDAP_TESTING.md +++ b/backend/docs/testing/LDAP_TESTING.md @@ -186,7 +186,7 @@ def test_employee_creation(api_client, with_ldap, settings): settings.LDAP_ENABLED = True else: settings.LDAP_ENABLED = False - + # Тест проверяет оба сценария # ... ``` @@ -238,7 +238,7 @@ steps: LDAP_ENABLED: true LDAP_URI: ldap://localhost:389 run: pytest tests/api/v1/employees/ -m ldap_required - + - name: Run tests without LDAP env: LDAP_ENABLED: false diff --git a/backend/employees/ldap/services/user_service.py b/backend/employees/ldap/services/user_service.py index 19c6d9ec..ece23e15 100644 --- a/backend/employees/ldap/services/user_service.py +++ b/backend/employees/ldap/services/user_service.py @@ -6,6 +6,7 @@ from __future__ import annotations +import logging from typing import Any, Dict, Iterable, List, Optional from django.conf import settings @@ -13,6 +14,8 @@ from django.db import transaction from django.utils import timezone from ldap3 import MODIFY_REPLACE, Connection +logger = logging.getLogger(__name__) + from ...models import Employee, LdapSyncState, Position from ..domain.dtos import DirectoryUserDTO from ..errors import (DirectoryDbError, DirectoryLdapError, diff --git a/backend/eusrr_backend/settings.py b/backend/eusrr_backend/settings.py index 6182e10e..7f59b2ed 100644 --- a/backend/eusrr_backend/settings.py +++ b/backend/eusrr_backend/settings.py @@ -437,7 +437,7 @@ LDAP_AUTO_CREATE = os.getenv("LDAP_AUTO_CREATE", "False") LDAP_RESPECT_IS_ACTIVE = True LDAP_RESPECT_AD_DISABLED = True LDAP_REGISTRATION_CREATE = True -LDAP_POSITIONS_BASE = os.getenv("LDAP_POSITIONS_BASE") +LDAP_POSITIONS_BASE = os.getenv("LDAP_POSITIONS_BASE", "OU=Positions,OU=company,DC=robotail,DC=local") diff --git a/backend/pytest_current.txt b/backend/pytest_current.txt new file mode 100644 index 00000000..0a2108a6 --- /dev/null +++ b/backend/pytest_current.txt @@ -0,0 +1,1385 @@ +============================= test session starts ============================= +platform win32 -- Python 3.12.6, pytest-8.4.1, pluggy-1.6.0 -- C:\Users\igor_\Dev\EUSRR\.venv\Scripts\python.exe +cachedir: .pytest_cache +django: version: 5.2.4, settings: eusrr_backend.settings (from ini) +rootdir: C:\Users\igor_\Dev\EUSRR\backend +configfile: pytest.ini +plugins: anyio-4.12.0, asyncio-1.3.0, django-4.11.1 +asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function +collecting ... collected 150 items + +tests/api/v1/employees/test_department_head_rights.py::test_head_can_update_department_without_explicit_role_perms FAILED [ 0%] +tests/api/v1/employees/test_department_head_rights.py::test_head_can_set_member_role_and_non_head_cannot FAILED [ 1%] +tests/api/v1/employees/test_department_head_rights.py::test_head_can_change_head_and_loses_rights_afterwards FAILED [ 2%] +tests/api/v1/employees/test_department_membership_separation.py::test_add_member_requires_manage_and_does_not_assign_role FAILED [ 2%] +tests/api/v1/employees/test_department_membership_separation.py::test_all_three_actions_require_auth_and_permissions FAILED [ 3%] +tests/api/v1/employees/test_department_membership_separation.py::test_set_member_role_requires_assign_not_manage FAILED [ 4%] +tests/api/v1/employees/test_department_membership_separation.py::test_set_member_role_does_not_create_membership_and_does_not_toggle_active FAILED [ 4%] +tests/api/v1/employees/test_department_membership_separation.py::test_add_member_ignores_role_parameter FAILED [ 5%] +tests/api/v1/employees/test_department_membership_separation.py::test_remove_member_requires_manage_and_does_not_touch_role FAILED [ 6%] +tests/api/v1/employees/test_department_roles.py::test_list_filtered_by_department FAILED [ 6%] +tests/api/v1/employees/test_department_roles.py::test_create_requires_assign_department_role FAILED [ 7%] +tests/api/v1/employees/test_department_roles.py::test_update_and_destroy_scope_enforced FAILED [ 8%] +tests/api/v1/employees/test_department_roles.py::test_perm_choices_and_perms_and_set_perms FAILED [ 8%] +tests/api/v1/employees/test_department_roles.py::test_set_perms_validates_payload FAILED [ 9%] +tests/api/v1/employees/test_department_roles.py::test_back_compat_permissions_field_contains_ids FAILED [ 10%] +tests/api/v1/employees/test_department_roles.py::test_create_with_scoped_permission_codes_and_update_name FAILED [ 10%] +tests/api/v1/employees/test_department_roles.py::test_cross_department_access_denied_for_set_perms FAILED [ 11%] +tests/api/v1/employees/test_department_roles_extra.py::test_unauth_cannot_list_or_get PASSED [ 12%] +tests/api/v1/employees/test_department_roles_extra.py::test_read_only_allowed_without_assign FAILED [ 12%] +tests/api/v1/employees/test_department_roles_extra.py::test_staff_override_for_write FAILED [ 13%] +tests/api/v1/employees/test_department_roles_extra.py::test_ordering_stable_by_name_then_id FAILED [ 14%] +tests/api/v1/employees/test_department_roles_extra.py::test_set_perms_ids_take_precedence_over_codes FAILED [ 14%] +tests/api/v1/employees/test_department_roles_extra.py::test_serializer_codes_override_scoped_permissions_on_create FAILED [ 15%] +tests/api/v1/employees/test_department_roles_extra.py::test_perm_choices_fields_and_nonempty FAILED [ 16%] +tests/api/v1/employees/test_department_roles_extra.py::test_destroy_requires_assign_in_same_department FAILED [ 16%] +tests/api/v1/employees/test_department_roles_extra.py::test_set_perms_dedup_and_replace_semantics FAILED [ 17%] +tests/api/v1/employees/test_departments.py::test_list_requires_auth PASSED [ 18%] +tests/api/v1/employees/test_departments.py::test_list_ok_for_authenticated FAILED [ 18%] +tests/api/v1/employees/test_departments.py::test_search_and_ordering FAILED [ 19%] +tests/api/v1/employees/test_departments.py::test_employees_count_adds_head_if_not_in_links FAILED [ 20%] +tests/api/v1/employees/test_departments.py::test_employees_count_not_double_count_head_if_in_links FAILED [ 20%] +tests/api/v1/employees/test_departments.py::test_create_requires_staff FAILED [ 21%] +tests/api/v1/employees/test_departments.py::test_destroy_requires_staff FAILED [ 22%] +tests/api/v1/employees/test_departments.py::test_partial_update_name_requires_manage_perm FAILED [ 22%] +tests/api/v1/employees/test_departments.py::test_partial_update_head_requires_change_head_perm FAILED [ 23%] +tests/api/v1/employees/test_departments.py::test_partial_update_head_same_value_does_not_require_extra_perm FAILED [ 24%] +tests/api/v1/employees/test_departments.py::test_set_head_by_role_with_perm FAILED [ 24%] +tests/api/v1/employees/test_departments.py::test_set_head_validation_inactive_employee FAILED [ 25%] +tests/api/v1/employees/test_departments.py::test_set_head_remove_with_null FAILED [ 26%] +tests/api/v1/employees/test_departments.py::test_set_head_requires_perm FAILED [ 26%] +tests/api/v1/employees/test_departments.py::test_set_member_role_happy_path FAILED [ 27%] +tests/api/v1/employees/test_departments.py::test_set_member_role_reject_foreign_role FAILED [ 28%] +tests/api/v1/employees/test_departments.py::test_set_member_role_requires_perm FAILED [ 28%] +tests/api/v1/employees/test_departments.py::test_set_member_role_missing_employee_id FAILED [ 29%] +tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_resets_email_verified_for_self FAILED [ 30%] +tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_via_me_endpoint_resets_verification FAILED [ 30%] +tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_sends_verification_code FAILED [ 31%] +tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_other_fields_does_not_reset_verification FAILED [ 32%] +tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_to_same_value_does_not_reset FAILED [ 32%] +tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_cannot_create_department SKIPPED [ 33%] +tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_cannot_update_other_employee SKIPPED [ 34%] +tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_can_update_self_non_critical_fields FAILED [ 34%] +tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_can_verify_email FAILED [ 35%] +tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_can_resend_code FAILED [ 36%] +tests/api/v1/employees/test_email_verification_security.py::TestResendEmailLogic::test_resend_email_generates_new_code FAILED [ 36%] +tests/api/v1/employees/test_email_verification_security.py::TestResendEmailLogic::test_resend_email_to_verified_user_fails FAILED [ 37%] +tests/api/v1/employees/test_email_verification_security.py::TestResendEmailLogic::test_resend_email_to_nonexistent_user_fails PASSED [ 38%] +tests/api/v1/employees/test_email_verification_security.py::TestResendEmailLogic::test_old_code_becomes_invalid_after_resend FAILED [ 38%] +tests/api/v1/employees/test_email_verification_security.py::TestStaffCanBypassRestrictions::test_admin_created_user_is_verified_by_default SKIPPED [ 39%] +tests/api/v1/employees/test_email_verification_security.py::TestStaffCanBypassRestrictions::test_superuser_is_verified_by_default PASSED [ 40%] +tests/api/v1/employees/test_email_verification_security.py::TestEmailVerificationWorkflow::test_full_registration_and_verification_flow FAILED [ 40%] +tests/api/v1/employees/test_email_verification_security.py::TestEmailVerificationWorkflow::test_change_email_and_verify_new_one FAILED [ 41%] +tests/api/v1/employees/test_employee_actions.py::test_list_requires_auth_and_filtering PASSED [ 42%] +tests/api/v1/employees/test_employee_actions.py::test_create_requires_perm_or_staff PASSED [ 42%] +tests/api/v1/employees/test_employee_actions.py::test_dismissal_deactivates_employee_and_links PASSED [ 43%] +tests/api/v1/employees/test_employee_actions.py::test_non_dismissal_activates_employee PASSED [ 44%] +tests/api/v1/employees/test_employee_actions.py::test_update_to_dismissed_applies_effects PASSED [ 44%] +tests/api/v1/employees/test_employee_actions.py::test_delete_requires_perm_or_staff PASSED [ 45%] +tests/api/v1/employees/test_employee_actions.py::test_dismissal_syncs_to_ldap_if_has_dn PASSED [ 46%] +tests/api/v1/employees/test_employee_actions.py::test_dismissal_handles_ldap_error_gracefully PASSED [ 46%] +tests/api/v1/employees/test_employee_actions.py::test_non_dismissal_syncs_activation_to_ldap PASSED [ 47%] +tests/api/v1/employees/test_employee_actions.py::test_dismissal_skips_ldap_if_no_dn PASSED [ 48%] +tests/api/v1/employees/test_employee_actions.py::test_dismissal_works_when_ldap_disabled FAILED [ 48%] +tests/api/v1/employees/test_employee_actions.py::test_dismissal_moves_to_dismissed_ou PASSED [ 49%] +tests/api/v1/employees/test_employee_actions.py::test_restoration_moves_from_dismissed_to_users FAILED [ 50%] +tests/api/v1/employees/test_employee_actions.py::test_dismissal_without_department_moves_to_dismissed PASSED [ 50%] +tests/api/v1/employees/test_employees.py::test_list_requires_auth PASSED [ 51%] +tests/api/v1/employees/test_employees.py::test_list_ok_for_authenticated PASSED [ 52%] +tests/api/v1/employees/test_employees.py::test_retrieve_requires_auth PASSED [ 52%] +tests/api/v1/employees/test_employees.py::test_retrieve_ok PASSED [ 53%] +tests/api/v1/employees/test_employees.py::test_search_by_email_and_last_name PASSED [ 54%] +tests/api/v1/employees/test_employees.py::test_filter_by_department_includes_members_and_head PASSED [ 54%] +tests/api/v1/employees/test_employees.py::test_filter_by_position PASSED [ 55%] +tests/api/v1/employees/test_employees.py::test_filter_by_skills_any_of PASSED [ 56%] +tests/api/v1/employees/test_employees.py::test_filter_by_email_verified_and_active PASSED [ 56%] +tests/api/v1/employees/test_employees.py::test_filter_actually_active_logic PASSED [ 57%] +tests/api/v1/employees/test_employees.py::test_create_requires_staff FAILED [ 58%] +tests/api/v1/employees/test_employees.py::test_partial_update_self_allowed PASSED [ 58%] +tests/api/v1/employees/test_employees.py::test_partial_update_other_forbidden_for_regular_user PASSED [ 59%] +tests/api/v1/employees/test_employees.py::test_partial_update_other_allowed_for_staff PASSED [ 60%] +tests/api/v1/employees/test_employees.py::test_destroy_requires_admin FAILED [ 60%] +tests/api/v1/employees/test_employees.py::test_me_get_returns_current_user PASSED [ 61%] +tests/api/v1/employees/test_employees.py::test_me_patch_updates_profile PASSED [ 62%] +tests/api/v1/employees/test_employees.py::test_me_patch_rejects_when_all_contacts_empty FAILED [ 62%] +tests/api/v1/employees/test_employees_fields_in_list.py::test_list_includes_basic_fields FAILED [ 63%] +tests/api/v1/employees/test_employees_fields_in_list.py::test_list_departments_include_role FAILED [ 64%] +tests/api/v1/employees/test_ldap_optional_departments.py::test_department_list_accessible_without_ldap PASSED [ 64%] +tests/api/v1/employees/test_ldap_optional_departments.py::test_retrieve_department_without_ldap PASSED [ 65%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_create_group_with_ldap SKIPPED [ 66%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_create_group_without_ldap ERROR [ 66%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_add_members_with_ldap SKIPPED [ 67%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_add_members_without_ldap ERROR [ 68%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_destroy_group_with_ldap SKIPPED [ 68%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_destroy_group_without_ldap ERROR [ 69%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_get_members_with_ldap SKIPPED [ 70%] +tests/api/v1/employees/test_ldap_optional_groups.py::test_get_members_without_ldap ERROR [ 70%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_is_ldap_enabled_returns_true_when_enabled PASSED [ 71%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_is_ldap_enabled_returns_false_when_disabled PASSED [ 72%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_is_ldap_enabled_returns_false_when_not_set PASSED [ 72%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_ldap_try_success_with_ldap_enabled PASSED [ 73%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_ldap_try_ldap_error_returns_502 PASSED [ 74%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_ldap_try_skips_when_ldap_disabled PASSED [ 74%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_ldap_try_service_error_returns_502 PASSED [ 75%] +tests/api/v1/employees/test_ldap_optional_helpers.py::test_ldap_try_db_error_returns_502 PASSED [ 76%] +tests/api/v1/employees/test_ldap_optional_register.py::test_register_with_ldap_creates_user_in_ldap_and_db SKIPPED [ 76%] +tests/api/v1/employees/test_ldap_optional_register.py::test_register_with_ldap_duplicate_email_returns_400 SKIPPED [ 77%] +tests/api/v1/employees/test_ldap_optional_register.py::test_register_without_ldap_creates_user_only_in_db FAILED [ 78%] +tests/api/v1/employees/test_ldap_optional_register.py::test_register_without_ldap_duplicate_email_returns_400 PASSED [ 78%] +tests/api/v1/employees/test_ldap_optional_register.py::test_register_validates_required_fields[with_ldap] SKIPPED [ 79%] +tests/api/v1/employees/test_ldap_optional_register.py::test_register_validates_required_fields[without_ldap] SKIPPED [ 80%] +tests/api/v1/employees/test_ldap_optional_verify_email.py::test_verify_email_with_ldap_activates_user_in_ldap_and_db SKIPPED [ 80%] +tests/api/v1/employees/test_ldap_optional_verify_email.py::test_verify_email_without_ldap_activates_user_only_in_db PASSED [ 81%] +tests/api/v1/employees/test_ldap_optional_verify_email.py::test_verify_email_with_wrong_code_returns_400_without_ldap PASSED [ 82%] +tests/api/v1/employees/test_ldap_optional_verify_email.py::test_verify_email_already_verified_returns_400_without_ldap PASSED [ 82%] +tests/api/v1/employees/test_ldap_optional_verify_email.py::test_verify_email_nonexistent_user_returns_404_without_ldap PASSED [ 83%] +tests/api/v1/employees/test_ldap_optional_verify_email.py::test_verify_email_empty_code_returns_400_without_ldap PASSED [ 84%] +tests/api/v1/employees/test_ldap_optional_verify_email.py::test_verify_email_ldap_error_returns_502 SKIPPED [ 84%] +tests/api/v1/employees/test_positions.py::test_list_requires_auth PASSED [ 85%] +tests/api/v1/employees/test_positions.py::test_create_permissions FAILED [ 86%] +tests/api/v1/employees/test_positions.py::test_update_permissions_and_search_ordering FAILED [ 86%] +tests/api/v1/employees/test_positions.py::test_delete_permissions PASSED [ 87%] +tests/api/v1/employees/test_positions.py::test_set_groups_requires_special_perm FAILED [ 88%] +tests/api/v1/employees/test_positions.py::test_add_and_remove_groups_and_permissions_endpoint FAILED [ 88%] +tests/api/v1/employees/test_positions.py::test_groups_payload_validation PASSED [ 89%] +tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assign_role_to_any_employee FAILED [ 90%] +tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_revoke_role FAILED [ 90%] +tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assignments_list FAILED [ 91%] +tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assign_requires_permission FAILED [ 92%] +tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assign_with_dept_permission FAILED [ 92%] +tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentPermissions::test_permission_check_via_role_assignment FAILED [ 93%] +tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentPermissions::test_inactive_assignment_does_not_grant_permission FAILED [ 94%] +tests/api/v1/employees/test_skills.py::test_list_search_ordering_requires_auth PASSED [ 94%] +tests/api/v1/employees/test_skills.py::test_create_allowed_for_regular_user PASSED [ 95%] +tests/api/v1/employees/test_skills.py::test_update_delete_permissions PASSED [ 96%] +tests/api/v1/employees/test_skills.py::test_staff_bypasses_permissions PASSED [ 96%] +tests/api/v1/employees/test_skills.py::test_bulk_create_by_regular_user PASSED [ 97%] +tests/api/v1/employees/test_skills.py::test_merge_requires_perm_and_reassigns PASSED [ 98%] +tests/api/v1/employees/test_skills.py::test_merge_same_id_and_invalid_ids PASSED [ 98%] +tests/api/v1/employees/test_skills.py::test_merge_without_reassign_or_delete PASSED [ 99%] +tests/api/v1/employees/test_skills.py::test_merge_idempotent_if_employee_already_has_target PASSED [100%] + +=================================== ERRORS ==================================== +______________ ERROR at setup of test_create_group_without_ldap _______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________ ERROR at setup of test_add_members_without_ldap _______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +______________ ERROR at setup of test_destroy_group_without_ldap ______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________ ERROR at setup of test_get_members_without_ldap _______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +================================== FAILURES =================================== +_________ test_head_can_update_department_without_explicit_role_perms _________ +employees\ldap\services\department_service.py:136: in update_department + current_dn = self._get_department_dn(dept) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\department_service.py:981: in _get_department_dn + raise DirectoryServiceError( +E employees.ldap.errors.DirectoryServiceError: Department has no ldap_dn in sync state + +The above exception was the direct cause of the following exception: +tests\api\v1\employees\test_department_head_rights.py:97: in test_head_can_update_department_without_explicit_role_perms + resp = api_client.patch(url, {"description": "new"}, format="json") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:315: in patch + response = super().patch( +..\.venv\Lib\site-packages\rest_framework\test.py:221: in patch + return self.generic('PATCH', path, data, content_type, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:237: in generic + return super().generic( +..\.venv\Lib\site-packages\django\test\client.py:671: in generic + return self.request(**r) + ^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:289: in request + return super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:241: in request + request = super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\test\client.py:1087: in request + self.check_exception(response) +..\.venv\Lib\site-packages\django\test\client.py:802: in check_exception + raise exc_value +..\.venv\Lib\site-packages\django\core\handlers\exception.py:55: in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\core\handlers\base.py:197: in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\views\decorators\csrf.py:65: in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\viewsets.py:125: in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:515: in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:475: in handle_exception + self.raise_uncaught_exception(exc) +..\.venv\Lib\site-packages\rest_framework\views.py:486: in raise_uncaught_exception + raise exc +..\.venv\Lib\site-packages\rest_framework\views.py:512: in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +api\v1\employees\views\departments.py:269: in partial_update + instance = svc.update_department(instance, changes) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\directory_service.py:218: in update_department + return self._department_service.update_department(dept, changes) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\department_service.py:138: in update_department + raise DirectoryServiceError( +E employees.ldap.errors.DirectoryServiceError: Íå íàéäåí DN îòäåëà (sync state): Department has no ldap_dn in sync state +---------------------------- Captured stderr setup ---------------------------- +Creating test database for alias 'default'... +---------------------------- Captured stderr call ----------------------------- +[ERROR] 2026-02-16 17:34:50,458 django.request:253 Internal Server Error: /api/v1/departments/1/ +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\department_service.py", line 136, in update_department + current_dn = self._get_department_dn(dept) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\department_service.py", line 981, in _get_department_dn + raise DirectoryServiceError( +employees.ldap.errors.DirectoryServiceError: Department has no ldap_dn in sync state + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\views\decorators\csrf.py", line 65, in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\viewsets.py", line 125, in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 515, in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 475, in handle_exception + self.raise_uncaught_exception(exc) + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 486, in raise_uncaught_exception + raise exc + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 512, in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\departments.py", line 269, in partial_update + instance = svc.update_department(instance, changes) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\directory_service.py", line 218, in update_department + return self._department_service.update_department(dept, changes) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\department_service.py", line 138, in update_department + raise DirectoryServiceError( +employees.ldap.errors.DirectoryServiceError: Íå íàéäåí DN îòäåëà (sync state): Department has no ldap_dn in sync state +______________ test_head_can_set_member_role_and_non_head_cannot ______________ +tests\api\v1\employees\test_department_head_rights.py:118: in test_head_can_set_member_role_and_non_head_cannot + assert resp.status_code == status.HTTP_200_OK +E assert 502 == 200 +E + where 502 = .status_code +E + and 200 = status.HTTP_200_OK +---------------------------- Captured stderr call ----------------------------- +[ERROR] 2026-02-16 17:34:50,809 django.request:253 Bad Gateway: /api/v1/departments/2/set_member_role/ +____________ test_head_can_change_head_and_loses_rights_afterwards ____________ +tests\api\v1\employees\test_department_head_rights.py:143: in test_head_can_change_head_and_loses_rights_afterwards + assert resp.status_code == status.HTTP_200_OK +E assert 400 == 200 +E + where 400 = .status_code +E + and 200 = status.HTTP_200_OK +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:50,890 django.request:253 Bad Request: /api/v1/departments/3/set_head/ +__________ test_add_member_requires_manage_and_does_not_assign_role ___________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_____________ test_all_three_actions_require_auth_and_permissions _____________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________ test_set_member_role_requires_assign_not_manage _______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ test_set_member_role_does_not_create_membership_and_does_not_toggle_active __ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +___________________ test_add_member_ignores_role_parameter ____________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________ test_remove_member_requires_manage_and_does_not_touch_role __________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +______________________ test_list_filtered_by_department _______________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________________ test_create_requires_assign_department_role _________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +___________________ test_update_and_destroy_scope_enforced ____________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +__________________ test_perm_choices_and_perms_and_set_perms __________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +______________________ test_set_perms_validates_payload _______________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________ test_back_compat_permissions_field_contains_ids _______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +__________ test_create_with_scoped_permission_codes_and_update_name ___________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +______________ test_cross_department_access_denied_for_set_perms ______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +____________________ test_read_only_allowed_without_assign ____________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +________________________ test_staff_override_for_write ________________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +____________________ test_ordering_stable_by_name_then_id _____________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +________________ test_set_perms_ids_take_precedence_over_codes ________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________ test_serializer_codes_override_scoped_permissions_on_create _________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +____________________ test_perm_choices_fields_and_nonempty ____________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________ test_destroy_requires_assign_in_same_department _______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________________ test_set_perms_dedup_and_replace_semantics __________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________________ test_list_ok_for_authenticated ________________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +__________________________ test_search_and_ordering ___________________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________ test_employees_count_adds_head_if_not_in_links ________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +___________ test_employees_count_not_double_count_head_if_in_links ____________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________________________ test_create_requires_staff __________________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________________________ test_destroy_requires_staff _________________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +________________ test_partial_update_name_requires_manage_perm ________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_____________ test_partial_update_head_requires_change_head_perm ______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______ test_partial_update_head_same_value_does_not_require_extra_perm _______ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________________ test_set_head_by_role_with_perm _______________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________________ test_set_head_validation_inactive_employee __________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________________ test_set_head_remove_with_null ________________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________________________ test_set_head_requires_perm _________________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______________________ test_set_member_role_happy_path _______________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +__________________ test_set_member_role_reject_foreign_role ___________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_____________________ test_set_member_role_requires_perm ______________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +__________________ test_set_member_role_missing_employee_id ___________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestEmailChangeResetsVerification.test_change_email_resets_email_verified_for_self _ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestEmailChangeResetsVerification.test_change_email_via_me_endpoint_resets_verification _ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestEmailChangeResetsVerification.test_change_email_sends_verification_code _ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestEmailChangeResetsVerification.test_change_other_fields_does_not_reset_verification _ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestEmailChangeResetsVerification.test_change_email_to_same_value_does_not_reset _ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestUnverifiedUserRestrictions.test_unverified_user_can_update_self_non_critical_fields _ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +____ TestUnverifiedUserRestrictions.test_unverified_user_can_verify_email _____ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_____ TestUnverifiedUserRestrictions.test_unverified_user_can_resend_code _____ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +__________ TestResendEmailLogic.test_resend_email_generates_new_code __________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +________ TestResendEmailLogic.test_resend_email_to_verified_user_fails ________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_______ TestResendEmailLogic.test_old_code_becomes_invalid_after_resend _______ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestEmailVerificationWorkflow.test_full_registration_and_verification_flow __ +tests\api\v1\employees\test_email_verification_security.py:430: in test_full_registration_and_verification_flow + assert response.status_code in [ +E assert 400 in [201, 502] +E + where 400 = .status_code +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:51,498 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views.auth:auth.py:161 [REGISTER] Received data: {'email': 'newuser@test.com', 'password': 'TestPass123!', 'password_confirmation': 'TestPass123!', 'first_name': 'New', 'last_name': 'User', 'telegram': '@newuser', 'birth_date': '1990-01-01', 'phone_number': '+79001234569'} +WARNING api.v1.employees.views.auth:auth.py:162 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views.auth:auth.py:180 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +_____ TestEmailVerificationWorkflow.test_change_email_and_verify_new_one ______ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +___________________ test_dismissal_works_when_ldap_disabled ___________________ +tests\api\v1\employees\test_employee_actions.py:430: in test_dismissal_works_when_ldap_disabled + mock_update.assert_not_called() +..\..\..\AppData\Local\Programs\Python\Python312\Lib\unittest\mock.py:908: in assert_not_called + raise AssertionError(msg) +E AssertionError: Expected 'update_user' to not have been called. Called 1 times. +E Calls: [call(, changes={'is_active': False})]. +------------------------------ Captured log call ------------------------------ +ERROR api.v1.employees.views.actions:actions.py:154 Failed to move dismissed employee: LDAP move user to base failed: {'result': 53, 'description': 'unwillingToPerform', 'dn': '', 'message': 'no global superior knowledge', 'referrals': None, 'type': 'modDNResponse'} +_______________ test_restoration_moves_from_dismissed_to_users ________________ +tests\api\v1\employees\test_employee_actions.py:515: in test_restoration_moves_from_dismissed_to_users + emp = _user(is_active=False) # Óâîëåííûé ñîòðóäíèê + ^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: _user() got an unexpected keyword argument 'is_active' +_________________________ test_create_requires_staff __________________________ +tests\api\v1\employees\test_employees.py:286: in test_create_requires_staff + assert resp.status_code == status.HTTP_400_BAD_REQUEST +E assert 201 == 400 +E + where 201 = .status_code +E + and 400 = status.HTTP_400_BAD_REQUEST +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:53,338 django.request:253 Forbidden: /api/v1/employees/ +_________________________ test_destroy_requires_admin _________________________ +employees\ldap\services\user_service.py:190: in delete_user_in_ldap_only + dn = self._get_employee_dn(instance) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\user_service.py:855: in _get_employee_dn + raise DirectoryServiceError("Employee has no ldap_dn") +E employees.ldap.errors.DirectoryServiceError: Employee has no ldap_dn + +During handling of the above exception, another exception occurred: +common\external_sync_mixin.py:188: in _perform_with_sync_instance + sync_method(**external_data) +employees\ldap\directory_service.py:133: in delete_user_in_ldap_only + return self._user_service.delete_user_in_ldap_only(instance) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\user_service.py:193: in delete_user_in_ldap_only + logger.warning( + ^^^^^^ +E NameError: name 'logger' is not defined + +During handling of the above exception, another exception occurred: +tests\api\v1\employees\test_employees.py:346: in test_destroy_requires_admin + resp = api_client.delete(url) + ^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:323: in delete + response = super().delete( +..\.venv\Lib\site-packages\rest_framework\test.py:225: in delete + return self.generic('DELETE', path, data, content_type, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:237: in generic + return super().generic( +..\.venv\Lib\site-packages\django\test\client.py:671: in generic + return self.request(**r) + ^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:289: in request + return super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:241: in request + request = super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\test\client.py:1087: in request + self.check_exception(response) +..\.venv\Lib\site-packages\django\test\client.py:802: in check_exception + raise exc_value +..\.venv\Lib\site-packages\django\core\handlers\exception.py:55: in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\core\handlers\base.py:197: in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\views\decorators\csrf.py:65: in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\viewsets.py:125: in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:515: in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:475: in handle_exception + self.raise_uncaught_exception(exc) +..\.venv\Lib\site-packages\rest_framework\views.py:486: in raise_uncaught_exception + raise exc +..\.venv\Lib\site-packages\rest_framework\views.py:512: in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\mixins.py:91: in destroy + self.perform_destroy(instance) +common\external_sync_mixin.py:103: in perform_destroy + return self._perform_with_sync_instance(instance, 'destroy', super().perform_destroy) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +common\external_sync_mixin.py:201: in _perform_with_sync_instance + raise ExternalSyncError(error_response) +E common.external_sync_mixin.ExternalSyncError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:53,602 django.request:253 Forbidden: /api/v1/employees/74/ +[ERROR] 2026-02-16 17:34:53,681 common.external_sync_mixin:195 External sync failed, rolling back: name 'logger' is not defined +[ERROR] 2026-02-16 17:34:53,684 django.request:253 Internal Server Error: /api/v1/employees/74/ +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\user_service.py", line 190, in delete_user_in_ldap_only + dn = self._get_employee_dn(instance) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\user_service.py", line 855, in _get_employee_dn + raise DirectoryServiceError("Employee has no ldap_dn") +employees.ldap.errors.DirectoryServiceError: Employee has no ldap_dn + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\backend\common\external_sync_mixin.py", line 188, in _perform_with_sync_instance + sync_method(**external_data) + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\directory_service.py", line 133, in delete_user_in_ldap_only + return self._user_service.delete_user_in_ldap_only(instance) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\user_service.py", line 193, in delete_user_in_ldap_only + logger.warning( + ^^^^^^ +NameError: name 'logger' is not defined + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\views\decorators\csrf.py", line 65, in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\viewsets.py", line 125, in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 515, in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 475, in handle_exception + self.raise_uncaught_exception(exc) + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 486, in raise_uncaught_exception + raise exc + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 512, in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\mixins.py", line 91, in destroy + self.perform_destroy(instance) + File "C:\Users\igor_\Dev\EUSRR\backend\common\external_sync_mixin.py", line 103, in perform_destroy + return self._perform_with_sync_instance(instance, 'destroy', super().perform_destroy) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\common\external_sync_mixin.py", line 201, in _perform_with_sync_instance + raise ExternalSyncError(error_response) +common.external_sync_mixin.ExternalSyncError +------------------------------ Captured log call ------------------------------ +ERROR api.v1.employees.views.employees:employees.py:193 LDAP sync failed for destroy on Employee None: name 'logger' is not defined +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\user_service.py", line 190, in delete_user_in_ldap_only + dn = self._get_employee_dn(instance) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\user_service.py", line 855, in _get_employee_dn + raise DirectoryServiceError("Employee has no ldap_dn") +employees.ldap.errors.DirectoryServiceError: Employee has no ldap_dn + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\backend\common\external_sync_mixin.py", line 188, in _perform_with_sync_instance + sync_method(**external_data) + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\directory_service.py", line 133, in delete_user_in_ldap_only + return self._user_service.delete_user_in_ldap_only(instance) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\user_service.py", line 193, in delete_user_in_ldap_only + logger.warning( + ^^^^^^ +NameError: name 'logger' is not defined +________________ test_me_patch_rejects_when_all_contacts_empty ________________ +tests\api\v1\employees\test_employees.py:385: in test_me_patch_rejects_when_all_contacts_empty + assert r.status_code == status.HTTP_400_BAD_REQUEST +E assert 200 == 400 +E + where 200 = .status_code +E + and 400 = status.HTTP_400_BAD_REQUEST +_______________________ test_list_includes_basic_fields _______________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_____________________ test_list_departments_include_role ______________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_____________ test_register_without_ldap_creates_user_only_in_db ______________ +tests\api\v1\employees\test_ldap_optional_register.py:120: in test_register_without_ldap_creates_user_only_in_db + assert response.status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + and 201 = status.HTTP_201_CREATED +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:54,064 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views.auth:auth.py:161 [REGISTER] Received data: {'email': 'test4000@example.com', 'first_name': 'Test', 'last_name': 'User', 'phone_number': '+79990001000', 'password': 'SecurePass123!', 'whatsapp': '+79990001001', 'birth_date': '1990-01-01'} +WARNING api.v1.employees.views.auth:auth.py:162 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views.auth:auth.py:180 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +___________________________ test_create_permissions ___________________________ +tests\api\v1\employees\test_positions.py:83: in test_create_permissions + resp = api_client.post(url, {"name": "NewPos"}, format="json") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:299: in post + response = super().post( +..\.venv\Lib\site-packages\rest_framework\test.py:213: in post + return self.generic('POST', path, data, content_type, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:237: in generic + return super().generic( +..\.venv\Lib\site-packages\django\test\client.py:671: in generic + return self.request(**r) + ^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:289: in request + return super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:241: in request + request = super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\test\client.py:1087: in request + self.check_exception(response) +..\.venv\Lib\site-packages\django\test\client.py:802: in check_exception + raise exc_value +..\.venv\Lib\site-packages\django\core\handlers\exception.py:55: in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\core\handlers\base.py:197: in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\views\decorators\csrf.py:65: in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\viewsets.py:125: in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:515: in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:475: in handle_exception + self.raise_uncaught_exception(exc) +..\.venv\Lib\site-packages\rest_framework\views.py:486: in raise_uncaught_exception + raise exc +..\.venv\Lib\site-packages\rest_framework\views.py:512: in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\mixins.py:19: in create + self.perform_create(serializer) +api\v1\employees\views\positions.py:136: in perform_create + err = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +api\v1\employees\views\_helpers.py:39: in _ldap_try + fn() +api\v1\employees\views\positions.py:136: in + err = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\directory_service.py:373: in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:72: in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:358: in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:212: in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:170: in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +E RuntimeError: LDAP_POSITIONS_BASE is not configured +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:54,287 django.request:253 Forbidden: /api/v1/positions/ +[ERROR] 2026-02-16 17:34:54,312 django.request:253 Internal Server Error: /api/v1/positions/ +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\views\decorators\csrf.py", line 65, in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\viewsets.py", line 125, in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 515, in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 475, in handle_exception + self.raise_uncaught_exception(exc) + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 486, in raise_uncaught_exception + raise exc + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 512, in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\mixins.py", line 19, in create + self.perform_create(serializer) + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 136, in perform_create + err = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\_helpers.py", line 39, in _ldap_try + fn() + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 136, in + err = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\directory_service.py", line 373, in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 72, in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 358, in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 212, in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 170, in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +RuntimeError: LDAP_POSITIONS_BASE is not configured +_________________ test_update_permissions_and_search_ordering _________________ +tests\api\v1\employees\test_positions.py:109: in test_update_permissions_and_search_ordering + resp = api_client.patch(url_detail, {"description": "updated"}, format="json") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:315: in patch + response = super().patch( +..\.venv\Lib\site-packages\rest_framework\test.py:221: in patch + return self.generic('PATCH', path, data, content_type, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:237: in generic + return super().generic( +..\.venv\Lib\site-packages\django\test\client.py:671: in generic + return self.request(**r) + ^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:289: in request + return super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:241: in request + request = super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\test\client.py:1087: in request + self.check_exception(response) +..\.venv\Lib\site-packages\django\test\client.py:802: in check_exception + raise exc_value +..\.venv\Lib\site-packages\django\core\handlers\exception.py:55: in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\core\handlers\base.py:197: in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\views\decorators\csrf.py:65: in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\viewsets.py:125: in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:515: in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:475: in handle_exception + self.raise_uncaught_exception(exc) +..\.venv\Lib\site-packages\rest_framework\views.py:486: in raise_uncaught_exception + raise exc +..\.venv\Lib\site-packages\rest_framework\views.py:512: in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\mixins.py:82: in partial_update + return self.update(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\mixins.py:68: in update + self.perform_update(serializer) +api\v1\employees\views\positions.py:142: in perform_update + _ldap_try(lambda: DirectoryService().reconcile_position(pos)) +api\v1\employees\views\_helpers.py:39: in _ldap_try + fn() +api\v1\employees\views\positions.py:142: in + _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\directory_service.py:373: in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:72: in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:358: in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:212: in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:170: in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +E RuntimeError: LDAP_POSITIONS_BASE is not configured +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:54,410 django.request:253 Forbidden: /api/v1/positions/6/ +[ERROR] 2026-02-16 17:34:54,435 django.request:253 Internal Server Error: /api/v1/positions/6/ +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\views\decorators\csrf.py", line 65, in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\viewsets.py", line 125, in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 515, in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 475, in handle_exception + self.raise_uncaught_exception(exc) + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 486, in raise_uncaught_exception + raise exc + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 512, in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\mixins.py", line 82, in partial_update + return self.update(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\mixins.py", line 68, in update + self.perform_update(serializer) + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 142, in perform_update + _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\_helpers.py", line 39, in _ldap_try + fn() + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 142, in + _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\directory_service.py", line 373, in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 72, in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 358, in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 212, in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 170, in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +RuntimeError: LDAP_POSITIONS_BASE is not configured +____________________ test_set_groups_requires_special_perm ____________________ +tests\api\v1\employees\test_positions.py:163: in test_set_groups_requires_special_perm + resp = api_client.post(url_set, {"groups": [grp1.id, grp2.id]}, format="json") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:299: in post + response = super().post( +..\.venv\Lib\site-packages\rest_framework\test.py:213: in post + return self.generic('POST', path, data, content_type, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:237: in generic + return super().generic( +..\.venv\Lib\site-packages\django\test\client.py:671: in generic + return self.request(**r) + ^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:289: in request + return super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:241: in request + request = super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\test\client.py:1087: in request + self.check_exception(response) +..\.venv\Lib\site-packages\django\test\client.py:802: in check_exception + raise exc_value +..\.venv\Lib\site-packages\django\core\handlers\exception.py:55: in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\core\handlers\base.py:197: in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\views\decorators\csrf.py:65: in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\viewsets.py:125: in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:515: in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:475: in handle_exception + self.raise_uncaught_exception(exc) +..\.venv\Lib\site-packages\rest_framework\views.py:486: in raise_uncaught_exception + raise exc +..\.venv\Lib\site-packages\rest_framework\views.py:512: in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +api\v1\employees\views\positions.py:70: in set_groups + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +api\v1\employees\views\_helpers.py:39: in _ldap_try + fn() +api\v1\employees\views\positions.py:70: in + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\directory_service.py:373: in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:72: in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:358: in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:212: in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:170: in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +E RuntimeError: LDAP_POSITIONS_BASE is not configured +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-02-16 17:34:54,593 django.request:253 Forbidden: /api/v1/positions/9/set_groups/ +[ERROR] 2026-02-16 17:34:54,620 django.request:253 Internal Server Error: /api/v1/positions/9/set_groups/ +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\views\decorators\csrf.py", line 65, in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\viewsets.py", line 125, in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 515, in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 475, in handle_exception + self.raise_uncaught_exception(exc) + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 486, in raise_uncaught_exception + raise exc + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 512, in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 70, in set_groups + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\_helpers.py", line 39, in _ldap_try + fn() + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 70, in + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\directory_service.py", line 373, in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 72, in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 358, in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 212, in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 170, in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +RuntimeError: LDAP_POSITIONS_BASE is not configured +_____________ test_add_and_remove_groups_and_permissions_endpoint _____________ +tests\api\v1\employees\test_positions.py:192: in test_add_and_remove_groups_and_permissions_endpoint + resp = api_client.post(url_add, {"groups": [gA.id]}, format="json") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:299: in post + response = super().post( +..\.venv\Lib\site-packages\rest_framework\test.py:213: in post + return self.generic('POST', path, data, content_type, **extra) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:237: in generic + return super().generic( +..\.venv\Lib\site-packages\django\test\client.py:671: in generic + return self.request(**r) + ^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:289: in request + return super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\test.py:241: in request + request = super().request(**kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\test\client.py:1087: in request + self.check_exception(response) +..\.venv\Lib\site-packages\django\test\client.py:802: in check_exception + raise exc_value +..\.venv\Lib\site-packages\django\core\handlers\exception.py:55: in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\core\handlers\base.py:197: in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\django\views\decorators\csrf.py:65: in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\viewsets.py:125: in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:515: in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +..\.venv\Lib\site-packages\rest_framework\views.py:475: in handle_exception + self.raise_uncaught_exception(exc) +..\.venv\Lib\site-packages\rest_framework\views.py:486: in raise_uncaught_exception + raise exc +..\.venv\Lib\site-packages\rest_framework\views.py:512: in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +api\v1\employees\views\positions.py:87: in add_groups + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +api\v1\employees\views\_helpers.py:39: in _ldap_try + fn() +api\v1\employees\views\positions.py:87: in + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\directory_service.py:373: in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:72: in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:358: in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:212: in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ +employees\ldap\services\position_service.py:170: in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +E RuntimeError: LDAP_POSITIONS_BASE is not configured +---------------------------- Captured stderr call ----------------------------- +[ERROR] 2026-02-16 17:34:54,811 django.request:253 Internal Server Error: /api/v1/positions/10/add_groups/ +Traceback (most recent call last): + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\exception.py", line 55, in inner + response = get_response(request) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\core\handlers\base.py", line 197, in _get_response + response = wrapped_callback(request, *callback_args, **callback_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\django\views\decorators\csrf.py", line 65, in _view_wrapper + return view_func(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\viewsets.py", line 125, in view + return self.dispatch(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 515, in dispatch + response = self.handle_exception(exc) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 475, in handle_exception + self.raise_uncaught_exception(exc) + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 486, in raise_uncaught_exception + raise exc + File "C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\rest_framework\views.py", line 512, in dispatch + response = handler(request, *args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 87, in add_groups + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\_helpers.py", line 39, in _ldap_try + fn() + File "C:\Users\igor_\Dev\EUSRR\backend\api\v1\employees\views\positions.py", line 87, in + err2 = _ldap_try(lambda: DirectoryService().reconcile_position(pos)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\directory_service.py", line 373, in reconcile_position + return self._position_service.reconcile_position(pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 72, in reconcile_position + return self._reconcile_position(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 358, in _reconcile_position + pos_dn = self._ensure_position_group(conn, pos) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 212, in _ensure_position_group + ensure_container_exists(conn, self._positions_base()) + ^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\igor_\Dev\EUSRR\backend\employees\ldap\services\position_service.py", line 170, in _positions_base + raise RuntimeError("LDAP_POSITIONS_BASE is not configured") +RuntimeError: LDAP_POSITIONS_BASE is not configured +________ TestRoleAssignmentEndpoints.test_assign_role_to_any_employee _________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +________________ TestRoleAssignmentEndpoints.test_revoke_role _________________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +______________ TestRoleAssignmentEndpoints.test_assignments_list ______________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_________ TestRoleAssignmentEndpoints.test_assign_requires_permission _________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +________ TestRoleAssignmentEndpoints.test_assign_with_dept_permission _________ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +___ TestRoleAssignmentPermissions.test_permission_check_via_role_assignment ___ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +_ TestRoleAssignmentPermissions.test_inactive_assignment_does_not_grant_permission _ +Fixture "make_user" called directly. Fixtures are not meant to be called directly, +but are created automatically when test functions request them as parameters. +See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and +https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly +============================== warnings summary =============================== +..\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952 + C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: tagMap is deprecated. Please use TAG_MAP instead. + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + +..\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952 + C:\Users\igor_\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: typeMap is deprecated. Please use TYPE_MAP instead. + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +=========================== short test summary info =========================== +FAILED tests/api/v1/employees/test_department_head_rights.py::test_head_can_update_department_without_explicit_role_perms +FAILED tests/api/v1/employees/test_department_head_rights.py::test_head_can_set_member_role_and_non_head_cannot +FAILED tests/api/v1/employees/test_department_head_rights.py::test_head_can_change_head_and_loses_rights_afterwards +FAILED tests/api/v1/employees/test_department_membership_separation.py::test_add_member_requires_manage_and_does_not_assign_role +FAILED tests/api/v1/employees/test_department_membership_separation.py::test_all_three_actions_require_auth_and_permissions +FAILED tests/api/v1/employees/test_department_membership_separation.py::test_set_member_role_requires_assign_not_manage +FAILED tests/api/v1/employees/test_department_membership_separation.py::test_set_member_role_does_not_create_membership_and_does_not_toggle_active +FAILED tests/api/v1/employees/test_department_membership_separation.py::test_add_member_ignores_role_parameter +FAILED tests/api/v1/employees/test_department_membership_separation.py::test_remove_member_requires_manage_and_does_not_touch_role +FAILED tests/api/v1/employees/test_department_roles.py::test_list_filtered_by_department +FAILED tests/api/v1/employees/test_department_roles.py::test_create_requires_assign_department_role +FAILED tests/api/v1/employees/test_department_roles.py::test_update_and_destroy_scope_enforced +FAILED tests/api/v1/employees/test_department_roles.py::test_perm_choices_and_perms_and_set_perms +FAILED tests/api/v1/employees/test_department_roles.py::test_set_perms_validates_payload +FAILED tests/api/v1/employees/test_department_roles.py::test_back_compat_permissions_field_contains_ids +FAILED tests/api/v1/employees/test_department_roles.py::test_create_with_scoped_permission_codes_and_update_name +FAILED tests/api/v1/employees/test_department_roles.py::test_cross_department_access_denied_for_set_perms +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_read_only_allowed_without_assign +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_staff_override_for_write +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_ordering_stable_by_name_then_id +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_set_perms_ids_take_precedence_over_codes +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_serializer_codes_override_scoped_permissions_on_create +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_perm_choices_fields_and_nonempty +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_destroy_requires_assign_in_same_department +FAILED tests/api/v1/employees/test_department_roles_extra.py::test_set_perms_dedup_and_replace_semantics +FAILED tests/api/v1/employees/test_departments.py::test_list_ok_for_authenticated +FAILED tests/api/v1/employees/test_departments.py::test_search_and_ordering +FAILED tests/api/v1/employees/test_departments.py::test_employees_count_adds_head_if_not_in_links +FAILED tests/api/v1/employees/test_departments.py::test_employees_count_not_double_count_head_if_in_links +FAILED tests/api/v1/employees/test_departments.py::test_create_requires_staff +FAILED tests/api/v1/employees/test_departments.py::test_destroy_requires_staff +FAILED tests/api/v1/employees/test_departments.py::test_partial_update_name_requires_manage_perm +FAILED tests/api/v1/employees/test_departments.py::test_partial_update_head_requires_change_head_perm +FAILED tests/api/v1/employees/test_departments.py::test_partial_update_head_same_value_does_not_require_extra_perm +FAILED tests/api/v1/employees/test_departments.py::test_set_head_by_role_with_perm +FAILED tests/api/v1/employees/test_departments.py::test_set_head_validation_inactive_employee +FAILED tests/api/v1/employees/test_departments.py::test_set_head_remove_with_null +FAILED tests/api/v1/employees/test_departments.py::test_set_head_requires_perm +FAILED tests/api/v1/employees/test_departments.py::test_set_member_role_happy_path +FAILED tests/api/v1/employees/test_departments.py::test_set_member_role_reject_foreign_role +FAILED tests/api/v1/employees/test_departments.py::test_set_member_role_requires_perm +FAILED tests/api/v1/employees/test_departments.py::test_set_member_role_missing_employee_id +FAILED tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_resets_email_verified_for_self +FAILED tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_via_me_endpoint_resets_verification +FAILED tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_sends_verification_code +FAILED tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_other_fields_does_not_reset_verification +FAILED tests/api/v1/employees/test_email_verification_security.py::TestEmailChangeResetsVerification::test_change_email_to_same_value_does_not_reset +FAILED tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_can_update_self_non_critical_fields +FAILED tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_can_verify_email +FAILED tests/api/v1/employees/test_email_verification_security.py::TestUnverifiedUserRestrictions::test_unverified_user_can_resend_code +FAILED tests/api/v1/employees/test_email_verification_security.py::TestResendEmailLogic::test_resend_email_generates_new_code +FAILED tests/api/v1/employees/test_email_verification_security.py::TestResendEmailLogic::test_resend_email_to_verified_user_fails +FAILED tests/api/v1/employees/test_email_verification_security.py::TestResendEmailLogic::test_old_code_becomes_invalid_after_resend +FAILED tests/api/v1/employees/test_email_verification_security.py::TestEmailVerificationWorkflow::test_full_registration_and_verification_flow +FAILED tests/api/v1/employees/test_email_verification_security.py::TestEmailVerificationWorkflow::test_change_email_and_verify_new_one +FAILED tests/api/v1/employees/test_employee_actions.py::test_dismissal_works_when_ldap_disabled +FAILED tests/api/v1/employees/test_employee_actions.py::test_restoration_moves_from_dismissed_to_users +FAILED tests/api/v1/employees/test_employees.py::test_create_requires_staff +FAILED tests/api/v1/employees/test_employees.py::test_destroy_requires_admin +FAILED tests/api/v1/employees/test_employees.py::test_me_patch_rejects_when_all_contacts_empty +FAILED tests/api/v1/employees/test_employees_fields_in_list.py::test_list_includes_basic_fields +FAILED tests/api/v1/employees/test_employees_fields_in_list.py::test_list_departments_include_role +FAILED tests/api/v1/employees/test_ldap_optional_register.py::test_register_without_ldap_creates_user_only_in_db +FAILED tests/api/v1/employees/test_positions.py::test_create_permissions - Ru... +FAILED tests/api/v1/employees/test_positions.py::test_update_permissions_and_search_ordering +FAILED tests/api/v1/employees/test_positions.py::test_set_groups_requires_special_perm +FAILED tests/api/v1/employees/test_positions.py::test_add_and_remove_groups_and_permissions_endpoint +FAILED tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assign_role_to_any_employee +FAILED tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_revoke_role +FAILED tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assignments_list +FAILED tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assign_requires_permission +FAILED tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentEndpoints::test_assign_with_dept_permission +FAILED tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentPermissions::test_permission_check_via_role_assignment +FAILED tests/api/v1/employees/test_role_assignment.py::TestRoleAssignmentPermissions::test_inactive_assignment_does_not_grant_permission +ERROR tests/api/v1/employees/test_ldap_optional_groups.py::test_create_group_without_ldap +ERROR tests/api/v1/employees/test_ldap_optional_groups.py::test_add_members_without_ldap +ERROR tests/api/v1/employees/test_ldap_optional_groups.py::test_destroy_group_without_ldap +ERROR tests/api/v1/employees/test_ldap_optional_groups.py::test_get_members_without_ldap +====== 74 failed, 59 passed, 13 skipped, 2 warnings, 4 errors in 17.79s ======= diff --git a/backend/scripts/update_test_helpers.py b/backend/scripts/update_test_helpers.py index 32b3edc8..69e210f7 100644 --- a/backend/scripts/update_test_helpers.py +++ b/backend/scripts/update_test_helpers.py @@ -25,31 +25,32 @@ files_to_update = [ "test_skills.py", ] + def add_import_if_missing(content: str) -> str: """Добавляет импорт test_helpers если его нет.""" import_line = "from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results" - + if "test_helpers" in content: return content - + # Находим последний import import_pattern = r'^(import |from )' lines = content.split('\n') last_import_idx = 0 - + for i, line in enumerate(lines): if re.match(import_pattern, line): last_import_idx = i - + # Вставляем после последнего импорта lines.insert(last_import_idx + 1, import_line) - + return '\n'.join(lines) def remove_local_functions(content: str) -> str: """Удаляет локальные определения helper функций.""" - + # Паттерны для удаления patterns = [ # make_user fixture @@ -67,10 +68,10 @@ def remove_local_functions(content: str) -> str: # extract_results r'def extract_results\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))', ] - + for pattern in patterns: content = re.sub(pattern, '', content, flags=re.DOTALL | re.MULTILINE) - + return content @@ -82,31 +83,31 @@ def replace_function_calls(content: str) -> str: r'\b_grant\(': 'grant_permission(', r'\b_grant_perm\(': 'grant_permission(', } - + for old, new in replacements.items(): content = re.sub(old, new, content) - + return content def update_file(filepath: Path): """Обновляет один файл.""" print(f"Обновляем {filepath.name}...") - + content = filepath.read_text(encoding='utf-8') - + # Добавляем импорт content = add_import_if_missing(content) - + # Удаляем локальные функции content = remove_local_functions(content) - + # Заменяем вызовы content = replace_function_calls(content) - + # Убираем лишние пустые строки content = re.sub(r'\n{3,}', '\n\n', content) - + filepath.write_text(content, encoding='utf-8') print(f" ✓ {filepath.name} обновлен") @@ -122,7 +123,7 @@ def main(): print(f" ✗ Ошибка в {filename}: {e}", file=sys.stderr) else: print(f" ! {filename} не найден") - + print("\n✅ Обновление завершено!") diff --git a/backend/tests/api/v1/employees/test_department_head_rights.py b/backend/tests/api/v1/employees/test_department_head_rights.py index 2fe251cb..0e97d98c 100644 --- a/backend/tests/api/v1/employees/test_department_head_rights.py +++ b/backend/tests/api/v1/employees/test_department_head_rights.py @@ -1,14 +1,15 @@ # backend/tests/api/v1/employees/test_department_head_rights.py import pytest -from django.urls import reverse from django.contrib.auth import get_user_model -from rest_framework.test import APIClient +from django.urls import reverse +from employees.models import (Department, DepartmentRole, Employee, + EmployeeDepartment) from rest_framework import status - -from employees.models import Department, DepartmentRole, EmployeeDepartment, Employee +from rest_framework.test import APIClient +from tests.api.v1.employees.test_helpers import (grant_permission, + make_department, make_user) from tests.conftest import _unique_phone from tests.test_config import DEFAULT_PASSWORD -from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department User = get_user_model() @@ -16,34 +17,8 @@ User = get_user_model() # Helpers # ========================= -def _unique_phone() -> str: - base = 79000000000 # 79 + 9 нулей - return str(base + User.objects.count()) - # Используем make_user из test_helpers -def _unique_email() -> str: - base = "asd@mail.com" - return str(Employee.objects.count()) + base - """ - Создаёт активного и 'email_verified' пользователя, чтобы не споткнуться о - валидацию при назначении руководителя. - """ - u = Employee.objects.create_user( - email=_unique_email(), - password=DEFAULT_PASSWORD, - phone_number=_unique_phone(), - send_activation_email=False, - first_name="T", - last_name="U", - ) - u.is_staff = staff - u.is_superuser = superuser - u.email_verified = True # <- критично для твоей нынешней валидации - u.is_active = True # <- тоже нужен для твоей ветки is_active - u.save(update_fields=["is_staff", "is_superuser", "email_verified", "is_active"]) - return u - def make_role( dept: Department, name: str, codes: list[str] | None = None ) -> DepartmentRole: @@ -77,7 +52,7 @@ def api_client() -> APIClient: # ========================= @pytest.mark.django_db -def test_head_can_update_department_without_explicit_role_perms(api_client: APIClient): +def test_head_can_update_department_without_explicit_role_perms(api_client: APIClient, ensure_ldap_disabled): head = make_user("head@example.com") d = Department.objects.create(name="Dept", description="old", head=head) @@ -91,7 +66,7 @@ def test_head_can_update_department_without_explicit_role_perms(api_client: APIC assert resp.json()["description"] == "new" @pytest.mark.django_db -def test_head_can_set_member_role_and_non_head_cannot(api_client: APIClient): +def test_head_can_set_member_role_and_non_head_cannot(api_client: APIClient, ensure_ldap_disabled): head = make_user("head@example.com") d = Department.objects.create(name="Dept", head=head) @@ -103,7 +78,8 @@ def test_head_can_set_member_role_and_non_head_cannot(api_client: APIClient): # руководитель назначает роль участнику api_client.force_authenticate(user=head) url = reverse("api:v1:departments-set-member-role", args=[d.id]) - payload = {"employee_id": worker.id, "role_id": role_worker.id, "is_active": True} + payload = {"employee_id": worker.id, + "role_id": role_worker.id, "is_active": True} resp = api_client.post(url, payload, format="json") assert resp.status_code == status.HTTP_200_OK data = resp.json() @@ -118,7 +94,7 @@ def test_head_can_set_member_role_and_non_head_cannot(api_client: APIClient): assert resp.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.django_db -def test_head_can_change_head_and_loses_rights_afterwards(api_client: APIClient): +def test_head_can_change_head_and_loses_rights_afterwards(api_client: APIClient, ensure_ldap_disabled): old_head = make_user("old@example.com") d = Department.objects.create(name="Dept", head=old_head) @@ -127,13 +103,15 @@ def test_head_can_change_head_and_loses_rights_afterwards(api_client: APIClient) # старый руководитель меняет руководителя на другого api_client.force_authenticate(user=old_head) url_set_head = reverse("api:v1:departments-set-head", args=[d.id]) - resp = api_client.post(url_set_head, {"head_id": new_head.id}, format="json") + resp = api_client.post( + url_set_head, {"head_id": new_head.id}, format="json") assert resp.status_code == status.HTTP_200_OK assert resp.json()["head"]["id"] == new_head.id # теперь старый руководитель больше не глава — у него не должно быть прав управления отделом url_detail = reverse("api:v1:departments-detail", args=[d.id]) - resp = api_client.patch(url_detail, {"description": "after"}, format="json") + resp = api_client.patch( + url_detail, {"description": "after"}, format="json") assert resp.status_code == status.HTTP_403_FORBIDDEN # а у нового руководителя — есть diff --git a/backend/tests/api/v1/employees/test_department_membership_separation.py b/backend/tests/api/v1/employees/test_department_membership_separation.py index caeecc5f..d5f4609b 100644 --- a/backend/tests/api/v1/employees/test_department_membership_separation.py +++ b/backend/tests/api/v1/employees/test_department_membership_separation.py @@ -11,6 +11,7 @@ from employees.models import ( EmployeeDepartment, Employee, ) +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results # ========================= # Helpers @@ -25,23 +26,6 @@ def _unique_phone_from_email(email: str) -> str: # "+79" + 9 цифр = 11-значный номер РФ return f"+79{n:09d}" -@pytest.fixture -def make_user(email: str, *, staff: bool = False, superuser: bool = False) -> Employee: - """Fixture для создания пользователей.""" - """ - Фабрика пользователя для тестов. Учитывает ограничение уникальности phone_number. - """ - return Employee.objects.create( - email=email, - phone_number=_unique_phone_from_email(email), - first_name="T", - last_name="U", - is_staff=staff, - is_superuser=superuser, - email_verified=True, - is_active=True, - ) - def ensure_dept_perm(code: str, name: str | None = None) -> DepartmentPermission: """ Создаёт (или возвращает) департаментское право с данным кодом. diff --git a/backend/tests/api/v1/employees/test_department_roles.py b/backend/tests/api/v1/employees/test_department_roles.py index 4f20e210..a2216483 100644 --- a/backend/tests/api/v1/employees/test_department_roles.py +++ b/backend/tests/api/v1/employees/test_department_roles.py @@ -12,6 +12,7 @@ from employees.models import ( EmployeeDepartment, # <-- если у тебя EmployeeDepartmentLink, замени импорт и упоминания ниже ) from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results User = get_user_model() @@ -19,30 +20,6 @@ User = get_user_model() # Helpers # ========================= -@pytest.fixture -def make_user(email: str, staff: bool = False, verified: bool = True) -> User: - """ - Создаёт пользователя с обязательным phone_number и отключённой отправкой активационного письма. - Поле 'verified' ставим только если оно существует в модели. - """ - extra = { - "phone_number": _unique_phone(), - "is_staff": staff, - "send_activation_email": False, - } - # Добавим verified, если поле есть в модели - try: - User._meta.get_field("verified") - extra["verified"] = verified - except Exception: - pass - - return User.objects.create_user( - email=email, - password="pwd12345", - **extra, - ) - def ensure_dept_perm(code: str, name: str | None = None) -> DepartmentPermission: return DepartmentPermission.objects.get_or_create( code=code, defaults={"name": name or code} diff --git a/backend/tests/api/v1/employees/test_department_roles_extra.py b/backend/tests/api/v1/employees/test_department_roles_extra.py index f7c2ab40..18e7087c 100644 --- a/backend/tests/api/v1/employees/test_department_roles_extra.py +++ b/backend/tests/api/v1/employees/test_department_roles_extra.py @@ -12,6 +12,7 @@ from employees.models import ( EmployeeDepartment, # если у вас EmployeeDepartmentLink, замените импорт и упоминания ниже ) from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results User = get_user_model() @@ -19,22 +20,6 @@ User = get_user_model() # Helpers # ========================= -@pytest.fixture -def make_user(email: str, staff: bool = False, verified: bool = True) -> User: - """Fixture для создания пользователей.""" - extra = { - "phone_number": _unique_phone(), - "is_staff": staff, - "send_activation_email": False, - } - # добавим verified если поле есть - try: - User._meta.get_field("verified") - extra["verified"] = verified - except Exception: - pass - return User.objects.create_user(email=email, password="pwd12345", **extra) - def ensure_perm(code: str, name: str | None = None) -> DepartmentPermission: return DepartmentPermission.objects.get_or_create( code=code, defaults={"name": name or code} diff --git a/backend/tests/api/v1/employees/test_departments.py b/backend/tests/api/v1/employees/test_departments.py index b9d60ca5..81a74eab 100644 --- a/backend/tests/api/v1/employees/test_departments.py +++ b/backend/tests/api/v1/employees/test_departments.py @@ -59,6 +59,7 @@ from employees.models import ( from rest_framework import status from rest_framework.test import APIClient from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results pytestmark = pytest.mark.django_db @@ -72,33 +73,6 @@ def api_client(): _phone_seq = itertools.count(1000) -def _unique_phone() -> str: - # +7999000XXX — валидный E.164, и всегда уникальный в рамках тестов - return f"+7999000{next(_phone_seq):03d}" - -@pytest.fixture -def make_user( - email: str, *, staff=False, superuser=False, verified=True, active=True, **extra -): - """Fixture для создания пользователей.""" - """ - Создаём пользователя напрямую (без менеджера, чтобы не отправлять почту). - """ - u = User.objects.create( - email=email, - phone_number=extra.pop("phone_number", _unique_phone()), - first_name=extra.pop("first_name", "FN"), - last_name=extra.pop("last_name", "LN"), - is_staff=staff, - is_superuser=superuser, - is_active=active, - email_verified=verified, - **extra, - ) - u.set_password("pass") - u.save() - return u - def perm_for_department(code: str) -> DepartmentPermission: """Безопасно получаем/создаём скоуп-право отдела по коду.""" p, _ = DepartmentPermission.objects.get_or_create( @@ -114,11 +88,6 @@ def make_role( r.scoped_permissions.add(*[perm_for_department(c) for c in codes]) return r -def extract_results(data): - if isinstance(data, dict) and "results" in data: - return data["results"] - return data - # ---------- tests: auth/list/basic ---------- def test_list_requires_auth(api_client: APIClient): diff --git a/backend/tests/api/v1/employees/test_email_verification_security.py b/backend/tests/api/v1/employees/test_email_verification_security.py index bfb7cb9f..c33b320f 100644 --- a/backend/tests/api/v1/employees/test_email_verification_security.py +++ b/backend/tests/api/v1/employees/test_email_verification_security.py @@ -7,11 +7,13 @@ 3. Повторная отправка кода работает корректно """ +from unittest.mock import patch + import pytest from django.contrib.auth import get_user_model from rest_framework import status from rest_framework.test import APIClient -from unittest.mock import patch +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results Employee = get_user_model() @@ -21,27 +23,6 @@ def disable_ldap(): with patch("api.v1.employees.views._helpers._is_ldap_enabled", return_value=False): yield -@pytest.fixture -def make_user(email, verified=True, **kwargs): - """Fixture для создания пользователей.""" - """Создаёт пользователя для тестов.""" - 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 TestEmailChangeResetsVerification: """Тесты на сброс email_verified при изменении email.""" diff --git a/backend/tests/api/v1/employees/test_employee_actions.py b/backend/tests/api/v1/employees/test_employee_actions.py index 5c6f9ba1..883ba007 100644 --- a/backend/tests/api/v1/employees/test_employee_actions.py +++ b/backend/tests/api/v1/employees/test_employee_actions.py @@ -7,71 +7,37 @@ from unittest.mock import patch, Mock from employees.models import Employee, Department, EmployeeDepartment, EmployeeAction from employees.constants import ACTION_DISMISSED, ACTION_CHOICES +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results # helpers _seq = 1 - def _email(p="u"): global _seq _seq += 1 return f"{p}{_seq}@example.com" - def _phone(): global _seq _seq += 1 return f"+7999{_seq:07d}" - -def _user(staff=False, superuser=False) -> Employee: - u = Employee.objects.create_user( - email=_email(), - password="pass", - phone_number=_phone(), - send_activation_email=False, - first_name="T", - last_name="U", - ) - u.is_staff = staff - u.is_superuser = superuser - u.email_verified = True - u.is_active = True - u.save(update_fields=["is_staff", "is_superuser", "email_verified", "is_active"]) - return u - - -def _grant(user: Employee, code: str): - app, codename = code.split(".", 1) - p = Permission.objects.get(content_type__app_label=app, codename=codename) - user.user_permissions.add(p) - user.save() - for a in ("_perm_cache", "_user_perm_cache", "_group_perm_cache"): - if hasattr(user, a): - try: - delattr(user, a) - except: - pass - return p - - def _any_non_dismissed(): for k, _ in ACTION_CHOICES: if k != ACTION_DISMISSED: return k pytest.skip("No non-dismissed action in ACTION_CHOICES") - @pytest.mark.django_db def test_list_requires_auth_and_filtering(api_client): url = reverse("api:v1:employee-actions-list") # unauth assert api_client.get(url).status_code in (401, 403) - actor = _user() + actor = make_user() api_client.force_authenticate(user=actor) - emp = _user() - other = _user() + emp = make_user() + other = make_user() # фиксируем базовую линию: авто-события могли уже создаться сигналами baseline_all = set(EmployeeAction.objects.values_list("id", flat=True)) baseline_emp = set( @@ -95,11 +61,10 @@ def test_list_requires_auth_and_filtering(api_client): ids = {row["id"] for row in resp.data} assert ids == baseline_emp | {a1.id} - @pytest.mark.django_db def test_create_requires_perm_or_staff(api_client): - actor = _user() - target = _user() + actor = make_user() + target = make_user() api_client.force_authenticate(user=actor) url = reverse("api:v1:employee-actions-list") payload = { @@ -115,26 +80,25 @@ def test_create_requires_perm_or_staff(api_client): assert resp.status_code == 403 # grant add perm -> 201 - _grant(actor, "employees.add_employeeaction") + grant_permission(actor, "employees.add_employeeaction") baseline = EmployeeAction.objects.filter(employee=target).count() resp = api_client.post(url, payload, format="json") assert resp.status_code == 201 assert EmployeeAction.objects.filter(employee=target).count() == baseline + 1 # staff bypass -> 201 - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) baseline2 = EmployeeAction.objects.filter(employee=target).count() resp = api_client.post(url, payload, format="json") assert resp.status_code == 201 assert EmployeeAction.objects.filter(employee=target).count() == baseline2 + 1 - @pytest.mark.django_db def test_dismissal_deactivates_employee_and_links(api_client): - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() dept = Department.objects.create(name="D") EmployeeDepartment.objects.create(employee=emp, department=dept, is_active=True) @@ -155,12 +119,11 @@ def test_dismissal_deactivates_employee_and_links(api_client): link = EmployeeDepartment.objects.get(employee=emp, department=dept) assert link.is_active is False and link.date_to is not None - @pytest.mark.django_db def test_non_dismissal_activates_employee(api_client): - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() emp.is_active = False emp.save(update_fields=["is_active"]) @@ -178,15 +141,14 @@ def test_non_dismissal_activates_employee(api_client): emp.refresh_from_db() assert emp.is_active is True - @pytest.mark.django_db def test_update_to_dismissed_applies_effects(api_client): - actor = _user() + actor = make_user() api_client.force_authenticate(user=actor) - _grant(actor, "employees.add_employeeaction") - _grant(actor, "employees.change_employeeaction") + grant_permission(actor, "employees.add_employeeaction") + grant_permission(actor, "employees.change_employeeaction") - emp = _user() + emp = make_user() act = EmployeeAction.objects.create( employee=emp, action=_any_non_dismissed(), date=timezone.now() ) @@ -197,12 +159,11 @@ def test_update_to_dismissed_applies_effects(api_client): emp.refresh_from_db() assert emp.is_active is False - @pytest.mark.django_db def test_delete_requires_perm_or_staff(api_client): - actor = _user() + actor = make_user() api_client.force_authenticate(user=actor) - emp = _user() + emp = make_user() act = EmployeeAction.objects.create( employee=emp, action=_any_non_dismissed(), date=timezone.now() ) @@ -212,10 +173,9 @@ def test_delete_requires_perm_or_staff(api_client): assert api_client.delete(url_det).status_code == 403 # grant -> 204 - _grant(actor, "employees.delete_employeeaction") + grant_permission(actor, "employees.delete_employeeaction") assert api_client.delete(url_det).status_code == 204 - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.directory_service.DirectoryService.update_user') @@ -227,9 +187,9 @@ def test_dismissal_syncs_to_ldap_if_has_dn( mock_is_enabled.return_value = True - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() # Создаём запись LdapSyncState (имитация сотрудника с LDAP) LdapSyncState.objects.create( @@ -260,7 +220,6 @@ def test_dismissal_syncs_to_ldap_if_has_dn( assert call_args[0][0].id == emp.id # первый аргумент - emp assert call_args[1]['changes'] == {'is_active': False} - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.directory_service.DirectoryService.update_user') @@ -274,9 +233,9 @@ def test_dismissal_handles_ldap_error_gracefully( mock_is_enabled.return_value = True mock_update.side_effect = DirectoryLdapError("LDAP connection failed") - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() # Создаём запись LdapSyncState LdapSyncState.objects.create( @@ -306,7 +265,6 @@ def test_dismissal_handles_ldap_error_gracefully( # LDAP был вызван (и упал) mock_update.assert_called_once() - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.directory_service.DirectoryService.update_user') @@ -318,9 +276,9 @@ def test_non_dismissal_syncs_activation_to_ldap( mock_is_enabled.return_value = True - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() emp.is_active = False emp.save(update_fields=["is_active"]) @@ -353,7 +311,6 @@ def test_non_dismissal_syncs_activation_to_ldap( assert call_args[0][0].id == emp.id assert call_args[1]['changes'] == {'is_active': True} - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.directory_service.DirectoryService.update_user') @@ -363,9 +320,9 @@ def test_dismissal_skips_ldap_if_no_dn( """Проверяет, что LDAP не вызывается без ldap_dn.""" mock_is_enabled.return_value = True - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() # НЕ создаём LdapSyncState - сотрудник без LDAP url = reverse("api:v1:employee-actions-list") @@ -386,7 +343,6 @@ def test_dismissal_skips_ldap_if_no_dn( # LDAP НЕ должен быть вызван mock_update.assert_not_called() - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.directory_service.DirectoryService.update_user') @@ -398,9 +354,9 @@ def test_dismissal_works_when_ldap_disabled( mock_is_enabled.return_value = False # LDAP отключен - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() # Даже если есть запись LdapSyncState LdapSyncState.objects.create( @@ -429,7 +385,6 @@ def test_dismissal_works_when_ldap_disabled( # LDAP НЕ должен быть вызван, т.к. отключен mock_update.assert_not_called() - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.directory_service.DirectoryService.remove_member') @@ -442,9 +397,9 @@ def test_dismissal_moves_to_dismissed_ou( mock_is_enabled.return_value = True - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() # Создаём отдел и связь dept = Department.objects.create(name="IT Department", description="IT") @@ -489,7 +444,6 @@ def test_dismissal_moves_to_dismissed_ou( assert call_args[0][0].id == dept.id # department assert call_args[0][1].id == emp.id # employee - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.infrastructure.connections._ldap') @@ -510,9 +464,9 @@ def test_restoration_moves_from_dismissed_to_users( mock_ldap_ctx.return_value.__enter__.return_value = mock_conn mock_ldap_ctx.return_value.__exit__.return_value = False - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user(is_active=False) # Уволенный сотрудник + emp = make_user(is_active=False) # Уволенный сотрудник # Создаём запись LdapSyncState - сотрудник в OU=Dismissed sync_state = LdapSyncState.objects.create( @@ -548,7 +502,6 @@ def test_restoration_moves_from_dismissed_to_users( # Проверяем, что был вызван _move_user_to_base (через mock LDAP) # Это косвенная проверка - мы мокнули _ldap(), так что код выполнился - @pytest.mark.django_db @patch('api.v1.employees.views._helpers._is_ldap_enabled') @patch('employees.ldap.infrastructure.connections._ldap') @@ -569,9 +522,9 @@ def test_dismissal_without_department_moves_to_dismissed( mock_ldap_ctx.return_value.__enter__.return_value = mock_conn mock_ldap_ctx.return_value.__exit__.return_value = False - staff = _user(staff=True) + staff = make_user(staff=True) api_client.force_authenticate(user=staff) - emp = _user() + emp = make_user() # Сотрудник НЕ состоит ни в одном отделе (нет EmployeeDepartment записей) # Создаём запись LdapSyncState - сотрудник в OU=Users diff --git a/backend/tests/api/v1/employees/test_employees.py b/backend/tests/api/v1/employees/test_employees.py index 933f8b8d..976af5c5 100644 --- a/backend/tests/api/v1/employees/test_employees.py +++ b/backend/tests/api/v1/employees/test_employees.py @@ -1,23 +1,17 @@ # tests/api/v1/employees/test_employees.py -import itertools import datetime as dt +import itertools import pytest from django.contrib.auth import get_user_model from django.urls import reverse +from employees.constants import ACTION_DISMISSED # для фильтра actually_active +from employees.models import (Department, Employee, EmployeeAction, + EmployeeDepartment, Position, Skill) from rest_framework import status from rest_framework.test import APIClient - -from employees.models import ( - Department, - Employee, - EmployeeAction, - EmployeeDepartment, - Position, - Skill, -) -from employees.constants import ACTION_DISMISSED # для фильтра actually_active from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results pytestmark = pytest.mark.django_db @@ -31,43 +25,6 @@ def api_client(): _phone_seq = itertools.count(2000) -def _unique_phone() -> str: - # валидный уникальный E.164 - return f"+7999000{next(_phone_seq):03d}" - -def make_user( - email: str, - *, - staff: bool = False, - superuser: bool = False, - verified: bool = True, - active: bool = True, - **extra, -) -> User: - """Helper для создания пользователей в тестах.""" - """ - Создаём пользователя напрямую (без менеджера, чтобы не слать почту). - """ - u = User.objects.create( - email=email, - phone_number=extra.pop("phone_number", _unique_phone()), - first_name=extra.pop("first_name", "FN"), - last_name=extra.pop("last_name", "LN"), - is_staff=staff, - is_superuser=superuser, - is_active=active, - email_verified=verified, - **extra, - ) - u.set_password("pass") - u.save() - return u - -def extract_results(data): - if isinstance(data, dict) and "results" in data: - return data["results"] - return data - # ---------- basic auth / list / retrieve ---------- def test_list_requires_auth(api_client: APIClient): @@ -150,7 +107,8 @@ def test_filter_by_department_includes_members_and_head(api_client: APIClient): outsider = make_user("out@example.com", last_name="Out") d = Department.objects.create(name="Dept", head=head) - EmployeeDepartment.objects.create(employee=member, department=d, is_active=True) + EmployeeDepartment.objects.create( + employee=member, department=d, is_active=True) url = reverse("api:v1:employees-list") r = api_client.get(url, {"department": d.id}) @@ -231,7 +189,8 @@ def test_filter_actually_active_logic(api_client: APIClient): # e_dismissed: email_verified=True, последнее действие — DISMISSED => не активен e_d = make_user("d@example.com", verified=True, active=True) EmployeeAction.objects.create( - employee=e_d, action=ACTION_DISMISSED, date=dt.datetime.now(dt.timezone.utc) + employee=e_d, action=ACTION_DISMISSED, date=dt.datetime.now( + dt.timezone.utc) ) # e_unver: email_verified=False, даже при active=True — не включается при actually_active=true @@ -265,7 +224,8 @@ def test_create_requires_staff(api_client: APIClient): # контакт специально НЕ передаём -> 400 из-за валидации контактов } resp = api_client.post(url, payload, format="json") - assert resp.status_code in (status.HTTP_403_FORBIDDEN, status.HTTP_401_UNAUTHORIZED) + assert resp.status_code in ( + status.HTTP_403_FORBIDDEN, status.HTTP_401_UNAUTHORIZED) # staff -> но без контактов всё равно 400 staff = make_user("staff@example.com", staff=True) diff --git a/backend/tests/api/v1/employees/test_employees_fields_in_list.py b/backend/tests/api/v1/employees/test_employees_fields_in_list.py index 6f9c582b..7e2b5087 100644 --- a/backend/tests/api/v1/employees/test_employees_fields_in_list.py +++ b/backend/tests/api/v1/employees/test_employees_fields_in_list.py @@ -9,6 +9,7 @@ from employees.models import (Department, DepartmentRole, EmployeeDepartment, from rest_framework import status from rest_framework.test import APIClient from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results pytestmark = pytest.mark.django_db User = get_user_model() @@ -21,37 +22,6 @@ def api(): _phone_seq = itertools.count(3000) -@pytest.fixture -def make_user( - email: str, - *, - staff: bool = False, - superuser: bool = False, - verified: bool = True, - active: bool = True, - **extra, -) -> User: - """Fixture для создания пользователей.""" - u = User.objects.create( - email=email, - phone_number=extra.pop("phone_number", _unique_phone()), - first_name=extra.pop("first_name", "FN"), - last_name=extra.pop("last_name", "LN"), - is_staff=staff, - is_superuser=superuser, - is_active=active, - email_verified=verified, - **extra, - ) - u.set_password("pass") - u.save() - return u - -def extract_results(data): - if isinstance(data, dict) and "results" in data: - return data["results"] - return data - def _get_item(items, emp_id): for it in items: if it.get("id") == emp_id: diff --git a/backend/tests/api/v1/employees/test_helpers.py b/backend/tests/api/v1/employees/test_helpers.py index 6af8f43d..b3fcc944 100644 --- a/backend/tests/api/v1/employees/test_helpers.py +++ b/backend/tests/api/v1/employees/test_helpers.py @@ -4,10 +4,10 @@ Используйте эти функции вместо создания собственных в каждом тестовом файле. """ import itertools + from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission -from employees.models import Employee, Department, DepartmentRole, Position - +from employees.models import Department, DepartmentRole, Employee, Position from tests.test_config import DEFAULT_PASSWORD, TEST_EMAIL_DOMAIN User = get_user_model() @@ -39,7 +39,7 @@ def make_user( ) -> User: """ Создает пользователя для тестов. - + Args: email: Email пользователя (если None, генерируется автоматически) staff: Флаг is_staff @@ -48,22 +48,22 @@ def make_user( active: Флаг is_active password: Пароль (по умолчанию DEFAULT_PASSWORD) **extra: Дополнительные поля - + Returns: User: Созданный пользователь """ if email is None: email = make_unique_email() - + if 'phone_number' not in extra: extra['phone_number'] = make_unique_phone() - + if 'first_name' not in extra: extra['first_name'] = 'Test' - + if 'last_name' not in extra: extra['last_name'] = 'User' - + user = User.objects.create( email=email, is_staff=staff, @@ -74,14 +74,14 @@ def make_user( ) user.set_password(password) user.save() - + return user def grant_permission(user: User, permission_codename: str): """ Выдает пользователю указанное разрешение. - + Args: user: Пользователь permission_codename: Codename разрешения в формате 'app_label.codename' @@ -96,7 +96,7 @@ def grant_permission(user: User, permission_codename: str): else: # Пытаемся найти по codename perm = Permission.objects.get(codename=permission_codename) - + user.user_permissions.add(perm) user.save() @@ -104,49 +104,49 @@ def grant_permission(user: User, permission_codename: str): def make_department(**kwargs) -> Department: """ Создает отдел для тестов. - + Args: **kwargs: Поля модели Department - + Returns: Department: Созданный отдел """ if 'name' not in kwargs: kwargs['name'] = f"Test Department {Department.objects.count() + 1}" - + return Department.objects.create(**kwargs) def make_position(**kwargs) -> Position: """ Создает должность для тестов. - + Args: **kwargs: Поля модели Position - + Returns: Position: Созданная должность """ if 'name' not in kwargs: kwargs['name'] = f"Test Position {Position.objects.count() + 1}" - + return Position.objects.create(**kwargs) def make_department_role(department: Department, **kwargs) -> DepartmentRole: """ Создает роль отдела для тестов. - + Args: department: Отдел **kwargs: Дополнительные поля - + Returns: DepartmentRole: Созданная роль """ if 'name' not in kwargs: kwargs['name'] = f"Test Role {DepartmentRole.objects.count() + 1}" - + return DepartmentRole.objects.create( department=department, **kwargs @@ -156,10 +156,10 @@ def make_department_role(department: Department, **kwargs) -> DepartmentRole: def extract_results(data): """ Извлекает результаты из пагинированного ответа API. - + Args: data: Данные ответа (dict или list) - + Returns: list: Список результатов """ diff --git a/backend/tests/api/v1/employees/test_ldap_optional_groups.py b/backend/tests/api/v1/employees/test_ldap_optional_groups.py index 47040348..43dad360 100644 --- a/backend/tests/api/v1/employees/test_ldap_optional_groups.py +++ b/backend/tests/api/v1/employees/test_ldap_optional_groups.py @@ -13,31 +13,13 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results User = get_user_model() pytestmark = pytest.mark.django_db _phone_seq = itertools.count(5000) -@pytest.fixture -def make_user(email, **kwargs): - """Fixture для создания пользователей.""" - """Создаёт пользователя напрямую""" - u = User.objects.create( - email=email, - phone_number=kwargs.pop("phone_number", _unique_phone()), - first_name=kwargs.pop("first_name", "Test"), - last_name=kwargs.pop("last_name", "User"), - is_staff=kwargs.pop("staff", False), - is_superuser=kwargs.pop("superuser", False), - is_active=kwargs.pop("active", True), - email_verified=kwargs.pop("verified", True), - **kwargs - ) - u.set_password("pass") - u.save() - return u - @pytest.fixture def api_client(): return APIClient() diff --git a/backend/tests/api/v1/employees/test_positions.py b/backend/tests/api/v1/employees/test_positions.py index 5054b7d7..79bd6425 100644 --- a/backend/tests/api/v1/employees/test_positions.py +++ b/backend/tests/api/v1/employees/test_positions.py @@ -8,15 +8,12 @@ from django.urls import reverse from employees.models import Employee, Position from rest_framework import status from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results # ---------- helpers ---------- -def _grant_perm(user, code: str): - app_label, codename = code.split(".") - perm = Permission.objects.get(content_type__app_label=app_label, codename=codename) - user.user_permissions.add(perm) - - # Сброс кэшей пермишенов у этого инстанса +def _flush_perm_cache(user): + """Сброс кэшей пермишенов у этого инстанса""" for attr in ("_perm_cache", "_user_perm_cache", "_group_perm_cache"): if hasattr(user, attr): delattr(user, attr) @@ -25,28 +22,8 @@ def _grant_perm(user, code: str): User = get_user_model() return User.objects.get(pk=user.pk) - -def _make_user(email: str, staff: bool = False, superuser: bool = False) -> Employee: - # Создаём пользователя через менеджер, контакты не требуются (clean не вызывается на save()) - u = Employee.objects.create_user( - email=email, - password="pass12345", - phone_number=_unique_phone(), - send_activation_email=False, - first_name="T", - last_name="U", - ) - u.is_staff = staff - u.is_superuser = superuser - u.email_verified = True - u.is_active = True - u.save(update_fields=["is_staff", "is_superuser", "email_verified", "is_active"]) - return u - - # ---------- tests ---------- - @pytest.mark.django_db def test_list_requires_auth(api_client): url = reverse("api:v1:positions-list") @@ -56,7 +33,7 @@ def test_list_requires_auth(api_client): assert resp.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN) # auth -> 200 - user = _make_user("user@example.com") + user = make_user("user@example.com") api_client.force_authenticate(user=user) Position.objects.create(name="Manager") Position.objects.create(name="Engineer") @@ -67,36 +44,34 @@ def test_list_requires_auth(api_client): names = [p["name"] for p in resp.data] assert {"Manager", "Engineer"} <= set(names) - @pytest.mark.django_db def test_create_permissions(api_client): url = reverse("api:v1:positions-list") # обычный пользователь без прав -> 403 - user = _make_user("user@example.com") + user = make_user("user@example.com") api_client.force_authenticate(user=user) resp = api_client.post(url, {"name": "NewPos"}, format="json") assert resp.status_code == status.HTTP_403_FORBIDDEN # с пермом add_position -> 201 - _grant_perm(user, "employees.add_position") + grant_permission(user, "employees.add_position") resp = api_client.post(url, {"name": "NewPos"}, format="json") assert resp.status_code == status.HTTP_201_CREATED assert resp.data["name"] == "NewPos" # staff без специальных прав -> 201 (staff допускается) - staff = _make_user("staff@example.com", staff=True) + staff = make_user("staff@example.com", staff=True) api_client.force_authenticate(user=staff) resp = api_client.post(url, {"name": "StaffPos"}, format="json") assert resp.status_code == status.HTTP_201_CREATED - @pytest.mark.django_db def test_update_permissions_and_search_ordering(api_client): p1 = Position.objects.create(name="Alpha", description="one") p2 = Position.objects.create(name="Beta", description="two") - user = _make_user("user@example.com") + user = make_user("user@example.com") api_client.force_authenticate(user=user) # без прав change_position -> 403 @@ -105,7 +80,7 @@ def test_update_permissions_and_search_ordering(api_client): assert resp.status_code == status.HTTP_403_FORBIDDEN # выдаём change_position -> 200 - _grant_perm(user, "employees.change_position") + grant_permission(user, "employees.change_position") resp = api_client.patch(url_detail, {"description": "updated"}, format="json") assert resp.status_code == status.HTTP_200_OK p1.refresh_from_db() @@ -123,11 +98,10 @@ def test_update_permissions_and_search_ordering(api_client): names = [row["name"] for row in resp.data] assert names == sorted(names, reverse=True) - @pytest.mark.django_db def test_delete_permissions(api_client): pos = Position.objects.create(name="ToDelete") - user = _make_user("user@example.com") + user = make_user("user@example.com") api_client.force_authenticate(user=user) url_detail = reverse("api:v1:positions-detail", args=[pos.id]) @@ -136,35 +110,33 @@ def test_delete_permissions(api_client): assert resp.status_code == status.HTTP_403_FORBIDDEN # с delete_position -> 204 - _grant_perm(user, "employees.delete_position") + grant_permission(user, "employees.delete_position") resp = api_client.delete(url_detail) assert resp.status_code == status.HTTP_204_NO_CONTENT assert not Position.objects.filter(id=pos.id).exists() - @pytest.mark.django_db def test_set_groups_requires_special_perm(api_client): pos = Position.objects.create(name="RoleOwner") grp1 = Group.objects.create(name="grp1") grp2 = Group.objects.create(name="grp2") - user = _make_user("user@example.com") + user = make_user("user@example.com") api_client.force_authenticate(user=user) url_set = reverse("api:v1:positions-set-groups", args=[pos.id]) # даже с change_position НЕЛЬЗЯ менять группы — нужен assign_position_groups - _grant_perm(user, "employees.change_position") + grant_permission(user, "employees.change_position") resp = api_client.post(url_set, {"groups": [grp1.id, grp2.id]}, format="json") assert resp.status_code == status.HTTP_403_FORBIDDEN # выдаём спец-пермиссию -> 200 - _grant_perm(user, "employees.assign_position_groups") + grant_permission(user, "employees.assign_position_groups") resp = api_client.post(url_set, {"groups": [grp1.id, grp2.id]}, format="json") assert resp.status_code == status.HTTP_200_OK assert set(pos.groups.values_list("id", flat=True)) == {grp1.id, grp2.id} - @pytest.mark.django_db def test_add_and_remove_groups_and_permissions_endpoint(api_client): # используем гарантированно существующие права @@ -183,9 +155,9 @@ def test_add_and_remove_groups_and_permissions_endpoint(api_client): pos = Position.objects.create(name="Aggregator") - user = _make_user("manager@example.com") + user = make_user("manager@example.com") api_client.force_authenticate(user=user) - _grant_perm(user, "employees.assign_position_groups") + grant_permission(user, "employees.assign_position_groups") # add_groups url_add = reverse("api:v1:positions-add-groups", args=[pos.id]) @@ -219,13 +191,12 @@ def test_add_and_remove_groups_and_permissions_endpoint(api_client): assert "groups_verbose" in resp.data assert any(g["name"] == "B" for g in resp.data["groups_verbose"]) - @pytest.mark.django_db def test_groups_payload_validation(api_client): pos = Position.objects.create(name="Validator") - user = _make_user("user@example.com") + user = make_user("user@example.com") api_client.force_authenticate(user=user) - _grant_perm(user, "employees.assign_position_groups") + grant_permission(user, "employees.assign_position_groups") url_set = reverse("api:v1:positions-set-groups", args=[pos.id]) diff --git a/backend/tests/api/v1/employees/test_role_assignment.py b/backend/tests/api/v1/employees/test_role_assignment.py index b518e93c..7fa660ad 100644 --- a/backend/tests/api/v1/employees/test_role_assignment.py +++ b/backend/tests/api/v1/employees/test_role_assignment.py @@ -11,22 +11,12 @@ from employees.models import ( EmployeeDepartment, RoleAssignment, ) from tests.conftest import _unique_phone +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results User = get_user_model() # В этом проекте User == Employee (кастомная модель) Employee = User -@pytest.fixture -def make_user(email: str, is_staff: bool = False) -> User: - """Создаёт пользователя (Employee).""" - return User.objects.create_user( - email=email, - password="pwd12345", - phone_number=_unique_phone(), - is_staff=is_staff, - send_activation_email=False, - ) - def make_dept(name: str, head: User = None) -> Department: """Создаёт отдел.""" return Department.objects.create(name=name, head=head) diff --git a/backend/tests/api/v1/employees/test_skills.py b/backend/tests/api/v1/employees/test_skills.py index ad8e9d5b..e81554ef 100644 --- a/backend/tests/api/v1/employees/test_skills.py +++ b/backend/tests/api/v1/employees/test_skills.py @@ -5,6 +5,7 @@ from employees.models import Employee, Skill from rest_framework import status from tests.conftest import _unique_email, _unique_phone from tests.test_config import DEFAULT_PASSWORD +from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results # --- helpers --- @@ -24,24 +25,8 @@ def _mk_user(staff=False, superuser=False) -> Employee: u.save(update_fields=["is_staff", "is_superuser", "email_verified", "is_active"]) return u - -def _grant_perm(user: Employee, perm_code: str): - app_label, codename = perm_code.split(".", 1) - p = Permission.objects.get(content_type__app_label=app_label, codename=codename) - user.user_permissions.add(p) - user.save() - for a in ("_perm_cache", "_user_perm_cache", "_group_perm_cache"): - if hasattr(user, a): - try: - delattr(user, a) - except: - pass - return p - - # --- tests --- - @pytest.mark.django_db def test_list_search_ordering_requires_auth(api_client): url = reverse("api:v1:skills-list") @@ -71,7 +56,6 @@ def test_list_search_ordering_requires_auth(api_client): names = [r["name"] for r in resp.data] assert names == sorted(names, reverse=True) - @pytest.mark.django_db def test_create_allowed_for_regular_user(api_client): user = _mk_user() @@ -81,7 +65,6 @@ def test_create_allowed_for_regular_user(api_client): assert resp.status_code == 201 assert Skill.objects.filter(name="Go").exists() - @pytest.mark.django_db def test_update_delete_permissions(api_client): user = _mk_user() @@ -98,19 +81,18 @@ def test_update_delete_permissions(api_client): assert resp.status_code == 403 # с правом change_skill -> PATCH разрешён - _grant_perm(user, "employees.change_skill") + grant_permission(user, "employees.change_skill") resp = api_client.patch(url_det, {"name": "Docker+"}, format="json") assert resp.status_code == 200 s.refresh_from_db() assert s.name == "Docker+" # с правом delete_skill -> DELETE разрешён - _grant_perm(user, "employees.delete_skill") + grant_permission(user, "employees.delete_skill") resp = api_client.delete(url_det) assert resp.status_code == 204 assert not Skill.objects.filter(id=s.id).exists() - @pytest.mark.django_db def test_staff_bypasses_permissions(api_client): staff = _mk_user(staff=True) @@ -123,7 +105,6 @@ def test_staff_bypasses_permissions(api_client): ) assert api_client.delete(url_det).status_code == 204 - @pytest.mark.django_db def test_bulk_create_by_regular_user(api_client): user = _mk_user() @@ -139,7 +120,6 @@ def test_bulk_create_by_regular_user(api_client): created_names = {row["name"] for row in data["created"]} assert created_names == {"Python", "SQL"} - @pytest.mark.django_db def test_merge_requires_perm_and_reassigns(api_client): user = _mk_user() @@ -161,7 +141,7 @@ def test_merge_requires_perm_and_reassigns(api_client): ) assert resp.status_code == 403 - _grant_perm(user, "employees.change_skill") + grant_permission(user, "employees.change_skill") resp = api_client.post( url, {"source_id": src.id, "target_id": dst.id}, format="json" ) @@ -174,12 +154,11 @@ def test_merge_requires_perm_and_reassigns(api_client): id=src.id ).exists() # по умолчанию delete_source=True - @pytest.mark.django_db def test_merge_same_id_and_invalid_ids(api_client): user = _mk_user() api_client.force_authenticate(user=user) - _grant_perm(user, "employees.change_skill") + grant_permission(user, "employees.change_skill") s = Skill.objects.create(name="A") url = reverse("api:v1:skills-merge") @@ -191,12 +170,11 @@ def test_merge_same_id_and_invalid_ids(api_client): resp = api_client.post(url, {"source_id": 999999, "target_id": s.id}, format="json") assert resp.status_code in (404,) - @pytest.mark.django_db def test_merge_without_reassign_or_delete(api_client): user = _mk_user() api_client.force_authenticate(user=user) - _grant_perm(user, "employees.change_skill") + grant_permission(user, "employees.change_skill") src = Skill.objects.create(name="ML") dst = Skill.objects.create(name="Machine Learning") @@ -223,12 +201,11 @@ def test_merge_without_reassign_or_delete(api_client): # источник не удалили assert Skill.objects.filter(id=src.id).exists() - @pytest.mark.django_db def test_merge_idempotent_if_employee_already_has_target(api_client): user = _mk_user() api_client.force_authenticate(user=user) - _grant_perm(user, "employees.change_skill") + grant_permission(user, "employees.change_skill") src = Skill.objects.create(name="Golang") dst = Skill.objects.create(name="Go") diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index cc8dc263..cb8d9177 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -11,7 +11,8 @@ import itertools import mimetypes import os -from datetime import datetime, date +from datetime import date, datetime + import pytest from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission @@ -19,17 +20,11 @@ from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile from employees.models import Department, DepartmentRole, EmployeeDepartment from rest_framework.test import APIClient - -from tests.test_config import ( - DEFAULT_PASSWORD, - DEFAULT_TEST_PHONE_PREFIX, - TEST_EMAIL_DOMAIN, - TEST_IMAGE_1X1_PNG_B64, - TEST_IMAGE_DATA_URI, -) - # Импортируем LDAP фикстуры from tests.ldap_fixtures import * # noqa +from tests.test_config import (DEFAULT_PASSWORD, DEFAULT_TEST_PHONE_PREFIX, + TEST_EMAIL_DOMAIN, TEST_IMAGE_1X1_PNG_B64, + TEST_IMAGE_DATA_URI) pytestmark = pytest.mark.django_db @@ -61,7 +56,8 @@ def _unique_email(prefix: str = "user") -> str: @pytest.fixture(autouse=True) def _fast_hashers_and_email(settings, tmp_path): # быстрые хеши паролей и локальная почта - settings.PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] + settings.PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.MD5PasswordHasher"] settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # изоляция медиа media_root = tmp_path / "test_media" @@ -103,11 +99,11 @@ def auth_client_factory(): def user_factory(): """ Фабрика для создания пользователей. - + Пример использования: user = user_factory(email="x@example.com", staff=False, superuser=False, verified=True, active=True, **extra) - + Создаёт пользователя напрямую (без менеджера create_user, чтобы не слать письма). """ @@ -123,7 +119,7 @@ def user_factory(): ): if email is None: email = _unique_email() - + u = User.objects.create( email=email, phone_number=extra.pop("phone_number", _unique_phone()), diff --git a/backend/tests/ldap_fixtures.py b/backend/tests/ldap_fixtures.py index 0920d26b..1e6ac08d 100644 --- a/backend/tests/ldap_fixtures.py +++ b/backend/tests/ldap_fixtures.py @@ -7,6 +7,7 @@ - ldap_config: конфигурация LDAP для тестов """ import os + import pytest from django.conf import settings @@ -15,13 +16,13 @@ from django.conf import settings def ldap_available(): """ Проверяет доступность LDAP сервера. - + Returns: bool: True если LDAP доступен и настроен """ if not settings.LDAP_ENABLED: return False - + try: from employees.ldap.infrastructure.connections import _ldap with _ldap() as conn: @@ -77,20 +78,20 @@ def ensure_ldap_disabled(settings): def ldap_cleanup(): """ Cleanup фикстура для удаления тестовых данных из LDAP после теста. - + Usage: def test_something(ldap_cleanup): # создаем объекты в LDAP ldap_cleanup.add_for_deletion(dn) """ cleanup_list = [] - + class Cleanup: def add_for_deletion(self, dn): cleanup_list.append(dn) - + yield Cleanup() - + # Cleanup после теста if cleanup_list and settings.LDAP_ENABLED: try: @@ -129,9 +130,9 @@ def pytest_collection_modifyitems(config, items): ldap_available = conn.bind() except: pass - + skip_ldap = pytest.mark.skip(reason="LDAP сервер недоступен") - + for item in items: if "ldap_required" in item.keywords and not ldap_available: item.add_marker(skip_ldap)