Хвосты аудита: тесты tasks/employees + чистка зависимостей

- apps/tasks/tests.py (10): TaskQuerySet.for_user (видимость по ролям),
  is_participant (кто меняет статус: автор/відповідальний/співвиконавець,
  но не спостерігач), is_overdue, set_status
- apps/employees/tests.py (10): clean() (разделение фармацевт/офис, даты,
  самоуправление), has_perm с override (grant/revoke/неактивные/
  просроченные/будущие), is_currently_active
- requirements.txt: убран дубль import-export==0.3.1 (используется
  django-import-export==4.4.0)
- удалён мёртвый пустой db.sqlite3 (в .gitignore, не трекался)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 10:45:16 +03:00
parent 01f0c6f002
commit ab29cd5db8
3 changed files with 207 additions and 3 deletions
+104 -2
View File
@@ -1,3 +1,105 @@
from django.test import TestCase
import datetime
# Create your tests here.
from django.contrib.auth.models import Group, Permission
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from apps.employees.models import Employee, EmployeePermissionOverride
def make_employee(username, **kw):
kw.setdefault('first_name', 'Иван')
kw.setdefault('last_name', 'Иванов')
kw.setdefault('hire_date', datetime.date(2020, 1, 1))
kw.setdefault('mattermost_sync_enabled', False)
kw.setdefault('ad_sync_enabled', False)
return Employee.objects.create_user(username=username, **kw)
class EmployeeCleanTests(TestCase):
"""Валидация clean(): разделение фармацевтов и офисных/складских, даты, руководитель."""
def test_pharmacy_requires_pharmacy(self):
e = make_employee('e1', user_location_type='pharmacy')
with self.assertRaises(ValidationError) as cm:
e.clean()
self.assertIn('user_pharmacy', cm.exception.message_dict)
def test_office_requires_department(self):
e = make_employee('e2', user_location_type='office')
with self.assertRaises(ValidationError) as cm:
e.clean()
self.assertIn('department', cm.exception.message_dict)
def test_termination_before_hire_rejected(self):
e = make_employee('e3', hire_date=datetime.date(2022, 1, 1),
termination_date=datetime.date(2021, 1, 1))
with self.assertRaises(ValidationError) as cm:
e.clean()
self.assertIn('termination_date', cm.exception.message_dict)
def test_cannot_be_own_manager(self):
e = make_employee('e4')
e.manager = e
with self.assertRaises(ValidationError) as cm:
e.clean()
self.assertIn('manager', cm.exception.message_dict)
class EmployeePermissionOverrideTests(TestCase):
"""has_perm с индивидуальными override: grant выдаёт, revoke отзывает даже
групповое право; неактивные/просроченные/будущие — игнорируются."""
@classmethod
def setUpTestData(cls):
cls.perm = Permission.objects.first()
cls.perm_str = f'{cls.perm.content_type.app_label}.{cls.perm.codename}'
def _override(self, employee, **kw):
kw.setdefault('override_type', 'grant')
return EmployeePermissionOverride.objects.create(
employee=employee, permission=self.perm, **kw)
def test_grant_override_gives_permission(self):
e = make_employee('g1')
self._override(e, override_type='grant')
self.assertTrue(Employee.objects.get(pk=e.pk).has_perm(self.perm_str))
def test_revoke_override_removes_group_permission(self):
e = make_employee('r1')
group = Group.objects.create(name='grp')
group.permissions.add(self.perm)
e.groups.add(group)
# базово право есть через группу
self.assertTrue(Employee.objects.get(pk=e.pk).has_perm(self.perm_str))
self._override(e, override_type='revoke')
self.assertFalse(Employee.objects.get(pk=e.pk).has_perm(self.perm_str))
def test_inactive_override_ignored(self):
e = make_employee('i1')
self._override(e, override_type='grant', is_active=False)
self.assertFalse(Employee.objects.get(pk=e.pk).has_perm(self.perm_str))
def test_expired_override_ignored(self):
e = make_employee('x1')
self._override(e, override_type='grant',
valid_until=timezone.now().date() - datetime.timedelta(days=1))
self.assertFalse(Employee.objects.get(pk=e.pk).has_perm(self.perm_str))
def test_future_override_ignored(self):
e = make_employee('f1')
self._override(e, override_type='grant',
valid_from=timezone.now().date() + datetime.timedelta(days=1))
self.assertFalse(Employee.objects.get(pk=e.pk).has_perm(self.perm_str))
def test_is_currently_active(self):
e = make_employee('c1')
today = timezone.now().date()
o = EmployeePermissionOverride(
employee=e, permission=self.perm, override_type='grant',
valid_from=today - datetime.timedelta(days=1),
valid_until=today + datetime.timedelta(days=1))
self.assertTrue(o.is_currently_active())
o.is_active = False
self.assertFalse(o.is_currently_active())
+103
View File
@@ -0,0 +1,103 @@
import datetime
from django.test import TestCase
from django.utils import timezone
from apps.employees.models import Employee
from apps.tasks.models import Task
def make_employee(username):
# sync-флаги выключены: тестам не нужны сигналы AD/Mattermost
return Employee.objects.create_user(
username=username, first_name='T', last_name='User',
hire_date=datetime.date(2020, 1, 1),
mattermost_sync_enabled=False, ad_sync_enabled=False,
)
class TaskVisibilityTests(TestCase):
"""TaskQuerySet.for_user — задачу видят автор, відповідальний,
співвиконавець и спостерігач; посторонний — нет."""
@classmethod
def setUpTestData(cls):
cls.creator = make_employee('creator')
cls.responsible = make_employee('responsible')
cls.co = make_employee('co')
cls.observer = make_employee('observer')
cls.outsider = make_employee('outsider')
cls.task = Task.objects.create(title='T', creator=cls.creator)
cls.task.responsible.add(cls.responsible)
cls.task.co_executors.add(cls.co)
cls.task.observers.add(cls.observer)
def test_for_user_includes_all_roles(self):
for user in (self.creator, self.responsible, self.co, self.observer):
self.assertIn(self.task, Task.objects.for_user(user), msg=user.username)
def test_for_user_excludes_outsider(self):
self.assertNotIn(self.task, Task.objects.for_user(self.outsider))
def test_for_user_distinct(self):
# один и тот же пользователь в двух ролях → одна строка, не дубль
self.task.co_executors.add(self.responsible)
self.assertEqual(Task.objects.for_user(self.responsible).count(), 1)
class TaskParticipantTests(TestCase):
"""is_participant — статус меняют автор/відповідальний/співвиконавець,
спостерігач и посторонний — нет."""
@classmethod
def setUpTestData(cls):
cls.creator = make_employee('creator')
cls.responsible = make_employee('responsible')
cls.co = make_employee('co')
cls.observer = make_employee('observer')
cls.outsider = make_employee('outsider')
cls.task = Task.objects.create(title='T', creator=cls.creator)
cls.task.responsible.add(cls.responsible)
cls.task.co_executors.add(cls.co)
cls.task.observers.add(cls.observer)
def test_participants_can_change_status(self):
self.assertTrue(self.task.is_participant(self.creator))
self.assertTrue(self.task.is_participant(self.responsible))
self.assertTrue(self.task.is_participant(self.co))
def test_observer_and_outsider_cannot(self):
self.assertFalse(self.task.is_participant(self.observer))
self.assertFalse(self.task.is_participant(self.outsider))
class TaskStatusTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = make_employee('u')
def _task(self, **kw):
return Task.objects.create(title='t', creator=self.user, **kw)
def test_overdue_when_past_due_and_open(self):
t = self._task(due_date=timezone.localdate() - datetime.timedelta(days=1))
self.assertTrue(t.is_overdue)
def test_not_overdue_when_done(self):
t = self._task(status='done',
due_date=timezone.localdate() - datetime.timedelta(days=1))
self.assertFalse(t.is_overdue)
def test_not_overdue_without_due_date(self):
self.assertFalse(self._task().is_overdue)
def test_set_status_done_sets_completed_at(self):
t = self._task()
t.set_status('done')
self.assertEqual(t.status, 'done')
self.assertIsNotNone(t.completed_at)
def test_set_status_non_done_clears_completed_at(self):
t = self._task(status='done', completed_at=timezone.now())
t.set_status('in_progress')
self.assertIsNone(t.completed_at)
-1
View File
@@ -34,7 +34,6 @@ h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.11
import-export==0.3.1
kombu==5.5.4
ldap3==2.9.1
mattermostautodriver==11.7.2