Хвосты аудита: тесты 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:
+104
-2
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user