Skip to content

Commit a2c2657

Browse files
committed
[tests] Added multi-tentant tests for shared objects
1 parent 4ea68be commit a2c2657

File tree

5 files changed

+198
-57
lines changed

5 files changed

+198
-57
lines changed

openwisp_controller/config/tests/test_api.py

Lines changed: 97 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from openwisp_controller.config.api.serializers import BaseConfigSerializer
1212
from openwisp_controller.tests.utils import TestAdminMixin
13-
from openwisp_users.tests.test_api import AuthenticationMixin
13+
from openwisp_users.tests.test_api import AuthenticationMixin, TestMultitenantApiMixin
1414
from openwisp_utils.tests import capture_any_output, catch_signal
1515

1616
from .. import settings as app_settings
@@ -33,7 +33,7 @@
3333
OrganizationUser = load_model("openwisp_users", "OrganizationUser")
3434

3535

36-
class ApiTestMixin:
36+
class ApiTestMixin(AuthenticationMixin, TestMultitenantApiMixin):
3737
@property
3838
def _template_data(self):
3939
return {
@@ -103,7 +103,6 @@ class TestConfigApi(
103103
CreateConfigTemplateMixin,
104104
TestVpnX509Mixin,
105105
CreateDeviceGroupMixin,
106-
AuthenticationMixin,
107106
TestCase,
108107
):
109108
def setUp(self):
@@ -671,29 +670,59 @@ def test_template_create_of_vpn_type(self):
671670
self.assertEqual(Template.objects.count(), 1)
672671
self.assertEqual(r.status_code, 201)
673672

674-
def test_template_create_with_shared_vpn(self):
675-
org1 = self._get_org()
676-
test_user = self._create_operator(organizations=[org1])
677-
self.client.force_login(test_user)
678-
vpn1 = self._create_vpn(name="vpn1", organization=None)
679-
path = reverse("config_api:template_list")
680-
data = self._template_data
681-
data["type"] = "vpn"
682-
data["vpn"] = vpn1.id
683-
data["organization"] = org1.pk
684-
r = self.client.post(path, data, content_type="application/json")
685-
self.assertEqual(r.status_code, 201)
686-
self.assertEqual(Template.objects.count(), 1)
687-
self.assertEqual(r.data["vpn"], vpn1.id)
688-
689-
def test_template_creation_with_no_org_by_operator(self):
690-
path = reverse("config_api:template_list")
691-
data = self._template_data
673+
def test_operator_access_shared_template(self):
692674
test_user = self._create_operator(organizations=[self._get_org()])
693-
self.client.force_login(test_user)
694-
r = self.client.post(path, data, content_type="application/json")
695-
self.assertEqual(r.status_code, 400)
696-
self.assertIn("This field may not be null.", str(r.content))
675+
token = self._obtain_auth_token(test_user)
676+
self._create_template(organization=None)
677+
self._test_org_user_access_shared_object(
678+
listview_name='config_api:template_list',
679+
detailview_name='config_api:template_detail',
680+
create_payload={'name': 'test', 'organization': ''},
681+
update_payload={'name': 'updated-test'},
682+
expected_count=1,
683+
token=token,
684+
expected_status_codes={
685+
'create': 400,
686+
'list': 200,
687+
'retrieve': 403,
688+
'update': 403,
689+
'delete': 403,
690+
'head': 403,
691+
'option': 200,
692+
},
693+
)
694+
695+
def test_org_admin_create_template_with_shared_vpn(self):
696+
org = self._get_org()
697+
vpn = self._create_vpn(organization=None)
698+
create_payload = self._template_data
699+
create_payload.update(
700+
{
701+
'organization': org.pk,
702+
'type': 'vpn',
703+
'vpn': vpn.pk,
704+
}
705+
)
706+
update_payload = create_payload.copy()
707+
update_payload['name'] = 'updated-test'
708+
test_user = self._create_operator(organizations=[org])
709+
self._test_org_user_access_shared_object(
710+
listview_name='config_api:template_list',
711+
detailview_name='config_api:template_detail',
712+
create_payload=create_payload,
713+
update_payload=update_payload,
714+
expected_count=1,
715+
expected_status_codes={
716+
'create': 201,
717+
'list': 200,
718+
'retrieve': 200,
719+
'update': 200,
720+
'delete': 204,
721+
'head': 403,
722+
'option': 200,
723+
},
724+
token=self._obtain_auth_token(test_user),
725+
)
697726

698727
def test_template_create_with_empty_config(self):
699728
path = reverse("config_api:template_list")
@@ -855,19 +884,50 @@ def test_vpn_create_api(self):
855884
self.assertEqual(r.status_code, 201)
856885
self.assertEqual(Vpn.objects.count(), 1)
857886

858-
def test_vpn_create_with_shared_objects(self):
859-
org1 = self._get_org()
860-
shared_ca = self._create_ca(name="shared_ca", organization=None)
861-
test_user = self._create_administrator(organizations=[org1])
862-
self.client.force_login(test_user)
887+
def test_org_admin_access_vpn_with_shared_objects(self):
888+
org = self._get_org()
889+
shared_ca = self._create_ca(name='shared_ca', organization=None)
890+
create_payload = self._vpn_data
891+
create_payload.update(
892+
{
893+
'organization': org.pk,
894+
'ca': shared_ca.pk,
895+
}
896+
)
897+
update_payload = create_payload.copy()
898+
update_payload['name'] = 'updated-test-vpn'
899+
administrator = self._create_administrator(organizations=[org])
900+
self._test_access_shared_object(
901+
listview_name='config_api:vpn_list',
902+
detailview_name='config_api:vpn_detail',
903+
create_payload=create_payload,
904+
update_payload=update_payload,
905+
expected_count=1,
906+
expected_status_codes={
907+
'create': 201,
908+
'list': 200,
909+
'retrieve': 200,
910+
'update': 200,
911+
'delete': 204,
912+
'head': 200,
913+
'option': 200,
914+
},
915+
token=self._obtain_auth_token(administrator),
916+
)
917+
918+
def test_org_admin_create_shared_vpn(self):
919+
shared_ca = self._create_ca(name='shared_ca', organization=None)
863920
data = self._vpn_data
864-
data["organization"] = org1.pk
865-
data["ca"] = shared_ca.pk
866-
path = reverse("config_api:vpn_list")
867-
r = self.client.post(path, data, content_type="application/json")
868-
self.assertEqual(Vpn.objects.count(), 1)
869-
self.assertEqual(r.status_code, 201)
870-
self.assertEqual(r.data["ca"], shared_ca.pk)
921+
data['ca'] = shared_ca.pk
922+
# API does not allow creating shared VPN by org admin,
923+
# therefore we create an object to test the detail view.
924+
self._create_vpn(organization=None, ca=shared_ca)
925+
self._test_org_user_access_shared_object(
926+
listview_name='config_api:vpn_list',
927+
detailview_name='config_api:vpn_detail',
928+
create_payload=data,
929+
expected_count=1,
930+
)
871931

872932
def test_vpn_list_api(self):
873933
org = self._get_org()

openwisp_controller/connection/api/serializers.py

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

6565
class CredentialSerializer(BaseSerializer):
6666
params = serializers.JSONField()
67+
include_shared = True
6768

6869
class Meta:
6970
model = Credentials

openwisp_controller/connection/tests/test_api.py

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

1414
from openwisp_controller.tests.utils import TestAdminMixin
15-
from openwisp_users.tests.test_api import AuthenticationMixin
15+
from openwisp_users.tests.test_api import APITestCase, AuthenticationMixin
1616

1717
from .. import settings as app_settings
1818
from ..api.views import ListViewPagination
@@ -387,9 +387,7 @@ def test_create_command_without_connection(self):
387387
)
388388

389389

390-
class TestConnectionApi(
391-
TestAdminMixin, AuthenticationMixin, TestCase, CreateConnectionsMixin
392-
):
390+
class TestConnectionApi(TestAdminMixin, CreateConnectionsMixin, APITestCase):
393391
def setUp(self):
394392
super().setUp()
395393
self._login()

openwisp_controller/pki/api/serializers.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,18 @@ class Meta:
7373
]
7474
read_only_fields = ["created", "modified"]
7575
extra_kwargs = {
76-
"organization": {"required": True},
77-
"common_name": {"default": "", "required": False},
78-
"key_length": {"initial": "2048"},
79-
"digest": {"initial": "sha256"},
80-
"passphrase": {"write_only": True},
81-
"validity_start": {"default": default_validity_start()},
82-
"validity_end": {"default": default_ca_validity_end()},
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},
82+
'common_name': {'default': '', 'required': False},
83+
'key_length': {'initial': '2048'},
84+
'digest': {'initial': 'sha256'},
85+
'passphrase': {'write_only': True},
86+
'validity_start': {'default': default_validity_start()},
87+
'validity_end': {'default': default_ca_validity_end()},
8388
}
8489

8590

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)