Skip to content

Commit 856832e

Browse files
Reuse permissions in tests
1 parent b169462 commit 856832e

5 files changed

Lines changed: 322 additions & 25 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Apart from superusers/admins, the application has three user roles with differen
3232

3333
Users can become Annotators or Master Annotators by adding them to the respective user groups in the Django admin interface.
3434

35-
The overview below shows the permissions for each role. Not all permissions are currently implemented.
35+
The matrix below shows the permissions for each role. Not all permissions are currently implemented.
3636

3737
(Last updated: November 14th, 2025)
3838

backend/problem/views/problem.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,21 @@
1414
from problem.serializers import ProblemInputSerializer, ProblemSerializer
1515
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
1616

17+
1718
class CreateProblemPermission(IsAuthenticated):
1819
def has_permission(self, request, view):
19-
return request.user.can_create_problem
20+
return super().has_permission(request, view) and request.user.can_create_problem
21+
2022

2123
class EditProblemPermission(IsAuthenticated):
2224
def has_permission(self, request, view):
23-
return request.user.can_edit_problem
25+
return super().has_permission(request, view) and request.user.can_edit_problem
2426

2527

2628
class ProblemView(ModelViewSet):
2729
queryset = Problem.objects.all()
2830
serializer_class = ProblemSerializer
2931

30-
3132
def get_permissions(self):
3233
if self.action == "create":
3334
return [CreateProblemPermission()]
@@ -128,7 +129,7 @@ def _handle_update_create_problem(
128129
problem_serializer = ProblemSerializer()
129130

130131
if problem_id is None:
131-
problem = problem_serializer.create(validated_input) # type: ignore
132+
problem = problem_serializer.create(validated_input) # type: ignore
132133
status = HTTP_201_CREATED
133134
else:
134135
problem_instance = get_object_or_404(
@@ -140,4 +141,3 @@ def _handle_update_create_problem(
140141
status = HTTP_200_OK
141142

142143
return Response({"id": problem.pk}, status=status)
143-
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import pytest
2+
from django.contrib.auth.models import Group, Permission
3+
from rest_framework.test import APIClient
4+
from rest_framework import status
5+
6+
from problem.models import Problem, Sentence
7+
from user.models import User, GroupName
8+
from user.permissions import annotator_permissions, master_annotator_permissions
9+
10+
11+
@pytest.fixture
12+
def api_client():
13+
"""Returns a DRF APIClient instance."""
14+
return APIClient()
15+
16+
17+
@pytest.fixture
18+
def visitor(db):
19+
"""Creates a visitor user (no special permissions)."""
20+
return User.objects.create_user(
21+
username="visitor",
22+
email="visitor@test.com",
23+
password="testpassword",
24+
)
25+
26+
27+
@pytest.fixture
28+
def annotator(db):
29+
"""Creates an annotator user with annotator group permissions."""
30+
user = User.objects.create_user(
31+
username="annotator",
32+
email="annotator@test.com",
33+
password="testpassword",
34+
)
35+
group, _ = Group.objects.get_or_create(name=GroupName.ANNOTATORS)
36+
37+
# Add annotator permissions
38+
for app_label, codename in annotator_permissions:
39+
try:
40+
perm = Permission.objects.get(
41+
content_type__app_label=app_label,
42+
codename=codename,
43+
)
44+
group.permissions.add(perm)
45+
except Permission.DoesNotExist:
46+
pass
47+
user.groups.add(group)
48+
return user
49+
50+
51+
@pytest.fixture
52+
def master_annotator(db):
53+
"""Creates a master annotator user with full permissions."""
54+
user = User.objects.create_user(
55+
username="master_annotator",
56+
email="master@test.com",
57+
password="testpassword",
58+
)
59+
group, _ = Group.objects.get_or_create(name=GroupName.MASTER_ANNOTATORS)
60+
61+
# Add master annotator permissions
62+
for app_label, codename in master_annotator_permissions:
63+
try:
64+
perm = Permission.objects.get(
65+
content_type__app_label=app_label,
66+
codename=codename,
67+
)
68+
group.permissions.add(perm)
69+
except Permission.DoesNotExist:
70+
pass
71+
user.groups.add(group)
72+
return user
73+
74+
75+
@pytest.fixture
76+
def sample_problem(db):
77+
"""Creates a sample problem for testing."""
78+
hypothesis = Sentence.objects.create(text="This is a hypothesis.")
79+
premise = Sentence.objects.create(text="This is a premise.")
80+
problem = Problem.objects.create(
81+
dataset=Problem.Dataset.USER,
82+
hypothesis=hypothesis,
83+
entailment_label=Problem.EntailmentLabel.NEUTRAL,
84+
extra_data={},
85+
)
86+
problem.premises.add(premise)
87+
return problem
88+
89+
90+
@pytest.fixture
91+
def problem_input_data():
92+
"""Returns valid input data for creating/updating a problem."""
93+
return {
94+
"premises": ["Test premise 1", "Test premise 2"],
95+
"hypothesis": "Test hypothesis",
96+
"entailmentLabel": "neutral",
97+
"kbItems": [],
98+
}
99+
100+
101+
class TestProblemViewPermissions:
102+
"""
103+
Tests for problem view permissions as documented in the README.
104+
"""
105+
106+
# ==================== LIST (Browse) Tests ====================
107+
108+
def test_unauthenticated_user_can_list_problems(self, api_client, sample_problem):
109+
"""Unauthenticated users should be able to browse problems (read-only)."""
110+
response = api_client.get("/api/problem/")
111+
assert response.status_code == status.HTTP_200_OK
112+
113+
def test_visitor_can_list_problems(self, api_client, visitor, sample_problem):
114+
"""Visitors should be able to browse problems."""
115+
api_client.force_authenticate(user=visitor)
116+
response = api_client.get("/api/problem/")
117+
assert response.status_code == status.HTTP_200_OK
118+
119+
def test_annotator_can_list_problems(self, api_client, annotator, sample_problem):
120+
"""Annotators should be able to browse problems."""
121+
api_client.force_authenticate(user=annotator)
122+
response = api_client.get("/api/problem/")
123+
assert response.status_code == status.HTTP_200_OK
124+
125+
def test_master_annotator_can_list_problems(
126+
self, api_client, master_annotator, sample_problem
127+
):
128+
"""Master annotators should be able to browse problems."""
129+
api_client.force_authenticate(user=master_annotator)
130+
response = api_client.get("/api/problem/")
131+
assert response.status_code == status.HTTP_200_OK
132+
133+
# ==================== RETRIEVE (Browse Single) Tests ====================
134+
135+
def test_unauthenticated_user_can_retrieve_problem(
136+
self, api_client, sample_problem
137+
):
138+
"""Unauthenticated users should be able to view a single problem."""
139+
response = api_client.get(f"/api/problem/{sample_problem.id}/")
140+
assert response.status_code == status.HTTP_200_OK
141+
142+
def test_visitor_can_retrieve_problem(self, api_client, visitor, sample_problem):
143+
"""Visitors should be able to view a single problem."""
144+
api_client.force_authenticate(user=visitor)
145+
response = api_client.get(f"/api/problem/{sample_problem.id}/")
146+
assert response.status_code == status.HTTP_200_OK
147+
148+
def test_annotator_can_retrieve_problem(
149+
self, api_client, annotator, sample_problem
150+
):
151+
"""Annotators should be able to view a single problem."""
152+
api_client.force_authenticate(user=annotator)
153+
response = api_client.get(f"/api/problem/{sample_problem.id}/")
154+
assert response.status_code == status.HTTP_200_OK
155+
156+
def test_master_annotator_can_retrieve_problem(
157+
self, api_client, master_annotator, sample_problem
158+
):
159+
"""Master annotators should be able to view a single problem."""
160+
api_client.force_authenticate(user=master_annotator)
161+
response = api_client.get(f"/api/problem/{sample_problem.id}/")
162+
assert response.status_code == status.HTTP_200_OK
163+
164+
# ==================== CREATE (Add Problems) Tests ====================
165+
166+
def test_unauthenticated_user_cannot_create_problem(
167+
self, api_client, problem_input_data
168+
):
169+
"""Unauthenticated users should not be able to create problems."""
170+
response = api_client.post("/api/problem/", problem_input_data, format="json")
171+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
172+
173+
def test_visitor_cannot_create_problem(
174+
self, api_client, visitor, problem_input_data
175+
):
176+
"""Visitors should not be able to create problems."""
177+
api_client.force_authenticate(user=visitor)
178+
response = api_client.post("/api/problem/", problem_input_data, format="json")
179+
assert response.status_code == status.HTTP_403_FORBIDDEN
180+
181+
def test_annotator_cannot_create_problem(
182+
self, api_client, annotator, problem_input_data
183+
):
184+
"""Annotators should not be able to create problems."""
185+
api_client.force_authenticate(user=annotator)
186+
response = api_client.post("/api/problem/", problem_input_data, format="json")
187+
assert response.status_code == status.HTTP_403_FORBIDDEN
188+
189+
def test_master_annotator_can_create_problem(
190+
self, api_client, master_annotator, problem_input_data
191+
):
192+
"""Master annotators should be able to create problems."""
193+
api_client.force_authenticate(user=master_annotator)
194+
response = api_client.post("/api/problem/", problem_input_data, format="json")
195+
print(response.data)
196+
assert response.status_code == status.HTTP_201_CREATED
197+
assert "id" in response.data
198+
199+
# ==================== UPDATE (Edit Problems) Tests ====================
200+
201+
def test_unauthenticated_user_cannot_update_problem(
202+
self, api_client, sample_problem, problem_input_data
203+
):
204+
"""Unauthenticated users should not be able to update problems."""
205+
response = api_client.patch(
206+
f"/api/problem/{sample_problem.id}/", problem_input_data, format="json"
207+
)
208+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
209+
210+
def test_visitor_cannot_update_problem(
211+
self, api_client, visitor, sample_problem, problem_input_data
212+
):
213+
"""Visitors should not be able to update problems."""
214+
api_client.force_authenticate(user=visitor)
215+
response = api_client.patch(
216+
f"/api/problem/{sample_problem.id}/", problem_input_data, format="json"
217+
)
218+
assert response.status_code == status.HTTP_403_FORBIDDEN
219+
220+
def test_annotator_cannot_update_problem(
221+
self, api_client, annotator, sample_problem, problem_input_data
222+
):
223+
"""Annotators should not be able to update problems."""
224+
api_client.force_authenticate(user=annotator)
225+
response = api_client.patch(
226+
f"/api/problem/{sample_problem.id}/", problem_input_data, format="json"
227+
)
228+
assert response.status_code == status.HTTP_403_FORBIDDEN
229+
230+
def test_master_annotator_can_update_problem(
231+
self, api_client, master_annotator, sample_problem, problem_input_data
232+
):
233+
"""Master annotators should be able to update user-created problems."""
234+
api_client.force_authenticate(user=master_annotator)
235+
response = api_client.patch(
236+
f"/api/problem/{sample_problem.id}/", problem_input_data, format="json"
237+
)
238+
assert response.status_code == status.HTTP_200_OK
239+
240+
241+
class TestUserRoleProperties:
242+
"""Tests for user role property methods used by permissions."""
243+
244+
def test_visitor_cannot_create_problem(self, visitor):
245+
"""Visitor's can_create_problem should return False."""
246+
assert visitor.can_create_problem is False
247+
248+
def test_visitor_cannot_edit_problem(self, visitor):
249+
"""Visitor's can_edit_problem should return False."""
250+
assert visitor.can_edit_problem is False
251+
252+
def test_annotator_cannot_create_problem(self, annotator):
253+
"""Annotator's can_create_problem should return False."""
254+
assert annotator.can_create_problem is False
255+
256+
def test_annotator_cannot_edit_problem(self, annotator):
257+
"""Annotator's can_edit_problem should return False."""
258+
assert annotator.can_edit_problem is False
259+
260+
def test_master_annotator_can_create_problem(self, master_annotator):
261+
"""Master annotator's can_create_problem should return True."""
262+
assert master_annotator.can_create_problem is True
263+
264+
def test_master_annotator_can_edit_problem(self, master_annotator):
265+
"""Master annotator's can_edit_problem should return True."""
266+
assert master_annotator.can_edit_problem is True
267+
268+
269+
class TestFirstEndpointPermissions:
270+
"""Tests for the /first endpoint permissions."""
271+
272+
def test_unauthenticated_user_can_access_first(self, api_client, sample_problem):
273+
"""Unauthenticated users should be able to access /first endpoint."""
274+
response = api_client.get("/api/problem/first/")
275+
assert response.status_code == status.HTTP_200_OK
276+
277+
def test_visitor_can_access_first(self, api_client, visitor, sample_problem):
278+
"""Visitors should be able to access /first endpoint."""
279+
api_client.force_authenticate(user=visitor)
280+
response = api_client.get("/api/problem/first/")
281+
assert response.status_code == status.HTTP_200_OK
282+
283+
def test_annotator_can_access_first(self, api_client, annotator, sample_problem):
284+
"""Annotators should be able to access /first endpoint."""
285+
api_client.force_authenticate(user=annotator)
286+
response = api_client.get("/api/problem/first/")
287+
assert response.status_code == status.HTTP_200_OK
288+
289+
def test_master_annotator_can_access_first(
290+
self, api_client, master_annotator, sample_problem
291+
):
292+
"""Master annotators should be able to access /first endpoint."""
293+
api_client.force_authenticate(user=master_annotator)
294+
response = api_client.get("/api/problem/first/")
295+
assert response.status_code == status.HTTP_200_OK

backend/user/migrations/0004_create_user_groups.py

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,8 @@
33
from django.db import migrations
44

55
from user.models import GroupName
6+
from user.permissions import annotator_permissions, master_annotator_permissions
67

7-
annotator_permissions = [
8-
"problem.view_silver_problems",
9-
"problem.add_knowledgebase",
10-
"problem.change_knowledgebase",
11-
"problem.delete_knowledgebase",
12-
"problem.view_knowledgebase",
13-
]
14-
15-
master_annotator_permissions = annotator_permissions + [
16-
"problem.copy_problems",
17-
"problem.view_hidden_problems",
18-
"problem.change_problem_status",
19-
"problem.change_problem_visibility",
20-
"problem.add_problem",
21-
"problem.change_problem",
22-
"problem.delete_problem",
23-
"problem.view_problem",
24-
]
258

269
permission_map = {
2710
GroupName.MASTER_ANNOTATORS: master_annotator_permissions,
@@ -40,7 +23,7 @@ def create_groups(apps, schema_editor):
4023
group, created = Group.objects.get_or_create(name=group_name.value)
4124
for perm_codename in perms:
4225
try:
43-
app_label, codename = perm_codename.split(".")
26+
app_label, codename = perm_codename
4427
permission = Permission.objects.get(
4528
content_type__app_label=app_label,
4629
codename=codename,

backend/user/permissions.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Django permissions are uniquely identified by their combination of a `content_type__app_label` and a `codename`.
2+
annotator_permissions = [
3+
("problem", "view_silver_problems"),
4+
("problem", "add_knowledgebase"),
5+
("problem", "change_knowledgebase"),
6+
("problem", "delete_knowledgebase"),
7+
("problem", "view_knowledgebase"),
8+
]
9+
10+
master_annotator_permissions = annotator_permissions + [
11+
("problem", "copy_problems"),
12+
("problem", "view_hidden_problems"),
13+
("problem", "change_problem_status"),
14+
("problem", "change_problem_visibility"),
15+
("problem", "add_problem"),
16+
("problem", "change_problem"),
17+
("problem", "delete_problem"),
18+
("problem", "view_problem"),
19+
]

0 commit comments

Comments
 (0)