Corp/backend/tests/test_schedule_permissions.py
2026-09-07 19:43:48 +03:00

1089 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# backend/tests/test_schedule_permissions.py
"""
Тесты для системы прав доступа календарей django-scheduler.
"""
from datetime import timedelta
from types import SimpleNamespace
import pytest
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient
from schedule.models import (
Calendar,
CalendarRelation,
Event,
EventRelation,
Occurrence,
Rule,
)
from scheduling.models import CalendarBinding
User = get_user_model()
@pytest.fixture
def api_client():
"""API клиент для тестов."""
return APIClient()
@pytest.fixture
def user1(db):
"""Первый тестовый пользователь."""
return User.objects.create_user(
username="user1",
email="user1@example.com",
password="testpass123",
first_name="User",
last_name="One",
phone_number="+79991234567", # Правильное имя параметра
is_active=True,
)
@pytest.fixture
def user2(db):
"""Второй тестовый пользователь."""
return User.objects.create_user(
username="user2",
email="user2@example.com",
password="testpass123",
first_name="User",
last_name="Two",
phone_number="+79991234568",
is_active=True,
)
@pytest.fixture
def user3(db):
"""Третий тестовый пользователь."""
return User.objects.create_user(
username="user3",
email="user3@example.com",
password="testpass123",
first_name="User",
last_name="Three",
phone_number="+79991234569",
is_active=True,
)
@pytest.fixture
def admin_user(db):
"""Администратор для проверки scope календарей."""
return User.objects.create_user(
username="admin",
email="admin@example.com",
password="testpass123",
first_name="Admin",
last_name="User",
phone_number="+79991234570",
is_staff=True,
is_superuser=True,
email_verified=True,
)
@pytest.mark.django_db
@pytest.mark.parametrize(
("field", "handler_name"),
[
("notify_event_created", "notify_event_created"),
("notify_event_changed", "notify_event_changed"),
("notify_event_cancelled", "notify_event_cancelled"),
],
)
def test_disabled_calendar_notification_rule_stops_dispatch(
monkeypatch, field, handler_name
):
from scheduling.notifications import handlers
calendar = Calendar.objects.create(name="Silent", slug=f"silent-{field}")
CalendarBinding.objects.create(calendar=calendar, **{field: False})
event = SimpleNamespace(calendar=calendar)
monkeypatch.setattr(
handlers,
"get_event_recipients",
lambda _event: pytest.fail("Recipients must not be loaded for a disabled rule"),
)
getattr(handlers, handler_name)(event)
@pytest.mark.django_db
class TestCalendarPermissions:
"""Тесты прав доступа к календарям."""
def test_create_calendar_creates_owner_relation(self, api_client, user1):
"""При создании календаря автоматически создается owner relation."""
api_client.force_authenticate(user=user1)
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
assert response.status_code == status.HTTP_201_CREATED
calendar_id = response.data["id"]
# Проверяем, что создался CalendarRelation с owner
ct = ContentType.objects.get_for_model(User)
relation = CalendarRelation.objects.get(
calendar_id=calendar_id, content_type=ct, object_id=user1.id
)
assert relation.distinction == "owner"
assert relation.inheritable is True
def test_get_calendars_shows_only_accessible(self, api_client, user1, user2):
"""Пользователь видит только свои календари."""
api_client.force_authenticate(user=user1)
# user1 создает календарь
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "User1 Calendar",
},
)
assert response.status_code == status.HTTP_201_CREATED
# user2 создает свой календарь
api_client.force_authenticate(user=user2)
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "User2 Calendar",
},
)
assert response.status_code == status.HTTP_201_CREATED
# user1 видит только свой календарь
api_client.force_authenticate(user=user1)
response = api_client.get("/api/v1/schedule/calendars/")
assert response.status_code == status.HTTP_200_OK
if isinstance(response.data, dict) and "results" in response.data:
calendars = response.data["results"]
else:
calendars = response.data
assert len(calendars) == 1
assert calendars[0]["name"] == "User1 Calendar"
def test_owner_can_make_calendar_available_to_entire_company(
self, api_client, user1, user2
):
api_client.force_authenticate(user=user1)
create_response = api_client.post(
"/api/v1/schedule/calendars/",
{"name": "Company Calendar"},
)
calendar_id = create_response.data["id"]
api_client.force_authenticate(user=user2)
assert api_client.get("/api/v1/schedule/calendars/").data == []
api_client.force_authenticate(user=user1)
access_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/access-scope/",
{"access_scope": "all"},
)
assert access_response.status_code == status.HTTP_200_OK
assert access_response.data["access_scope"] == "all"
event_response = api_client.post(
"/api/v1/schedule/events/",
{
"title": "Company event",
"calendar": calendar_id,
"start": timezone.now().isoformat(),
"end": (timezone.now() + timedelta(hours=1)).isoformat(),
},
)
assert event_response.status_code == status.HTTP_201_CREATED
api_client.force_authenticate(user=user2)
list_response = api_client.get("/api/v1/schedule/calendars/")
assert [calendar["id"] for calendar in list_response.data] == [calendar_id]
assert list_response.data[0]["user_role"] == "viewer"
assert list_response.data[0]["can_create_events"] is False
events_response = api_client.get(
f"/api/v1/schedule/events/?calendar={calendar_id}"
)
assert [event["id"] for event in events_response.data["results"]] == [
event_response.data["id"]
]
forbidden_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/access-scope/",
{"access_scope": "restricted"},
)
assert forbidden_response.status_code == status.HTTP_403_FORBIDDEN
api_client.force_authenticate(user=user1)
restricted_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/access-scope/",
{"access_scope": "restricted"},
)
assert restricted_response.status_code == status.HTTP_200_OK
api_client.force_authenticate(user=user2)
assert api_client.get("/api/v1/schedule/calendars/").data == []
def test_add_participant_to_calendar(self, api_client, user1, user2):
"""Owner может добавить участника к календарю."""
api_client.force_authenticate(user=user1)
# Создаем календарь
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Shared Calendar",
},
)
calendar_id = response.data["id"]
# Добавляем user2 как viewer
response = api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user2.id, "role": "viewer"},
)
assert response.status_code == status.HTTP_201_CREATED
assert response.data["user"]["id"] == user2.id
assert response.data["distinction"] == "viewer"
# Теперь user2 должен видеть календарь
api_client.force_authenticate(user=user2)
response = api_client.get("/api/v1/schedule/calendars/")
if isinstance(response.data, dict) and "results" in response.data:
calendars = response.data["results"]
else:
calendars = response.data
assert len(calendars) == 1
assert calendars[0]["id"] == calendar_id
def test_cannot_add_inactive_participant_to_calendar(
self, api_client, user1, user2
):
"""Уволенного сотрудника нельзя заново добавить в календарь."""
api_client.force_authenticate(user=user1)
calendar_response = api_client.post(
"/api/v1/schedule/calendars/",
{"name": "Active participants only"},
)
user2.is_active = False
user2.save(update_fields=["is_active"])
response = api_client.post(
f"/api/v1/schedule/calendars/{calendar_response.data['id']}/add-participant/",
{"user_id": user2.id, "role": "viewer"},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "user_id" in response.data
def test_non_owner_cannot_add_participant(self, api_client, user1, user2, user3):
"""Не-owner не может добавлять участников."""
api_client.force_authenticate(user=user1)
# user1 создает календарь
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = response.data["id"]
# Добавляем user2 как editor
api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user2.id, "role": "editor"},
)
# user2 пытается добавить user3 - должно быть запрещено
api_client.force_authenticate(user=user2)
response = api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user3.id, "role": "viewer"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_remove_participant_from_calendar(self, api_client, user1, user2):
"""Owner может удалить участника из календаря."""
api_client.force_authenticate(user=user1)
# Создаем календарь и добавляем user2
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = response.data["id"]
api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user2.id, "role": "viewer"},
)
# Удаляем user2
response = api_client.delete(
f"/api/v1/schedule/calendars/{calendar_id}/remove-participant/{user2.id}/"
)
assert response.status_code == status.HTTP_204_NO_CONTENT
# user2 больше не видит календарь
api_client.force_authenticate(user=user2)
response = api_client.get("/api/v1/schedule/calendars/")
if isinstance(response.data, dict) and "results" in response.data:
calendars = response.data["results"]
else:
calendars = response.data
assert len(calendars) == 0
def test_cannot_remove_owner(self, api_client, user1):
"""Нельзя удалить owner из календаря."""
api_client.force_authenticate(user=user1)
# Создаем календарь
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = response.data["id"]
# Пытаемся удалить себя (owner)
response = api_client.delete(
f"/api/v1/schedule/calendars/{calendar_id}/remove-participant/{user1.id}/"
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_list_participants(self, api_client, user1, user2, user3):
"""Получение списка участников календаря."""
api_client.force_authenticate(user=user1)
# Создаем календарь
response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = response.data["id"]
# Добавляем участников
api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user2.id, "role": "editor"},
)
api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user3.id, "role": "viewer"},
)
# Получаем список участников
response = api_client.get(
f"/api/v1/schedule/calendars/{calendar_id}/participants/"
)
assert response.status_code == status.HTTP_200_OK
assert len(response.data) == 3 # owner + 2 участника
# Проверяем роли
roles = {p["user"]["id"]: p["distinction"] for p in response.data}
assert roles[user1.id] == "owner"
assert roles[user2.id] == "editor"
assert roles[user3.id] == "viewer"
def test_calendar_notification_settings_keep_defaults_and_allow_editor(
self, api_client, user1, user2, user3
):
api_client.force_authenticate(user=user1)
create_response = api_client.post(
"/api/v1/schedule/calendars/",
{"name": "Notification settings"},
)
calendar_id = create_response.data["id"]
api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user2.id, "role": "editor"},
)
api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user3.id, "role": "viewer"},
)
defaults_response = api_client.get(
f"/api/v1/schedule/calendars/{calendar_id}/notification-settings/"
)
assert defaults_response.status_code == status.HTTP_200_OK
assert defaults_response.data == {
"notify_event_created": True,
"notify_event_changed": True,
"notify_event_cancelled": True,
"can_manage": True,
}
api_client.force_authenticate(user=user2)
update_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/notification-settings/",
{"notify_event_changed": False},
format="json",
)
assert update_response.status_code == status.HTTP_200_OK
assert update_response.data["notify_event_changed"] is False
api_client.force_authenticate(user=user3)
forbidden_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/notification-settings/",
{"notify_event_created": False},
format="json",
)
assert forbidden_response.status_code == status.HTTP_403_FORBIDDEN
def test_owner_can_change_roles_but_cannot_remove_last_owner_role(
self, api_client, user1, user2
):
api_client.force_authenticate(user=user1)
create_response = api_client.post(
"/api/v1/schedule/calendars/",
{"name": "Role settings"},
)
calendar_id = create_response.data["id"]
api_client.post(
f"/api/v1/schedule/calendars/{calendar_id}/add-participant/",
{"user_id": user2.id, "role": "viewer"},
)
update_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/participant-role/{user2.id}/",
{"role": "editor"},
format="json",
)
assert update_response.status_code == status.HTTP_200_OK
assert update_response.data["distinction"] == "editor"
last_owner_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/participant-role/{user1.id}/",
{"role": "viewer"},
format="json",
)
assert last_owner_response.status_code == status.HTTP_400_BAD_REQUEST
assert "role" in last_owner_response.data
api_client.force_authenticate(user=user2)
forbidden_response = api_client.patch(
f"/api/v1/schedule/calendars/{calendar_id}/participant-role/{user1.id}/",
{"role": "viewer"},
format="json",
)
assert forbidden_response.status_code == status.HTTP_403_FORBIDDEN
def test_admin_list_scope_defaults_to_accessible_only(
self, api_client, admin_user, user1
):
"""По умолчанию админ видит свои доступные календари, а не весь реестр."""
api_client.force_authenticate(user=admin_user)
admin_calendar_response = api_client.post(
"/api/v1/schedule/calendars/", {"name": "Admin Calendar"}
)
assert admin_calendar_response.status_code == status.HTTP_201_CREATED
admin_calendar_id = admin_calendar_response.data["id"]
api_client.force_authenticate(user=user1)
other_calendar_response = api_client.post(
"/api/v1/schedule/calendars/", {"name": "Other Calendar"}
)
assert other_calendar_response.status_code == status.HTTP_201_CREATED
other_calendar_id = other_calendar_response.data["id"]
api_client.force_authenticate(user=admin_user)
default_response = api_client.get("/api/v1/schedule/calendars/")
default_calendars = default_response.data
assert {cal["id"] for cal in default_calendars} == {admin_calendar_id}
full_response = api_client.get("/api/v1/schedule/calendars/?scope=all")
full_calendars = full_response.data
assert {cal["id"] for cal in full_calendars} == {
admin_calendar_id,
other_calendar_id,
}
@pytest.mark.django_db
class TestRecurringEvents:
"""Тесты повторяющихся событий."""
def test_admin_event_scope_matches_calendar_scope(
self, api_client, admin_user, user1
):
"""У admin default list/occurrences не должны тянуть чужие календари."""
api_client.force_authenticate(user=admin_user)
admin_calendar_response = api_client.post(
"/api/v1/schedule/calendars/", {"name": "Admin Calendar"}
)
admin_calendar_id = admin_calendar_response.data["id"]
api_client.force_authenticate(user=user1)
other_calendar_response = api_client.post(
"/api/v1/schedule/calendars/", {"name": "Other Calendar"}
)
other_calendar_id = other_calendar_response.data["id"]
start = timezone.now().replace(microsecond=0, second=0)
range_end = start + timedelta(hours=2)
Event.objects.create(
calendar_id=admin_calendar_id,
title="Admin event",
start=start,
end=start + timedelta(hours=1),
creator=admin_user,
)
Event.objects.create(
calendar_id=other_calendar_id,
title="Other event",
start=start,
end=start + timedelta(hours=1),
creator=user1,
)
api_client.force_authenticate(user=admin_user)
default_events = api_client.get(
"/api/v1/schedule/events/",
{
"start": start.isoformat(),
"end": range_end.isoformat(),
},
)
assert default_events.status_code == status.HTTP_200_OK
default_events_list = (
default_events.data["results"]
if isinstance(default_events.data, dict)
else default_events.data
)
assert {event["title"] for event in default_events_list} == {"Admin event"}
all_events = api_client.get(
"/api/v1/schedule/events/",
{
"scope": "all",
"start": start.isoformat(),
"end": range_end.isoformat(),
},
)
assert all_events.status_code == status.HTTP_200_OK
all_events_list = (
all_events.data["results"]
if isinstance(all_events.data, dict)
else all_events.data
)
assert {event["title"] for event in all_events_list} == {
"Admin event",
"Other event",
}
default_occurrences = api_client.get(
"/api/v1/schedule/events/occurrences/",
{
"start": start.isoformat(),
"end": range_end.isoformat(),
},
)
assert default_occurrences.status_code == status.HTTP_200_OK
assert {event["title"] for event in default_occurrences.data} == {"Admin event"}
all_occurrences = api_client.get(
"/api/v1/schedule/events/occurrences/",
{
"scope": "all",
"start": start.isoformat(),
"end": range_end.isoformat(),
},
)
assert all_occurrences.status_code == status.HTTP_200_OK
assert {event["title"] for event in all_occurrences.data} == {
"Admin event",
"Other event",
}
def test_admin_accessible_scope_excludes_unrelated_department_occurrences(
self, api_client, admin_user, user1
):
"""Календарь отдела не становится доступным только из-за его типа."""
department_calendar = Calendar.objects.create(
name="Sales",
slug="department-sales",
)
CalendarBinding.objects.create(
calendar=department_calendar,
type=CalendarBinding.BindingType.DEPARTMENT,
)
rule = Rule.objects.create(
name="Department daily",
description="Department recurrence",
frequency="DAILY",
params="",
)
start = timezone.now().replace(microsecond=0, second=0)
range_end = start + timedelta(days=2)
Event.objects.create(
calendar=department_calendar,
title="Sales recurring event",
start=start,
end=start + timedelta(hours=1),
creator=user1,
rule=rule,
end_recurring_period=range_end,
)
api_client.force_authenticate(user=admin_user)
accessible_occurrences = api_client.get(
"/api/v1/schedule/events/occurrences/",
{
"start": start.isoformat(),
"end": range_end.isoformat(),
},
)
assert accessible_occurrences.status_code == status.HTTP_200_OK
assert accessible_occurrences.data == []
all_occurrences = api_client.get(
"/api/v1/schedule/events/occurrences/",
{
"scope": "all",
"start": start.isoformat(),
"end": range_end.isoformat(),
},
)
assert all_occurrences.status_code == status.HTTP_200_OK
assert {event["title"] for event in all_occurrences.data} == {
"Sales recurring event",
}
def test_create_rule(self, api_client, user1):
"""Создание правила повторения."""
api_client.force_authenticate(user=user1)
response = api_client.post(
"/api/v1/schedule/rules/",
{
"name": "Weekly",
"description": "Every week on Monday",
"frequency": "WEEKLY",
"params": "byweekday:0", # Monday
},
)
assert response.status_code == status.HTTP_201_CREATED
assert response.data["frequency"] == "WEEKLY"
assert response.data["frequency_display"] == "Еженедельно" # Django локализация
def test_create_recurring_event(self, api_client, user1):
"""Создание события с повторением."""
api_client.force_authenticate(user=user1)
# Создаем календарь
cal_response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = cal_response.data["id"]
# Создаем правило
rule_response = api_client.post(
"/api/v1/schedule/rules/",
{
"name": "Daily",
"description": "Every day",
"frequency": "DAILY",
"params": "",
},
)
rule_id = rule_response.data["id"]
# Создаем событие с правилом
from datetime import timedelta
from django.utils import timezone
start = timezone.now()
end = start + timedelta(hours=1)
end_recurring = start + timedelta(days=30)
response = api_client.post(
"/api/v1/schedule/events/",
{
"title": "Daily Meeting",
"description": "Team standup",
"start": start.isoformat(),
"end": end.isoformat(),
"calendar": calendar_id,
"rule": rule_id,
"end_recurring_period": end_recurring.isoformat(),
},
)
assert response.status_code == status.HTTP_201_CREATED
assert response.data["rule"] == rule_id
assert response.data["end_recurring_period"] is not None
def test_get_occurrences(self, api_client, user1):
"""Получение материализованных вхождений для recurring события."""
api_client.force_authenticate(user=user1)
# Создаем календарь
cal_response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = cal_response.data["id"]
# Создаем правило - еженедельно
rule_response = api_client.post(
"/api/v1/schedule/rules/",
{
"name": "Weekly",
"description": "Every week",
"frequency": "WEEKLY",
"params": "",
},
)
rule_id = rule_response.data["id"]
# Создаем событие
from datetime import timedelta
from django.utils import timezone
start = timezone.now()
end = start + timedelta(hours=1)
end_recurring = start + timedelta(weeks=4)
api_client.post(
"/api/v1/schedule/events/",
{
"title": "Weekly Meeting",
"start": start.isoformat(),
"end": end.isoformat(),
"calendar": calendar_id,
"rule": rule_id,
"end_recurring_period": end_recurring.isoformat(),
},
)
# Получаем occurrences
response = api_client.get(
"/api/v1/schedule/events/occurrences/",
{
"calendar": calendar_id,
"start": start.isoformat(),
"end": (start + timedelta(weeks=5)).isoformat(),
},
)
assert response.status_code == status.HTTP_200_OK
# Должно быть ~4 вхождения (еженедельно в течение 4 недель)
assert len(response.data) >= 4
@pytest.mark.django_db
class TestiCalendarExport:
"""Тесты экспорта в iCalendar формат."""
def test_export_calendar_to_ics(self, api_client, user1):
"""Экспорт календаря в .ics файл."""
api_client.force_authenticate(user=user1)
# Создаем календарь
cal_response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = cal_response.data["id"]
# Добавляем событие
from datetime import timedelta
from django.utils import timezone
start = timezone.now()
end = start + timedelta(hours=2)
api_client.post(
"/api/v1/schedule/events/",
{
"title": "Test Event",
"description": "Test Description",
"start": start.isoformat(),
"end": end.isoformat(),
"calendar": calendar_id,
},
)
# Экспортируем календарь
response = api_client.get(
f"/api/v1/schedule/calendars/{calendar_id}/export-ical/"
)
assert response.status_code == status.HTTP_200_OK
assert response["Content-Type"] == "text/calendar"
assert "Content-Disposition" in response
assert ".ics" in response["Content-Disposition"]
# Проверяем содержимое
content = response.content.decode("utf-8")
assert "BEGIN:VCALENDAR" in content
assert "BEGIN:VEVENT" in content
assert "Test Event" in content
assert "END:VEVENT" in content
assert "END:VCALENDAR" in content
@pytest.mark.django_db
class TestEventRelations:
"""Тесты участников событий."""
def test_add_participant_to_event(self, api_client, user1, user2):
"""Добавление участника к событию."""
api_client.force_authenticate(user=user1)
# Создаем календарь
cal_response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = cal_response.data["id"]
# Создаем событие
from datetime import timedelta
from django.utils import timezone
start = timezone.now()
end = start + timedelta(hours=1)
event_response = api_client.post(
"/api/v1/schedule/events/",
{
"title": "Meeting",
"start": start.isoformat(),
"end": end.isoformat(),
"calendar": calendar_id,
},
)
event_id = event_response.data["id"]
# Добавляем user2 как участника
ct = ContentType.objects.get_for_model(User)
response = api_client.post(
"/api/v1/schedule/relations/",
{
"event": event_id,
"content_type": ct.id,
"object_id": user2.id,
"distinction": "attendee",
},
)
assert response.status_code == status.HTTP_201_CREATED
assert response.data["event"] == event_id
def test_list_event_participants(self, api_client, user1, user2, user3):
"""Получение списка участников события."""
api_client.force_authenticate(user=user1)
# Создаем календарь и событие
cal_response = api_client.post(
"/api/v1/schedule/calendars/",
{
"name": "Test Calendar",
},
)
calendar_id = cal_response.data["id"]
from datetime import timedelta
from django.utils import timezone
start = timezone.now()
end = start + timedelta(hours=1)
event_response = api_client.post(
"/api/v1/schedule/events/",
{
"title": "Team Meeting",
"start": start.isoformat(),
"end": end.isoformat(),
"calendar": calendar_id,
},
)
event_id = event_response.data["id"]
# Добавляем участников
ct = ContentType.objects.get_for_model(User)
api_client.post(
"/api/v1/schedule/relations/",
{
"event": event_id,
"content_type": ct.id,
"object_id": user2.id,
"distinction": "attendee",
},
)
api_client.post(
"/api/v1/schedule/relations/",
{
"event": event_id,
"content_type": ct.id,
"object_id": user3.id,
"distinction": "attendee",
},
)
# Получаем список участников
response = api_client.get("/api/v1/schedule/relations/", {"event": event_id})
assert response.status_code == status.HTTP_200_OK
if isinstance(response.data, dict) and "results" in response.data:
relations = response.data["results"]
else:
relations = response.data
assert len(relations) == 2
@pytest.mark.django_db
class TestScheduleObjectBoundaries:
"""Низкоуровневые endpoints не обходят доступ к событию."""
def test_occurrences_relations_and_rules_respect_event_access(
self, api_client, user1, user2
):
calendar = Calendar.objects.create(
name="Private calendar",
slug="private-calendar",
)
user_ct = ContentType.objects.get_for_model(User)
CalendarRelation.objects.create(
calendar=calendar,
content_type=user_ct,
object_id=user1.id,
distinction="owner",
inheritable=True,
)
rule = Rule.objects.create(
name="Private daily",
description="Private recurrence",
frequency="DAILY",
params="",
)
start = timezone.now().replace(microsecond=0)
event = Event.objects.create(
calendar=calendar,
creator=user1,
rule=rule,
title="Private event",
start=start,
end=start + timedelta(hours=1),
end_recurring_period=start + timedelta(days=3),
)
occurrence = Occurrence.objects.create(
event=event,
title=event.title,
description="",
start=start,
end=start + timedelta(hours=1),
original_start=start,
original_end=start + timedelta(hours=1),
)
api_client.force_authenticate(user=user2)
occurrence_response = api_client.get("/api/v1/schedule/occurrences/")
assert occurrence_response.status_code == status.HTTP_200_OK
occurrence_results = (
occurrence_response.data.get("results", [])
if isinstance(occurrence_response.data, dict)
else occurrence_response.data
)
assert all(item["id"] != occurrence.id for item in occurrence_results)
relation_response = api_client.get(
"/api/v1/schedule/relations/", {"event": event.id}
)
assert relation_response.status_code == status.HTTP_200_OK
relation_results = (
relation_response.data.get("results", [])
if isinstance(relation_response.data, dict)
else relation_response.data
)
assert relation_results == []
forbidden_relation = api_client.post(
"/api/v1/schedule/relations/",
{
"event": event.id,
"content_type": user_ct.id,
"object_id": user2.id,
"distinction": "attendee",
},
)
assert forbidden_relation.status_code == status.HTTP_403_FORBIDDEN
forbidden_rule_update = api_client.patch(
f"/api/v1/schedule/rules/{rule.id}/",
{"description": "Leaked update"},
format="json",
)
assert forbidden_rule_update.status_code == status.HTTP_403_FORBIDDEN
EventRelation.objects.create(
event=event,
content_type=user_ct,
object_id=user2.id,
distinction="attendee",
)
assert (
api_client.get(f"/api/v1/schedule/events/{event.id}/").status_code
== status.HTTP_200_OK
)
visible_occurrences = api_client.get(
"/api/v1/schedule/occurrences/", {"event": event.id}
)
visible_results = (
visible_occurrences.data.get("results", [])
if isinstance(visible_occurrences.data, dict)
else visible_occurrences.data
)
assert any(item["id"] == occurrence.id for item in visible_results)