#31 API Tests
* writes test for sharing using the API * fixes bug on frontend form where an exception occured on generating a new API token if no token existed, yet * adds permission constraint (default group) for using the api in general * fixes default-group-only behaviour for sharing-API, so users can only add new users and not removing them, as long as they do not have any other group membership like registration or conservation office * changes 'ksptoken' to 'Ksptoken' to match CGI standard for http header keys
This commit is contained in:
parent
6deff28389
commit
1b0ab1be07
@ -5,4 +5,4 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 21.01.22
|
||||
|
||||
"""
|
||||
KSP_TOKEN_HEADER_IDENTIFIER = "ksptoken"
|
||||
KSP_TOKEN_HEADER_IDENTIFIER = "Ksptoken"
|
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
7
api/tests/v1/__init__.py
Normal file
7
api/tests/v1/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 27.01.22
|
||||
|
||||
"""
|
150
api/tests/v1/test_api_sharing.py
Normal file
150
api/tests/v1/test_api_sharing.py
Normal file
@ -0,0 +1,150 @@
|
||||
import json
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from konova.settings import DEFAULT_GROUP
|
||||
from konova.tests.test_views import BaseTestCase
|
||||
from konova.utils.user_checks import is_default_group_only
|
||||
|
||||
|
||||
class APIV1SharingTestCase(BaseTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
""" Creates a test file in memory for using in further tests
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
super().setUpTestData()
|
||||
cls.superuser.get_API_token()
|
||||
cls.superuser.api_token.is_active = True
|
||||
cls.superuser.api_token.save()
|
||||
default_group = cls.groups.get(name=DEFAULT_GROUP)
|
||||
cls.superuser.groups.add(default_group)
|
||||
|
||||
cls.header_data = {
|
||||
"HTTP_ksptoken": cls.superuser.api_token.token,
|
||||
}
|
||||
|
||||
def _run_share_request(self, url, user_list: list):
|
||||
data = {
|
||||
"users": user_list
|
||||
}
|
||||
data = json.dumps(data)
|
||||
response = self.client.put(
|
||||
url,
|
||||
data,
|
||||
**self.header_data
|
||||
)
|
||||
return response
|
||||
|
||||
def _test_api_sharing(self, obj, url):
|
||||
""" Generic test for testing sharing of a ShareableObjectMixin object
|
||||
|
||||
Args:
|
||||
obj (ShareableObjectMixin): The object
|
||||
url (str): The url to be used for a request
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.assertEqual(obj.users.count(), 0)
|
||||
user_list = [
|
||||
self.superuser.username,
|
||||
self.user.username,
|
||||
]
|
||||
|
||||
response = self._run_share_request(url, user_list)
|
||||
|
||||
# Must fail, since performing user has no access on requested object
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertTrue(len(json.loads(response.content.decode("utf-8")).get("errors", [])) > 0)
|
||||
|
||||
# Add performing user to shared access users and rerun the request
|
||||
obj.users.add(self.superuser)
|
||||
response = self._run_share_request(url, user_list)
|
||||
|
||||
shared_users = obj.shared_users
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(shared_users.count(), 2)
|
||||
self.assertIn(self.superuser, shared_users)
|
||||
self.assertIn(self.user, shared_users)
|
||||
|
||||
def test_api_token_invalid(self):
|
||||
""" Tests that a request with an invalid token won't be successfull
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
|
||||
# Expect the first request to work properly
|
||||
self.intervention.users.add(self.superuser)
|
||||
response = self._run_share_request(share_url, [self.superuser.username])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Change the token
|
||||
self.header_data["HTTP_ksptoken"] = f"{self.superuser.api_token.token}__X"
|
||||
|
||||
# Expect the request to fail now
|
||||
response = self._run_share_request(share_url, [self.superuser.username])
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_api_intervention_sharing(self):
|
||||
""" Tests proper sharing of intervention
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
|
||||
self._test_api_sharing(self.intervention, share_url)
|
||||
|
||||
def test_api_eco_account_sharing(self):
|
||||
""" Tests proper sharing of eco account
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
share_url = reverse("api:v1:ecoaccount-share", args=(self.eco_account.id,))
|
||||
self._test_api_sharing(self.eco_account, share_url)
|
||||
|
||||
def test_api_ema_sharing(self):
|
||||
""" Tests proper sharing of ema
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
share_url = reverse("api:v1:ema-share", args=(self.ema.id,))
|
||||
self._test_api_sharing(self.ema, share_url)
|
||||
|
||||
def test_api_sharing_as_default_group_only(self):
|
||||
""" Tests that sharing using the API as an only default group user works as expected.
|
||||
|
||||
Expected:
|
||||
Default only user can only add new users, having shared access. Removing them from the list of users
|
||||
having shared access is only possible if the user has further rights, e.g. being part of a registration
|
||||
or conservation office group.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
|
||||
|
||||
# Give the user only default group rights
|
||||
default_group = self.groups.get(name=DEFAULT_GROUP)
|
||||
self.superuser.groups.set([default_group])
|
||||
self.assertTrue(is_default_group_only(self.superuser))
|
||||
|
||||
# Add only him as shared_users an object
|
||||
self.intervention.users.set([self.superuser])
|
||||
self.assertEqual(self.intervention.users.count(), 1)
|
||||
|
||||
# Try to add another user via API -> must work!
|
||||
response = self._run_share_request(share_url, [self.superuser.username, self.user.username])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.intervention.users.count(), 2)
|
||||
|
||||
# Now try to remove the user again -> expect no changes at all to the shared user list
|
||||
response = self._run_share_request(share_url, [self.superuser.username])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.intervention.users.count(), 2)
|
@ -18,6 +18,7 @@ from compensation.models import EcoAccount
|
||||
from ema.models import Ema
|
||||
from intervention.models import Intervention
|
||||
from konova.utils.message_templates import DATA_UNSHARED
|
||||
from konova.utils.user_checks import is_default_group_only
|
||||
from user.models import User
|
||||
|
||||
|
||||
@ -39,6 +40,8 @@ class AbstractAPIView(View):
|
||||
try:
|
||||
# Fetch the proper user from the given request header token
|
||||
self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None))
|
||||
if not self.user.is_default_user():
|
||||
raise PermissionError("Default permissions required")
|
||||
except PermissionError as e:
|
||||
return self.return_error_response(e, 403)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
@ -240,6 +243,15 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
||||
new_users_objs = []
|
||||
for user in new_users:
|
||||
new_users_objs.append(User.objects.get(username=user))
|
||||
|
||||
if is_default_group_only(self.user):
|
||||
# Default only users are not allowed to remove other users from having access. They can only add new ones!
|
||||
new_users_to_be_added = User.objects.filter(
|
||||
username__in=new_users
|
||||
).exclude(
|
||||
id__in=obj.shared_users
|
||||
)
|
||||
new_users_objs = obj.shared_users.union(new_users_to_be_added)
|
||||
obj.share_with_list(new_users_objs)
|
||||
return True
|
||||
|
||||
|
@ -7,6 +7,7 @@ Created on: 26.10.21
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from ema.models import Ema
|
||||
from user.models import User
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||
@ -52,6 +53,7 @@ class BaseTestCase(TestCase):
|
||||
cls.intervention = cls.create_dummy_intervention()
|
||||
cls.compensation = cls.create_dummy_compensation()
|
||||
cls.eco_account = cls.create_dummy_eco_account()
|
||||
cls.ema = cls.create_dummy_ema()
|
||||
cls.create_dummy_states()
|
||||
cls.create_dummy_action()
|
||||
cls.codes = cls.create_dummy_codes()
|
||||
@ -168,6 +170,30 @@ class BaseTestCase(TestCase):
|
||||
)
|
||||
return eco_account
|
||||
|
||||
@classmethod
|
||||
def create_dummy_ema(cls):
|
||||
""" Creates an ema which can be used for tests
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Create dummy data
|
||||
# Create log entry
|
||||
action = UserActionLogEntry.get_created_action(cls.superuser)
|
||||
geometry = Geometry.objects.create()
|
||||
# Create responsible data object
|
||||
responsible_data = Responsibility.objects.create()
|
||||
# Finally create main object, holding the other objects
|
||||
ema = Ema.objects.create(
|
||||
identifier="TEST",
|
||||
title="Test_title",
|
||||
responsible=responsible_data,
|
||||
created=action,
|
||||
geometry=geometry,
|
||||
comment="Test",
|
||||
)
|
||||
return ema
|
||||
|
||||
@classmethod
|
||||
def create_dummy_states(cls):
|
||||
""" Creates an intervention which can be used for tests
|
||||
|
@ -152,6 +152,7 @@ class UserAPITokenForm(BaseForm):
|
||||
"""
|
||||
user = self.instance
|
||||
new_token = self.cleaned_data["token"]
|
||||
if user.api_token is not None:
|
||||
user.api_token.delete()
|
||||
new_token = APIUserToken.objects.create(
|
||||
token=new_token
|
||||
|
@ -12,7 +12,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Authenticated by admins' %}</th>
|
||||
{% if user.api_token.is_active %}
|
||||
{% if user.api_token is None %}
|
||||
<td></td>
|
||||
{% elif user.api_token.is_active %}
|
||||
<td class="text-success" title="{% trans 'Token has been verified and can be used' %}">{% fa5_icon 'check-circle' %}</td>
|
||||
{% else %}
|
||||
<td class="text-primary" title="{% trans 'Token waiting for verification' %}">{% fa5_icon 'hourglass-half' %}</td>
|
||||
|
Loading…
Reference in New Issue
Block a user