Skip to content

Commit 0929c13

Browse files
committed
[test] Added reusable tests for multi-tenant API
1 parent fb68e26 commit 0929c13

File tree

4 files changed

+193
-112
lines changed

4 files changed

+193
-112
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

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

tests/testapp/tests/test_permission_classes.py

Lines changed: 33 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
from django.contrib.auth import get_user_model
22
from django.contrib.auth.models import Permission
3-
from django.test import TestCase
43
from django.urls import reverse
54
from swapper import load_model
65

76
from openwisp_users.api.throttling import AuthRateThrottle
7+
from openwisp_users.tests.test_api import APITestCase
88

9+
from .. import CreateMixin
910
from ..models import Template
10-
from .mixins import TestMultitenancyMixin
1111

1212
User = get_user_model()
1313
Group = load_model('openwisp_users', 'Group')
1414
OrganizationUser = load_model('openwisp_users', 'OrganizationUser')
1515

1616

17-
class TestPermissionClasses(TestMultitenancyMixin, TestCase):
17+
class TestPermissionClasses(CreateMixin, APITestCase):
1818
def setUp(self):
1919
AuthRateThrottle.rate = 0
2020
self.template_model = Template
@@ -231,114 +231,39 @@ def test_view_django_model_permission_with_change_perm(self):
231231
)
232232
self.assertEqual(response.status_code, 200)
233233

234-
def _test_access_shared_object(
235-
self, token, expected_templates_count=1, expected_status_codes={}
236-
):
237-
auth = dict(HTTP_AUTHORIZATION=f'Bearer {token}')
238-
template = self._create_template(organization=None)
239-
240-
with self.subTest('Test listing templates'):
241-
response = self.client.get(reverse('test_template_list'), **auth)
242-
data = response.data.copy()
243-
# Only check "templates" in response.
244-
if isinstance(data, dict):
245-
data.pop('detail', None)
246-
self.assertEqual(response.status_code, expected_status_codes['list'])
247-
self.assertEqual(len(data), expected_templates_count)
248-
249-
with self.subTest('Test creating template'):
250-
response = self.client.post(
251-
reverse('test_template_list'),
252-
data={'name': 'Test Template', 'organization': None},
253-
content_type='application/json',
254-
**auth,
255-
)
256-
self.assertEqual(response.status_code, expected_status_codes['create'])
257-
if expected_status_codes['create'] == 400:
258-
self.assertEqual(
259-
str(response.data['organization'][0]), 'This field may not be null.'
260-
)
261-
262-
with self.subTest('Test retreiving template'):
263-
response = self.client.get(
264-
reverse('test_template_detail', args=[template.id]), **auth
265-
)
266-
self.assertEqual(response.status_code, expected_status_codes['retrieve'])
267-
268-
with self.subTest('Test updating template'):
269-
response = self.client.put(
270-
reverse('test_template_detail', args=[template.id]),
271-
data={'name': 'Name changed'},
272-
content_type='application/json',
273-
**auth,
274-
)
275-
self.assertEqual(response.status_code, expected_status_codes['update'])
276-
277-
with self.subTest('Test deleting template'):
278-
response = self.client.delete(
279-
reverse('test_template_detail', args=[template.id]), **auth
280-
)
281-
self.assertEqual(response.status_code, expected_status_codes['delete'])
282-
283-
with self.subTest('Test HEAD and OPTION methods'):
284-
response = self.client.head(reverse('test_template_list'), **auth)
285-
self.assertEqual(response.status_code, expected_status_codes['head'])
286-
287-
response = self.client.options(reverse('test_template_list'), **auth)
288-
self.assertEqual(response.status_code, expected_status_codes['option'])
289-
290234
def test_superuser_access_shared_object(self):
291-
superuser = self._get_admin()
292-
token = self._obtain_auth_token(username=superuser)
293-
self._test_access_shared_object(
294-
token,
295-
expected_status_codes={
296-
'create': 201,
297-
'list': 200,
298-
'retrieve': 200,
299-
'update': 200,
300-
'delete': 204,
301-
'head': 200,
302-
'option': 200,
303-
},
235+
self._test_superuser_access_shared_object(
236+
token=None,
237+
listview_name='test_template_list',
238+
detailview_name='test_template_detail',
239+
create_payload={'name': 'test', 'organization': ''},
240+
update_payload={'name': 'updated-test'},
241+
expected_count=1,
304242
)
305243

306244
def test_org_manager_access_shared_object(self):
307-
org_manager = self._create_administrator()
308-
token = self._obtain_auth_token(username=org_manager)
309-
# First user is automatically owner, so created dummy
310-
# user to keep operator as manager only.
311-
self._create_org_user(user=self._get_user(), is_admin=True)
312-
self._create_org_user(user=org_manager, is_admin=True)
313-
self._test_access_shared_object(
314-
token,
315-
expected_status_codes={
316-
'create': 400,
317-
'list': 200,
318-
'retrieve': 200,
319-
'update': 403,
320-
'delete': 403,
321-
'head': 200,
322-
'option': 200,
323-
},
245+
template = self._create_template(organization=None)
246+
self._test_org_user_access_shared_object(
247+
listview_path=reverse('test_template_list'),
248+
detailview_path=reverse('test_template_detail', args=[template.pk]),
249+
create_payload={'name': 'test', 'organization': ''},
250+
update_payload={'name': 'updated-test'},
251+
expected_count=1,
324252
)
325253

326254
def test_org_owner_access_shared_object(self):
327255
# The first admin of an organization automatically
328256
# becomes organization owner.
329257
org_owner = self._create_administrator(organizations=[self._get_org()])
330258
token = self._obtain_auth_token(username=org_owner)
331-
self._test_access_shared_object(
332-
token,
333-
expected_status_codes={
334-
'create': 400,
335-
'list': 200,
336-
'retrieve': 200,
337-
'update': 403,
338-
'delete': 403,
339-
'head': 200,
340-
'option': 200,
341-
},
259+
template = self._create_template(organization=None)
260+
self._test_org_user_access_shared_object(
261+
listview_path=reverse('test_template_list'),
262+
detailview_path=reverse('test_template_detail', args=[template.pk]),
263+
create_payload={'name': 'test', 'organization': ''},
264+
update_payload={'name': 'updated-test'},
265+
expected_count=1,
266+
token=token,
342267
)
343268

344269
def test_org_user_access_shared_object(self):
@@ -348,9 +273,14 @@ def test_org_user_access_shared_object(self):
348273
user = self._create_administrator()
349274
token = self._obtain_auth_token(username=user)
350275
self._create_org_user(user=user, is_admin=False)
351-
self._test_access_shared_object(
352-
token,
353-
expected_templates_count=0,
276+
template = self._create_template(organization=None)
277+
self._test_org_user_access_shared_object(
278+
listview_path=reverse('test_template_list'),
279+
detailview_path=reverse('test_template_detail', args=[template.pk]),
280+
create_payload={'name': 'test', 'organization': ''},
281+
update_payload={'name': 'updated-test'},
282+
expected_count=0,
283+
token=token,
354284
expected_status_codes={
355285
'create': 400,
356286
'list': 200,

0 commit comments

Comments
 (0)