Skip to content

Commit e6ff370

Browse files
committed
[tests] Added multi-tentant tests for shared objects
1 parent 957c4a3 commit e6ff370

File tree

5 files changed

+190
-49
lines changed

5 files changed

+190
-49
lines changed

openwisp_controller/config/tests/test_api.py

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from openwisp_controller.config.api.serializers import BaseConfigSerializer
99
from openwisp_controller.tests.utils import TestAdminMixin
10-
from openwisp_users.tests.test_api import AuthenticationMixin
10+
from openwisp_users.tests.test_api import AuthenticationMixin, TestMultitenantApiMixin
1111
from openwisp_utils.tests import capture_any_output, catch_signal
1212

1313
from .. import settings as app_settings
@@ -30,7 +30,7 @@
3030
OrganizationUser = load_model('openwisp_users', 'OrganizationUser')
3131

3232

33-
class ApiTestMixin:
33+
class ApiTestMixin(AuthenticationMixin, TestMultitenantApiMixin):
3434
@property
3535
def _template_data(self):
3636
return {
@@ -100,7 +100,6 @@ class TestConfigApi(
100100
CreateConfigTemplateMixin,
101101
TestVpnX509Mixin,
102102
CreateDeviceGroupMixin,
103-
AuthenticationMixin,
104103
TestCase,
105104
):
106105
def setUp(self):
@@ -648,29 +647,59 @@ def test_template_create_of_vpn_type(self):
648647
self.assertEqual(Template.objects.count(), 1)
649648
self.assertEqual(r.status_code, 201)
650649

651-
def test_template_create_with_shared_vpn(self):
652-
org1 = self._get_org()
653-
test_user = self._create_operator(organizations=[org1])
654-
self.client.force_login(test_user)
655-
vpn1 = self._create_vpn(name='vpn1', organization=None)
656-
path = reverse('config_api:template_list')
657-
data = self._template_data
658-
data['type'] = 'vpn'
659-
data['vpn'] = vpn1.id
660-
data['organization'] = org1.pk
661-
r = self.client.post(path, data, content_type='application/json')
662-
self.assertEqual(r.status_code, 201)
663-
self.assertEqual(Template.objects.count(), 1)
664-
self.assertEqual(r.data['vpn'], vpn1.id)
665-
666-
def test_template_creation_with_no_org_by_operator(self):
667-
path = reverse('config_api:template_list')
668-
data = self._template_data
650+
def test_operator_access_shared_template(self):
669651
test_user = self._create_operator(organizations=[self._get_org()])
670-
self.client.force_login(test_user)
671-
r = self.client.post(path, data, content_type='application/json')
672-
self.assertEqual(r.status_code, 400)
673-
self.assertIn('This field may not be null.', str(r.content))
652+
token = self._obtain_auth_token(test_user)
653+
self._create_template(organization=None)
654+
self._test_org_user_access_shared_object(
655+
listview_name='config_api:template_list',
656+
detailview_name='config_api:template_detail',
657+
create_payload={'name': 'test', 'organization': ''},
658+
update_payload={'name': 'updated-test'},
659+
expected_count=1,
660+
token=token,
661+
expected_status_codes={
662+
'create': 400,
663+
'list': 200,
664+
'retrieve': 403,
665+
'update': 403,
666+
'delete': 403,
667+
'head': 403,
668+
'option': 200,
669+
},
670+
)
671+
672+
def test_org_admin_create_template_with_shared_vpn(self):
673+
org = self._get_org()
674+
vpn = self._create_vpn(organization=None)
675+
create_payload = self._template_data
676+
create_payload.update(
677+
{
678+
'organization': org.pk,
679+
'type': 'vpn',
680+
'vpn': vpn.pk,
681+
}
682+
)
683+
update_payload = create_payload.copy()
684+
update_payload['name'] = 'updated-test'
685+
test_user = self._create_operator(organizations=[org])
686+
self._test_org_user_access_shared_object(
687+
listview_name='config_api:template_list',
688+
detailview_name='config_api:template_detail',
689+
create_payload=create_payload,
690+
update_payload=update_payload,
691+
expected_count=1,
692+
expected_status_codes={
693+
'create': 201,
694+
'list': 200,
695+
'retrieve': 200,
696+
'update': 200,
697+
'delete': 204,
698+
'head': 403,
699+
'option': 200,
700+
},
701+
token=self._obtain_auth_token(test_user),
702+
)
674703

675704
def test_template_create_with_empty_config(self):
676705
path = reverse('config_api:template_list')
@@ -832,19 +861,50 @@ def test_vpn_create_api(self):
832861
self.assertEqual(r.status_code, 201)
833862
self.assertEqual(Vpn.objects.count(), 1)
834863

835-
def test_vpn_create_with_shared_objects(self):
836-
org1 = self._get_org()
864+
def test_org_admin_access_vpn_with_shared_objects(self):
865+
org = self._get_org()
866+
shared_ca = self._create_ca(name='shared_ca', organization=None)
867+
create_payload = self._vpn_data
868+
create_payload.update(
869+
{
870+
'organization': org.pk,
871+
'ca': shared_ca.pk,
872+
}
873+
)
874+
update_payload = create_payload.copy()
875+
update_payload['name'] = 'updated-test-vpn'
876+
administrator = self._create_administrator(organizations=[org])
877+
self._test_access_shared_object(
878+
listview_name='config_api:vpn_list',
879+
detailview_name='config_api:vpn_detail',
880+
create_payload=create_payload,
881+
update_payload=update_payload,
882+
expected_count=1,
883+
expected_status_codes={
884+
'create': 201,
885+
'list': 200,
886+
'retrieve': 200,
887+
'update': 200,
888+
'delete': 204,
889+
'head': 200,
890+
'option': 200,
891+
},
892+
token=self._obtain_auth_token(administrator),
893+
)
894+
895+
def test_org_admin_create_shared_vpn(self):
837896
shared_ca = self._create_ca(name='shared_ca', organization=None)
838-
test_user = self._create_administrator(organizations=[org1])
839-
self.client.force_login(test_user)
840897
data = self._vpn_data
841-
data['organization'] = org1.pk
842898
data['ca'] = shared_ca.pk
843-
path = reverse('config_api:vpn_list')
844-
r = self.client.post(path, data, content_type='application/json')
845-
self.assertEqual(Vpn.objects.count(), 1)
846-
self.assertEqual(r.status_code, 201)
847-
self.assertEqual(r.data['ca'], shared_ca.pk)
899+
# API does not allow creating shared VPN by org admin,
900+
# therefore we create an object to test the detail view.
901+
self._create_vpn(organization=None, ca=shared_ca)
902+
self._test_org_user_access_shared_object(
903+
listview_name='config_api:vpn_list',
904+
detailview_name='config_api:vpn_detail',
905+
create_payload=data,
906+
expected_count=1,
907+
)
848908

849909
def test_vpn_list_api(self):
850910
org = self._get_org()

openwisp_controller/connection/api/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class Meta:
5151

5252
class CredentialSerializer(BaseSerializer):
5353
params = serializers.JSONField()
54+
include_shared = True
5455

5556
class Meta:
5657
model = Credentials

openwisp_controller/connection/tests/test_api.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from swapper import load_model
1212

1313
from openwisp_controller.tests.utils import TestAdminMixin
14-
from openwisp_users.tests.test_api import AuthenticationMixin
14+
from openwisp_users.tests.test_api import APITestCase, AuthenticationMixin
1515

1616
from .. import settings as app_settings
1717
from ..api.views import ListViewPagination
@@ -343,9 +343,7 @@ def test_non_existent_command(self):
343343
)
344344

345345

346-
class TestConnectionApi(
347-
TestAdminMixin, AuthenticationMixin, TestCase, CreateConnectionsMixin
348-
):
346+
class TestConnectionApi(TestAdminMixin, CreateConnectionsMixin, APITestCase):
349347
def setUp(self):
350348
super().setUp()
351349
self._login()

openwisp_controller/pki/api/serializers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ class Meta:
7373
]
7474
read_only_fields = ['created', 'modified']
7575
extra_kwargs = {
76-
'organization': {'required': True},
76+
# In DRF 3.16+, nullable fields that are part of a unique constraint
77+
# automatically get `default: None`, which can cause validation issues.
78+
# Setting the default to `serializers.empty` ensures DRF does not treat
79+
# these fields as both required and having a default value, avoiding
80+
# conflicts.
81+
'organization': {'required': True, 'default': serializers.empty},
7782
'common_name': {'default': '', 'required': False},
7883
'key_length': {'initial': '2048'},
7984
'digest': {'initial': 'sha256'},

openwisp_controller/pki/tests/test_api.py

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from django.test import TestCase
21
from django.urls import reverse
32
from packaging.version import parse as parse_version
43
from rest_framework import VERSION as REST_FRAMEWORK_VERSION
54
from swapper import load_model
65

76
from openwisp_controller.tests.utils import TestAdminMixin
8-
from openwisp_users.tests.test_api import AuthenticationMixin
9-
from openwisp_users.tests.utils import TestOrganizationMixin
7+
from openwisp_users.tests.test_api import APITestCase
108
from openwisp_utils.tests import AssertNumQueriesSubTestMixin, capture_any_output
119

1210
from .utils import TestPkiMixin
@@ -16,12 +14,7 @@
1614

1715

1816
class TestPkiApi(
19-
AssertNumQueriesSubTestMixin,
20-
TestAdminMixin,
21-
TestPkiMixin,
22-
TestOrganizationMixin,
23-
AuthenticationMixin,
24-
TestCase,
17+
AssertNumQueriesSubTestMixin, TestAdminMixin, TestPkiMixin, APITestCase
2518
):
2619
def setUp(self):
2720
super().setUp()
@@ -168,6 +161,31 @@ def test_ca_post_renew_api(self):
168161
self.assertNotEqual(ca1.serial_number, old_serial_num)
169162
self.assertNotEqual(r.data['serial_number'], old_serial_num)
170163

164+
def test_org_admin_access_shared_ca(self):
165+
# API wouldn't allow creating the object,
166+
# therefore, we create one here to test list view.
167+
self._create_ca()
168+
169+
create_payload = self._ca_data
170+
update_payload = create_payload.copy()
171+
update_payload['name'] = 'updated-name'
172+
self._test_org_user_access_shared_object(
173+
listview_name='pki_api:ca_list',
174+
detailview_name='pki_api:ca_detail',
175+
create_payload=create_payload,
176+
update_payload=update_payload,
177+
expected_count=1,
178+
expected_status_codes={
179+
'create': 400,
180+
'list': 200,
181+
'retrieve': 403,
182+
'update': 403,
183+
'delete': 403,
184+
'head': 403,
185+
'option': 200,
186+
},
187+
)
188+
171189
def test_cert_post_api(self):
172190
path = reverse('pki_api:cert_list')
173191
data = self._cert_data
@@ -307,6 +325,65 @@ def test_post_cert_revoke_api(self):
307325
self.assertTrue(cert1.revoked)
308326
self.assertTrue(r.data['revoked'])
309327

328+
def test_org_admin_access_shared_cert(self):
329+
# API wouldn't allow creating the object,
330+
# therefore, we create one here to test list view.
331+
shared_ca = self._create_ca(organization=None)
332+
self._create_cert(ca=shared_ca)
333+
334+
create_payload = self._cert_data
335+
create_payload['ca'] = shared_ca.pk
336+
update_payload = create_payload.copy()
337+
update_payload['name'] = 'update-name'
338+
self._test_org_user_access_shared_object(
339+
listview_name='pki_api:cert_list',
340+
detailview_name='pki_api:cert_detail',
341+
create_payload=create_payload,
342+
update_payload=update_payload,
343+
expected_count=1,
344+
expected_status_codes={
345+
'create': 400,
346+
'list': 200,
347+
'retrieve': 403,
348+
'update': 403,
349+
'delete': 403,
350+
'head': 403,
351+
'option': 200,
352+
},
353+
)
354+
355+
def test_org_admin_access_cert_with_shared_ca(self):
356+
org = self._get_org()
357+
shared_ca = self._create_ca(organization=None)
358+
create_payload = self._cert_data
359+
create_payload.update(
360+
{
361+
'organization': org.pk,
362+
'ca': shared_ca.pk,
363+
}
364+
)
365+
update_payload = {
366+
'name': 'updated-name',
367+
'organization': org.pk,
368+
'notes': 'new-notes',
369+
}
370+
self._test_org_user_access_shared_object(
371+
listview_name='pki_api:cert_list',
372+
detailview_name='pki_api:cert_detail',
373+
create_payload=create_payload,
374+
update_payload=update_payload,
375+
expected_count=1,
376+
expected_status_codes={
377+
'create': 201,
378+
'list': 200,
379+
'retrieve': 200,
380+
'update': 200,
381+
'delete': 204,
382+
'head': 403,
383+
'option': 200,
384+
},
385+
)
386+
310387
@capture_any_output()
311388
def test_bearer_authentication(self):
312389
self.client.logout()

0 commit comments

Comments
 (0)