mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-14 15:28:52 +00:00
Merge commit from fork
* Ensure the MeUserSerializer correctly marks fields as read-only * Bump API version * Add unit tests for the "me" endpoint * Additional unit tests * Add OPTIONS test
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 471
|
INVENTREE_API_VERSION = 472
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v472 -> 2026-04-01 : https://github.com/inventree/InvenTree/pull/xxxx
|
||||||
|
- Fixes writable fields on the user detail endpoint
|
||||||
|
|
||||||
v471 -> 2026-04-07 : https://github.com/inventree/InvenTree/pull/11685
|
v471 -> 2026-04-07 : https://github.com/inventree/InvenTree/pull/11685
|
||||||
- Adds data importer support for the "SalesOrderShipment" model
|
- Adds data importer support for the "SalesOrderShipment" model
|
||||||
|
|
||||||
|
|||||||
@@ -393,6 +393,9 @@ class MeUserSerializer(ExtendedUserSerializer):
|
|||||||
but ensures that certain fields are read-only.
|
but ensures that certain fields are read-only.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Remove the 'group_ids' field, as this is not relevant for the 'me' endpoint
|
||||||
|
fields = [f for f in ExtendedUserSerializer.Meta.fields if f != 'group_ids']
|
||||||
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
*ExtendedUserSerializer.Meta.read_only_fields,
|
*ExtendedUserSerializer.Meta.read_only_fields,
|
||||||
'is_active',
|
'is_active',
|
||||||
@@ -402,6 +405,28 @@ class MeUserSerializer(ExtendedUserSerializer):
|
|||||||
|
|
||||||
profile = UserProfileSerializer(many=False, read_only=True)
|
profile = UserProfileSerializer(many=False, read_only=True)
|
||||||
|
|
||||||
|
# Redefine the fields from ExtendedUserSerializer, to ensure they are marked as read-only
|
||||||
|
is_staff = serializers.BooleanField(
|
||||||
|
label=_('Staff'),
|
||||||
|
help_text=_('Does this user have staff permissions'),
|
||||||
|
required=False,
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_superuser = serializers.BooleanField(
|
||||||
|
label=_('Superuser'),
|
||||||
|
help_text=_('Is this user a superuser'),
|
||||||
|
required=False,
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_active = serializers.BooleanField(
|
||||||
|
label=_('Active'),
|
||||||
|
help_text=_('Is this user account active'),
|
||||||
|
required=False,
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_random_password(length=14):
|
def make_random_password(length=14):
|
||||||
"""Generate a random password of given length."""
|
"""Generate a random password of given length."""
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_api_url(self):
|
def test_api_url(self):
|
||||||
"""Test the 'api_url attribute in related API endpoints.
|
"""Test the 'api_url' attribute in related API endpoints.
|
||||||
|
|
||||||
Ref: https://github.com/inventree/InvenTree/pull/10182
|
Ref: https://github.com/inventree/InvenTree/pull/10182
|
||||||
"""
|
"""
|
||||||
@@ -129,6 +129,19 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertIn('Only a superuser can adjust this field', str(response.data))
|
self.assertIn('Only a superuser can adjust this field', str(response.data))
|
||||||
|
|
||||||
|
# Try again, but with superuser access
|
||||||
|
self.user.is_superuser = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
url,
|
||||||
|
data={**data, 'username': 'Superuser', 'is_superuser': True},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['username'], 'Superuser')
|
||||||
|
self.assertEqual(response.data['is_superuser'], True)
|
||||||
|
|
||||||
def test_user_detail(self):
|
def test_user_detail(self):
|
||||||
"""Test the UserDetail API endpoint."""
|
"""Test the UserDetail API endpoint."""
|
||||||
user = User.objects.first()
|
user = User.objects.first()
|
||||||
@@ -143,7 +156,7 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
self.get(url, expected_code=200)
|
self.get(url, expected_code=200)
|
||||||
|
|
||||||
# Let's try to update the user
|
# Let's try to update the user
|
||||||
data = {'is_active': False, 'is_staff': False}
|
data = {'is_active': True, 'is_staff': False}
|
||||||
|
|
||||||
self.patch(url, data=data, expected_code=403)
|
self.patch(url, data=data, expected_code=403)
|
||||||
|
|
||||||
@@ -158,6 +171,26 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.patch(url, data=data, expected_code=200)
|
self.patch(url, data=data, expected_code=200)
|
||||||
|
|
||||||
|
# Try to change the "is_superuser" field - only a superuser can do this
|
||||||
|
data['is_superuser'] = True
|
||||||
|
response = self.patch(url, data=data, expected_code=403)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
'You do not have permission to perform this action', str(response.data)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.user.is_staff = True
|
||||||
|
self.user.is_superuser = True
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
for val in [True, False]:
|
||||||
|
data['is_staff'] = True
|
||||||
|
data['is_superuser'] = val
|
||||||
|
|
||||||
|
response = self.patch(url, data=data, expected_code=200)
|
||||||
|
self.assertEqual(response.data['is_superuser'], val)
|
||||||
|
self.assertEqual(response.data['is_staff'], True)
|
||||||
|
|
||||||
# Try again, but logged out - expect no access to the endpoint
|
# Try again, but logged out - expect no access to the endpoint
|
||||||
self.logout()
|
self.logout()
|
||||||
self.get(url, expected_code=401)
|
self.get(url, expected_code=401)
|
||||||
@@ -229,6 +262,71 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(data['permissions']), len(perms) + len(build_perms))
|
self.assertEqual(len(data['permissions']), len(perms) + len(build_perms))
|
||||||
|
|
||||||
|
def test_me_endpoint(self):
|
||||||
|
"""Test against the users /me/ endpoint."""
|
||||||
|
url = reverse('api-user-me')
|
||||||
|
|
||||||
|
# Test endpoint options
|
||||||
|
response = self.options(url, expected_code=200)
|
||||||
|
|
||||||
|
# Check that particular fields are present, and have the correct attributes
|
||||||
|
fields = response.data['actions']['PUT']
|
||||||
|
|
||||||
|
for name in [
|
||||||
|
'pk',
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'groups',
|
||||||
|
'is_active',
|
||||||
|
'is_staff',
|
||||||
|
'is_superuser',
|
||||||
|
]:
|
||||||
|
self.assertIn(name, fields)
|
||||||
|
|
||||||
|
for name in ['is_active', 'is_staff', 'is_superuser']:
|
||||||
|
self.assertTrue(fields[name]['read_only'])
|
||||||
|
|
||||||
|
# Perform a GET request against the endpoint
|
||||||
|
response = self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
for field in [
|
||||||
|
'pk',
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'is_active',
|
||||||
|
'is_staff',
|
||||||
|
'is_superuser',
|
||||||
|
]:
|
||||||
|
self.assertIn(field, response.data)
|
||||||
|
|
||||||
|
# Change their own username
|
||||||
|
for name in ['Henry', 'Sally']:
|
||||||
|
response = self.patch(url, data={'username': name}, expected_code=200)
|
||||||
|
self.assertEqual(response.data['username'], name)
|
||||||
|
|
||||||
|
# Defined starting point for the user
|
||||||
|
for v in [True, False]:
|
||||||
|
self.user.is_staff = v
|
||||||
|
self.user.is_superuser = v
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
for key in ['is_staff', 'is_superuser']:
|
||||||
|
for val in [True, False]:
|
||||||
|
response = self.patch(url, data={key: val}, expected_code=200)
|
||||||
|
|
||||||
|
# Check that the field was *NOT CHANGED*
|
||||||
|
self.assertEqual(response.data[key], v)
|
||||||
|
|
||||||
|
# Ensure we cannot change the "is_active" field either
|
||||||
|
response = self.patch(url, data={'is_active': False}, expected_code=200)
|
||||||
|
self.assertEqual(response.data['is_active'], True)
|
||||||
|
|
||||||
|
self.user.is_active = False
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
# User cannot fetch their own details if they are not active
|
||||||
|
response = self.get(url, expected_code=401)
|
||||||
|
|
||||||
|
|
||||||
class SuperuserAPITests(InvenTreeAPITestCase):
|
class SuperuserAPITests(InvenTreeAPITestCase):
|
||||||
"""Tests for user API endpoints that require superuser rights."""
|
"""Tests for user API endpoints that require superuser rights."""
|
||||||
@@ -245,7 +343,7 @@ class SuperuserAPITests(InvenTreeAPITestCase):
|
|||||||
resp = self.put(url, {'password': 1}, expected_code=400)
|
resp = self.put(url, {'password': 1}, expected_code=400)
|
||||||
self.assertContains(resp, 'This password is too short', status_code=400)
|
self.assertContains(resp, 'This password is too short', status_code=400)
|
||||||
|
|
||||||
# now with overwerite
|
# now with overwrite
|
||||||
resp = self.put(
|
resp = self.put(
|
||||||
url, {'password': 1, 'override_warning': True}, expected_code=200
|
url, {'password': 1, 'override_warning': True}, expected_code=200
|
||||||
)
|
)
|
||||||
@@ -422,7 +520,7 @@ class UserTokenTests(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(ApiToken.objects.count(), 1)
|
self.assertEqual(ApiToken.objects.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
class GroupDetialTests(InvenTreeAPITestCase):
|
class GroupDetailTests(InvenTreeAPITestCase):
|
||||||
"""Tests for the GroupDetail API endpoint."""
|
"""Tests for the GroupDetail API endpoint."""
|
||||||
|
|
||||||
fixtures = ['users']
|
fixtures = ['users']
|
||||||
|
|||||||
Reference in New Issue
Block a user