Skip to content

Commit 80e742f

Browse files
committed
[test] Added reusable tests for multi-tenant API
1 parent 89618c0 commit 80e742f

File tree

5 files changed

+208
-119
lines changed

5 files changed

+208
-119
lines changed

openwisp_users/tests/test_api/__init__.py

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,159 @@ def _obtain_auth_token(self, username="operator", password="tester"):
1212
return response.data["token"]
1313

1414

15-
class APITestCase(TestMultitenantAdminMixin, AuthenticationMixin, TestCase):
15+
class TestMultitenantApiMixin(TestMultitenantAdminMixin):
16+
def _test_access_shared_object(
17+
self,
18+
token,
19+
listview_name=None,
20+
listview_path=None,
21+
detailview_name=None,
22+
detailview_path=None,
23+
create_payload=None,
24+
update_payload=None,
25+
expected_count=0,
26+
expected_status_codes=None,
27+
):
28+
auth = dict(HTTP_AUTHORIZATION=f'Bearer {token}')
29+
create_payload = create_payload or {}
30+
update_payload = update_payload or {}
31+
expected_status_codes = expected_status_codes or {}
32+
33+
if listview_name or listview_path:
34+
listview_path = listview_path or reverse(listview_name)
35+
with self.subTest('HEAD and OPTION methods'):
36+
response = self.client.head(listview_path, **auth)
37+
self.assertEqual(response.status_code, expected_status_codes['head'])
38+
39+
response = self.client.options(listview_path, **auth)
40+
self.assertEqual(response.status_code, expected_status_codes['option'])
41+
42+
with self.subTest('Create shared object'):
43+
response = self.client.post(
44+
listview_path,
45+
data=create_payload,
46+
content_type='application/json',
47+
**auth,
48+
)
49+
self.assertEqual(response.status_code, expected_status_codes['create'])
50+
if expected_status_codes['create'] == 400:
51+
self.assertEqual(
52+
str(response.data['organization'][0]),
53+
'This field may not be null.',
54+
)
55+
56+
with self.subTest('List all shared objects'):
57+
response = self.client.get(listview_path, **auth)
58+
self.assertEqual(response.status_code, expected_status_codes['list'])
59+
data = response.data
60+
if not isinstance(response.data, list):
61+
data = data.get('results', data)
62+
self.assertEqual(len(data), expected_count)
63+
64+
if detailview_name or detailview_path:
65+
if not detailview_path and listview_name and expected_count > 0:
66+
detailview_path = reverse(detailview_name, args=[data[0]['id']])
67+
68+
with self.subTest('Retrieve shared object'):
69+
response = self.client.get(detailview_path, **auth)
70+
self.assertEqual(
71+
response.status_code, expected_status_codes['retrieve']
72+
)
73+
74+
with self.subTest('Update shared object'):
75+
response = self.client.put(
76+
detailview_path,
77+
data=update_payload,
78+
content_type='application/json',
79+
**auth,
80+
)
81+
self.assertEqual(response.status_code, expected_status_codes['update'])
82+
83+
with self.subTest('Delete shared object'):
84+
response = self.client.delete(detailview_path, **auth)
85+
self.assertEqual(response.status_code, expected_status_codes['delete'])
86+
87+
def _test_org_user_access_shared_object(
88+
self,
89+
listview_name=None,
90+
listview_path=None,
91+
detailview_name=None,
92+
detailview_path=None,
93+
create_payload=None,
94+
update_payload=None,
95+
expected_count=0,
96+
expected_status_codes=None,
97+
token=None,
98+
):
99+
"""
100+
Non-superusers can only view shared objects.
101+
They cannot create, update, or delete them.
102+
"""
103+
if not token:
104+
user = self._create_administrator(organizations=[self._get_org()])
105+
token = self._obtain_auth_token(user.username, 'tester')
106+
if not expected_status_codes:
107+
expected_status_codes = {
108+
'create': 400,
109+
'list': 200,
110+
'retrieve': 200,
111+
'update': 403,
112+
'delete': 403,
113+
'head': 200,
114+
'option': 200,
115+
}
116+
self._test_access_shared_object(
117+
token=token,
118+
listview_name=listview_name,
119+
listview_path=listview_path,
120+
detailview_name=detailview_name,
121+
detailview_path=detailview_path,
122+
create_payload=create_payload,
123+
update_payload=update_payload,
124+
expected_count=expected_count,
125+
expected_status_codes=expected_status_codes,
126+
)
127+
128+
def _test_superuser_access_shared_object(
129+
self,
130+
token,
131+
listview_path=None,
132+
listview_name=None,
133+
detailview_path=None,
134+
detailview_name=None,
135+
create_payload=None,
136+
update_payload=None,
137+
expected_count=1,
138+
expected_status_codes=None,
139+
):
140+
"""
141+
Superusers can perform all operations on shared objects.
142+
"""
143+
if not token:
144+
user = self._create_admin()
145+
token = self._obtain_auth_token(user.username, 'tester')
146+
if not expected_status_codes:
147+
expected_status_codes = {
148+
'create': 201,
149+
'list': 200,
150+
'retrieve': 200,
151+
'update': 200,
152+
'delete': 204,
153+
'head': 200,
154+
'option': 200,
155+
}
156+
self._test_access_shared_object(
157+
token=token,
158+
listview_name=listview_name,
159+
listview_path=listview_path,
160+
detailview_name=detailview_name,
161+
detailview_path=detailview_path,
162+
create_payload=create_payload,
163+
update_payload=update_payload,
164+
expected_count=expected_count,
165+
expected_status_codes=expected_status_codes,
166+
)
167+
168+
169+
class APITestCase(TestMultitenantApiMixin, AuthenticationMixin, TestCase):
16170
pass

openwisp_users/tests/utils.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,14 @@ def _test_recoverlist_operator_403(self, app_label, model_label):
240240
self.assertEqual(response.status_code, 403)
241241

242242
def _test_org_admin_create_shareable_object(
243-
self, path, payload, model, expected_count=0, error_message=None, user=None
243+
self,
244+
path,
245+
payload,
246+
model,
247+
expected_count=0,
248+
user=None,
249+
error_message=None,
250+
raises_error=True,
244251
):
245252
"""
246253
Verifies a non-superuser cannot create a shareable object
@@ -253,12 +260,13 @@ def _test_org_admin_create_shareable_object(
253260
data=payload,
254261
follow=True,
255262
)
256-
error_message = error_message or (
257-
'<div class="form-row errors field-organization">\n'
258-
' <ul class="errorlist"{}>'
259-
'<li>This field is required.</li></ul>'
260-
).format(' id="id_organization_error"' if django.VERSION >= (5, 2) else '')
261-
self.assertContains(response, error_message)
263+
if raises_error:
264+
error_message = error_message or (
265+
'<div class="form-row errors field-organization">\n'
266+
' <ul class="errorlist"{}>'
267+
'<li>This field is required.</li></ul>'
268+
).format(' id="id_organization_error"' if django.VERSION >= (5, 2) else '')
269+
self.assertContains(response, error_message)
262270
self.assertEqual(model.objects.count(), expected_count)
263271

264272
def _test_org_admin_view_shareable_object(

tests/testapp/tests/mixins.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
from openwisp_users.tests.test_api import AuthenticationMixin
2-
from openwisp_users.tests.utils import TestMultitenantAdminMixin
1+
from openwisp_users.tests.test_api import AuthenticationMixin, TestMultitenantApiMixin
32

43
from .. import CreateMixin
54

65

7-
class TestMultitenancyMixin(
8-
CreateMixin, TestMultitenantAdminMixin, AuthenticationMixin
9-
):
6+
class TestMultitenancyMixin(CreateMixin, TestMultitenantApiMixin, AuthenticationMixin):
107
pass

tests/testapp/tests/test_filter_classes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
from django.contrib.auth import get_user_model
2-
from django.test import TestCase
32
from django.urls import reverse
43
from packaging.version import parse as version_parse
54
from rest_framework import VERSION as REST_FRAMEWORK_VERSION
65
from swapper import load_model
76

87
from openwisp_users.api.throttling import AuthRateThrottle
8+
from openwisp_users.tests.test_api import APITestCase
99
from openwisp_utils.tests import AssertNumQueriesSubTestMixin
1010

11+
from .. import CreateMixin
1112
from ..models import Book, Library, Shelf, Tag
12-
from .mixins import TestMultitenancyMixin
1313

1414
OrganizationUser = load_model("openwisp_users", "OrganizationUser")
1515
OrganizationOwner = load_model("openwisp_users", "OrganizationOwner")
1616
User = get_user_model()
1717

1818

19-
class TestFilterClasses(AssertNumQueriesSubTestMixin, TestMultitenancyMixin, TestCase):
19+
class TestFilterClasses(AssertNumQueriesSubTestMixin, CreateMixin, APITestCase):
2020
def setUp(self):
2121
AuthRateThrottle.rate = 0
2222
self.shelf_model = Shelf

0 commit comments

Comments
 (0)