mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-17 21:25:41 +02:00
✨(api) add endpoint to manage ThreadAccess
Allows creating, updating, and deleting access to threads based on user role.
This commit is contained in:
@@ -276,3 +276,70 @@ class IsAllowedToCreateMessage(IsAuthenticated):
|
||||
# return True
|
||||
#
|
||||
# return False
|
||||
|
||||
|
||||
class IsAllowedToManageThreadAccess(IsAuthenticated):
|
||||
"""Permission class for access to create, update, delete and list thread accesses."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
# Get thread_id from URL kwargs instead of query params/data
|
||||
thread_id = view.kwargs.get("thread_id")
|
||||
if not thread_id:
|
||||
return False
|
||||
|
||||
# if create action, check if user has admin/editor access to the mailbox and the thread access role is editor
|
||||
if view.action == "create":
|
||||
# authenticated user wants to create a thread access for a specific thread
|
||||
# check if user has admin/editor access to the mailbox and the thread access role is editor already exists for them
|
||||
return (
|
||||
models.ThreadAccess.objects.select_related("mailbox")
|
||||
.filter(
|
||||
thread_id=thread_id,
|
||||
mailbox__accesses__user=request.user,
|
||||
mailbox__accesses__role__in=[
|
||||
enums.MailboxRoleChoices.ADMIN,
|
||||
enums.MailboxRoleChoices.EDITOR,
|
||||
],
|
||||
role=enums.ThreadAccessRoleChoices.EDITOR,
|
||||
)
|
||||
.exists()
|
||||
)
|
||||
elif view.action == "list":
|
||||
# list is only allowed for a user with access to the thread
|
||||
return (
|
||||
models.ThreadAccess.objects.select_related("mailbox")
|
||||
.filter(
|
||||
thread_id=thread_id,
|
||||
mailbox__accesses__user=request.user,
|
||||
mailbox__accesses__role__in=[
|
||||
enums.MailboxRoleChoices.ADMIN,
|
||||
enums.MailboxRoleChoices.EDITOR,
|
||||
],
|
||||
role=enums.ThreadAccessRoleChoices.EDITOR,
|
||||
)
|
||||
.exists()
|
||||
)
|
||||
else:
|
||||
return True # to proceed to object-level checks
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""Check if user has permission to access the specific object (ThreadAccess).
|
||||
Manage retrieve, update, destroy actions here.
|
||||
"""
|
||||
# Verify the thread access belongs to the thread in the URL
|
||||
if str(obj.thread.id) != view.kwargs.get("thread_id"):
|
||||
return False
|
||||
|
||||
return (
|
||||
models.ThreadAccess.objects.select_related("mailbox")
|
||||
.filter(
|
||||
thread=obj.thread,
|
||||
mailbox__accesses__user=request.user,
|
||||
mailbox__accesses__role__in=[
|
||||
enums.MailboxRoleChoices.ADMIN,
|
||||
enums.MailboxRoleChoices.EDITOR,
|
||||
],
|
||||
role=enums.ThreadAccessRoleChoices.EDITOR,
|
||||
)
|
||||
.exists()
|
||||
)
|
||||
|
||||
@@ -279,3 +279,12 @@ class MessageSerializer(serializers.ModelSerializer):
|
||||
"is_trashed",
|
||||
]
|
||||
read_only_fields = fields # Mark all as read-only
|
||||
|
||||
|
||||
class ThreadAccessSerializer(serializers.ModelSerializer):
|
||||
"""Serialize thread access information."""
|
||||
|
||||
class Meta:
|
||||
model = models.ThreadAccess
|
||||
fields = ["id", "thread", "mailbox", "role", "created_at", "updated_at"]
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""API ViewSet for ThreadAccess model."""
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
extend_schema,
|
||||
)
|
||||
from rest_framework import mixins, viewsets
|
||||
|
||||
from core import models
|
||||
|
||||
from .. import permissions, serializers
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=["thread-access"],
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="mailbox_id",
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
description="Filter thread accesses by mailbox ID.",
|
||||
required=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
class ThreadAccessViewSet(
|
||||
viewsets.GenericViewSet,
|
||||
mixins.ListModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
):
|
||||
"""ViewSet for ThreadAccess model."""
|
||||
|
||||
serializer_class = serializers.ThreadAccessSerializer
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
permissions.IsAllowedToManageThreadAccess,
|
||||
]
|
||||
lookup_field = "id"
|
||||
lookup_url_kwarg = "id"
|
||||
queryset = models.ThreadAccess.objects.all()
|
||||
|
||||
def get_queryset(self):
|
||||
"""Restrict results to thread accesses for the specified thread."""
|
||||
# Get thread_id from URL kwargs (provided by nested router)
|
||||
thread_id = self.kwargs.get("thread_id")
|
||||
if not thread_id:
|
||||
return models.ThreadAccess.objects.none()
|
||||
|
||||
# Filter by thread_id from URL
|
||||
queryset = self.queryset.filter(thread_id=thread_id)
|
||||
|
||||
# Optional mailbox filter
|
||||
mailbox_id = self.request.GET.get("mailbox_id")
|
||||
if mailbox_id:
|
||||
queryset = queryset.filter(mailbox_id=mailbox_id)
|
||||
return queryset.distinct()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Create a new thread access."""
|
||||
request.data["thread"] = self.kwargs.get("thread_id")
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""List thread accesses for the specified thread."""
|
||||
return super().list(request, *args, **kwargs)
|
||||
@@ -21,6 +21,13 @@ class MailboxRoleChoices(models.TextChoices):
|
||||
ADMIN = "admin", _("Admin")
|
||||
|
||||
|
||||
class ThreadAccessRoleChoices(models.TextChoices):
|
||||
"""Defines the possible roles a mailbox can have to access to a thread."""
|
||||
|
||||
VIEWER = "viewer", _("Viewer")
|
||||
EDITOR = "editor", _("Editor")
|
||||
|
||||
|
||||
class MessageRecipientTypeChoices(models.TextChoices):
|
||||
"""Defines the possible types of message recipients."""
|
||||
|
||||
@@ -29,13 +36,6 @@ class MessageRecipientTypeChoices(models.TextChoices):
|
||||
BCC = "bcc", _("Bcc")
|
||||
|
||||
|
||||
class ThreadAccessRoleChoices(models.TextChoices):
|
||||
"""Defines the possible roles a mailbox can have to access to a thread."""
|
||||
|
||||
VIEWER = "viewer", _("Viewer")
|
||||
EDITOR = "editor", _("Editor")
|
||||
|
||||
|
||||
THREAD_STATS_FIELDS_MAP = {
|
||||
"unread": "count_unread",
|
||||
"trashed": "count_trashed",
|
||||
|
||||
@@ -187,7 +187,6 @@ class TestApiDraftAndSendMessage:
|
||||
assert sent_message.thread.sender_names == [sent_message.sender.name]
|
||||
assert sent_message.thread.messaged_at is not None
|
||||
|
||||
|
||||
@patch("core.mda.outbound.send_outbound_message")
|
||||
def test_draft_and_send_message_success_delegated_access(
|
||||
self, mock_send_outbound_message, mailbox, authenticated_user, send_url
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
"""Tests for the ThreadAccess API endpoints."""
|
||||
|
||||
import uuid
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
import pytest
|
||||
from rest_framework import status
|
||||
|
||||
from core import enums, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def get_thread_access_url(thread_id, access_id=None):
|
||||
"""Helper function to get the thread access URL."""
|
||||
if access_id:
|
||||
return reverse("thread-access-detail", args=[thread_id, access_id])
|
||||
return reverse("thread-access-list", args=[thread_id])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mailbox_with_access():
|
||||
"""Create a mailbox with access for a user."""
|
||||
user = factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=enums.MailboxRoleChoices.ADMIN,
|
||||
)
|
||||
return user, mailbox
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def thread_with_editor_access(mailbox_with_access):
|
||||
"""Create a thread with access for a mailbox."""
|
||||
user, mailbox = mailbox_with_access
|
||||
thread = factories.ThreadFactory()
|
||||
thread_access = factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=enums.ThreadAccessRoleChoices.EDITOR,
|
||||
)
|
||||
return user, mailbox, thread, thread_access
|
||||
|
||||
|
||||
class TestThreadAccessList:
|
||||
"""Test the GET /threads/{thread_id}/accesses/ endpoint."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR),
|
||||
],
|
||||
)
|
||||
def test_list_thread_access_success(
|
||||
self,
|
||||
api_client,
|
||||
thread_access_role,
|
||||
mailbox_access_role,
|
||||
django_assert_num_queries,
|
||||
):
|
||||
"""Test listing thread accesses of a thread."""
|
||||
user = factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
|
||||
thread = factories.ThreadFactory()
|
||||
factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
api_client.force_authenticate(user=user)
|
||||
# Create other accesses for thread
|
||||
factories.ThreadAccessFactory.create_batch(10, thread=thread)
|
||||
# Create others thread accesses for different threads
|
||||
other_thread = factories.ThreadFactory()
|
||||
factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=other_thread,
|
||||
)
|
||||
factories.ThreadAccessFactory.create_batch(5, thread=other_thread)
|
||||
|
||||
with django_assert_num_queries(3):
|
||||
response = api_client.get(get_thread_access_url(thread.id))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 11
|
||||
assert response.data["results"][0]["thread"] == thread.id
|
||||
|
||||
def test_list_thread_access_filter_by_mailbox(
|
||||
self, api_client, thread_with_editor_access, django_assert_num_queries
|
||||
):
|
||||
"""Test listing thread accesses filtered by mailbox."""
|
||||
user, mailbox, thread, thread_access = thread_with_editor_access
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
# Create another thread access for a different mailbox
|
||||
other_mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=other_mailbox,
|
||||
user=user,
|
||||
role=enums.MailboxRoleChoices.ADMIN,
|
||||
)
|
||||
factories.ThreadAccessFactory(
|
||||
mailbox=other_mailbox,
|
||||
thread=thread,
|
||||
role=enums.ThreadAccessRoleChoices.EDITOR,
|
||||
)
|
||||
with django_assert_num_queries(3):
|
||||
response = api_client.get(
|
||||
f"{get_thread_access_url(thread.id)}?mailbox_id={mailbox.id}"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["count"] == 1
|
||||
assert response.data["results"][0]["mailbox"] == mailbox.id
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER),
|
||||
],
|
||||
)
|
||||
def test_list_thread_access_forbidden(
|
||||
self, api_client, thread_access_role, mailbox_access_role
|
||||
):
|
||||
"""Test listing thread accesses without permission."""
|
||||
user = factories.UserFactory()
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
# Create a mailbox and thread access that the user doesn't have access to manage
|
||||
mailbox = factories.MailboxFactory()
|
||||
thread = factories.ThreadFactory()
|
||||
factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
|
||||
# Test that user cannot access thread accesses for a thread they don't have proper access to
|
||||
response = api_client.get(get_thread_access_url(thread.id))
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
# Test that user cannot access thread accesses for a non-existent thread
|
||||
response = api_client.get(get_thread_access_url(uuid.uuid4()))
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_list_thread_access_unauthorized(self, api_client):
|
||||
"""Test listing thread accesses without authentication."""
|
||||
thread = factories.ThreadFactory()
|
||||
response = api_client.get(get_thread_access_url(thread.id))
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestThreadAccessCreate:
|
||||
"""Test the POST /threads/{thread_id}/accesses/ endpoint."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR),
|
||||
],
|
||||
)
|
||||
def test_create_thread_access_success(
|
||||
self, api_client, thread_access_role, mailbox_access_role
|
||||
):
|
||||
"""Test creating a thread access successfully."""
|
||||
user = factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
|
||||
thread = factories.ThreadFactory()
|
||||
factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
delegated_mailbox = factories.MailboxFactory()
|
||||
data = {
|
||||
"mailbox": str(delegated_mailbox.id),
|
||||
"role": enums.ThreadAccessRoleChoices.VIEWER,
|
||||
}
|
||||
|
||||
response = api_client.post(get_thread_access_url(thread.id), data)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data["thread"] == thread.id
|
||||
assert response.data["mailbox"] == delegated_mailbox.id
|
||||
assert response.data["role"] == enums.ThreadAccessRoleChoices.VIEWER
|
||||
|
||||
def test_create_thread_access_duplicate(
|
||||
self, api_client, thread_with_editor_access
|
||||
):
|
||||
"""Test creating a duplicate thread access."""
|
||||
user, mailbox, thread, _ = thread_with_editor_access
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
data = {
|
||||
"mailbox": str(mailbox.id),
|
||||
"role": enums.ThreadAccessRoleChoices.EDITOR,
|
||||
}
|
||||
|
||||
response = api_client.post(get_thread_access_url(thread.id), data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER),
|
||||
],
|
||||
)
|
||||
def test_create_thread_access_forbidden(
|
||||
self, api_client, thread_access_role, mailbox_access_role
|
||||
):
|
||||
"""Test creating a thread access without permission."""
|
||||
user = factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
|
||||
thread = factories.ThreadFactory()
|
||||
factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
delegated_mailbox = factories.MailboxFactory()
|
||||
data = {
|
||||
"mailbox": str(delegated_mailbox.id),
|
||||
"role": enums.ThreadAccessRoleChoices.VIEWER,
|
||||
}
|
||||
|
||||
response = api_client.post(get_thread_access_url(thread.id), data)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_create_thread_access_invalid_data(
|
||||
self, api_client, thread_with_editor_access
|
||||
):
|
||||
"""Test creating a thread access with invalid data."""
|
||||
user, mailbox, thread, _ = thread_with_editor_access
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
data = {
|
||||
"mailbox": str(mailbox.id),
|
||||
"role": "invalid_role", # Invalid role
|
||||
}
|
||||
|
||||
response = api_client.post(get_thread_access_url(thread.id), data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_create_thread_access_unauthorized(self, api_client):
|
||||
"""Test creating a thread access without authentication."""
|
||||
thread = factories.ThreadFactory()
|
||||
response = api_client.post(get_thread_access_url(thread.id), {})
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestThreadAccessUpdate:
|
||||
"""Test the PUT/PATCH /threads/{thread_id}/accesses/{id}/ endpoint."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR),
|
||||
],
|
||||
)
|
||||
def test_update_thread_access_success(
|
||||
self, api_client, thread_access_role, mailbox_access_role
|
||||
):
|
||||
"""Test updating a thread access successfully."""
|
||||
user = factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
|
||||
thread = factories.ThreadFactory()
|
||||
factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
thread_access = factories.ThreadAccessFactory(
|
||||
thread=thread, role=enums.ThreadAccessRoleChoices.VIEWER
|
||||
)
|
||||
|
||||
url = get_thread_access_url(thread.id, thread_access.id)
|
||||
data = {"role": enums.ThreadAccessRoleChoices.EDITOR}
|
||||
|
||||
response = api_client.patch(url, data)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data["role"] == enums.ThreadAccessRoleChoices.EDITOR
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER),
|
||||
],
|
||||
)
|
||||
def test_update_thread_access_forbidden(
|
||||
self, api_client, thread_access_role, mailbox_access_role
|
||||
):
|
||||
"""Test updating a thread access without permission."""
|
||||
user = factories.UserFactory()
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
# Create a thread access that the user doesn't have right role to modify
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
thread = factories.ThreadFactory()
|
||||
thread_access = factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
url = get_thread_access_url(thread.id, thread_access.id)
|
||||
data = {"role": enums.ThreadAccessRoleChoices.EDITOR}
|
||||
|
||||
response = api_client.patch(url, data)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
# Create a thread access that the user doesn't have any role to modify
|
||||
thread = factories.ThreadFactory()
|
||||
thread_access = factories.ThreadAccessFactory()
|
||||
|
||||
url = get_thread_access_url(thread.id, thread_access.id)
|
||||
data = {"role": enums.ThreadAccessRoleChoices.EDITOR}
|
||||
|
||||
response = api_client.patch(url, data)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_update_thread_access_not_found(self, api_client, mailbox_with_access):
|
||||
"""Test updating a non-existent thread access."""
|
||||
user, _ = mailbox_with_access
|
||||
api_client.force_authenticate(user=user)
|
||||
thread = factories.ThreadFactory()
|
||||
|
||||
url = get_thread_access_url(thread.id, uuid.uuid4())
|
||||
data = {"role": enums.ThreadAccessRoleChoices.EDITOR}
|
||||
|
||||
response = api_client.patch(url, data)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_update_thread_access_unauthorized(self, api_client):
|
||||
"""Test updating a thread access without authentication."""
|
||||
thread = factories.ThreadFactory()
|
||||
url = get_thread_access_url(thread.id, uuid.uuid4())
|
||||
response = api_client.patch(url, {})
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestThreadAccessDelete:
|
||||
"""Test the DELETE /threads/{thread_id}/accesses/{id}/ endpoint."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.EDITOR),
|
||||
],
|
||||
)
|
||||
def test_delete_thread_access_success(
|
||||
self, api_client, thread_access_role, mailbox_access_role
|
||||
):
|
||||
"""Test deleting a thread access successfully."""
|
||||
user = factories.UserFactory()
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
thread = factories.ThreadFactory()
|
||||
thread_access = factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
url = get_thread_access_url(thread.id, thread_access.id)
|
||||
response = api_client.delete(url)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
# Verify the thread access was deleted
|
||||
assert not models.ThreadAccess.objects.filter(id=thread_access.id).exists()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thread_access_role, mailbox_access_role",
|
||||
[
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.ADMIN),
|
||||
(enums.ThreadAccessRoleChoices.VIEWER, enums.MailboxRoleChoices.EDITOR),
|
||||
(enums.ThreadAccessRoleChoices.EDITOR, enums.MailboxRoleChoices.VIEWER),
|
||||
],
|
||||
)
|
||||
def test_delete_thread_access_forbidden(
|
||||
self, api_client, thread_access_role, mailbox_access_role
|
||||
):
|
||||
"""Test deleting a thread access without permission."""
|
||||
user = factories.UserFactory()
|
||||
api_client.force_authenticate(user=user)
|
||||
|
||||
# Create a thread access that the user doesn't have any role to delete
|
||||
mailbox = factories.MailboxFactory()
|
||||
factories.MailboxAccessFactory(
|
||||
mailbox=mailbox,
|
||||
user=user,
|
||||
role=mailbox_access_role,
|
||||
)
|
||||
thread = factories.ThreadFactory()
|
||||
thread_access = factories.ThreadAccessFactory(
|
||||
mailbox=mailbox,
|
||||
thread=thread,
|
||||
role=thread_access_role,
|
||||
)
|
||||
|
||||
url = get_thread_access_url(thread.id, thread_access.id)
|
||||
response = api_client.delete(url)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
# Create a thread access that the user doesn't have any role to delete
|
||||
thread_access = factories.ThreadAccessFactory()
|
||||
url = get_thread_access_url(thread.id, thread_access.id)
|
||||
response = api_client.delete(url)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_delete_thread_access_not_found(self, api_client, mailbox_with_access):
|
||||
"""Test deleting a non-existent thread access."""
|
||||
user, _ = mailbox_with_access
|
||||
api_client.force_authenticate(user=user)
|
||||
thread = factories.ThreadFactory()
|
||||
|
||||
url = get_thread_access_url(thread.id, uuid.uuid4())
|
||||
response = api_client.delete(url)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_delete_thread_access_unauthorized(self, api_client):
|
||||
"""Test deleting a thread access without authentication."""
|
||||
thread = factories.ThreadFactory()
|
||||
url = get_thread_access_url(thread.id, uuid.uuid4())
|
||||
response = api_client.delete(url)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
@@ -1,7 +1,7 @@
|
||||
"""URL configuration for the core app."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import include, path
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
@@ -15,6 +15,7 @@ from core.api.viewsets.mta import MTAViewSet
|
||||
from core.api.viewsets.send import SendMessageView
|
||||
from core.api.viewsets.task import TaskDetailView
|
||||
from core.api.viewsets.thread import ThreadViewSet
|
||||
from core.api.viewsets.thread_access import ThreadAccessViewSet
|
||||
from core.api.viewsets.user import UserViewSet
|
||||
from core.authentication.urls import urlpatterns as oidc_urls
|
||||
|
||||
@@ -23,14 +24,33 @@ router = DefaultRouter()
|
||||
router.register("mta", MTAViewSet, basename="mta")
|
||||
router.register("users", UserViewSet, basename="users")
|
||||
router.register("mailboxes", MailboxViewSet, basename="mailboxes")
|
||||
router.register("threads", ThreadViewSet, basename="threads")
|
||||
router.register("messages", MessageViewSet, basename="messages")
|
||||
router.register("blob", BlobViewSet, basename="blob")
|
||||
router.register("thread-access", ThreadAccessViewSet, basename="thread-access")
|
||||
|
||||
# Add nested router for thread accesses
|
||||
thread_router = DefaultRouter()
|
||||
thread_router.register("threads", ThreadViewSet, basename="threads")
|
||||
|
||||
thread_related_router = DefaultRouter()
|
||||
thread_related_router.register(
|
||||
"accesses", ThreadAccessViewSet, basename="thread-access"
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
f"api/{settings.API_VERSION}/",
|
||||
include([*router.urls, *oidc_urls]),
|
||||
include(
|
||||
[
|
||||
*router.urls,
|
||||
*thread_router.urls,
|
||||
re_path(
|
||||
r"^threads/(?P<thread_id>[\w-]+)/",
|
||||
include(thread_related_router.urls),
|
||||
),
|
||||
*oidc_urls,
|
||||
]
|
||||
),
|
||||
),
|
||||
path(f"api/{settings.API_VERSION}/config/", ConfigView.as_view()),
|
||||
path(
|
||||
|
||||
Reference in New Issue
Block a user