test_models.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import secrets
  2. from datetime import timedelta
  3. from unittest.mock import patch
  4. from django.core.exceptions import ValidationError
  5. from django.test import TestCase, override_settings
  6. from django.utils import timezone
  7. from users.choices import TokenVersionChoices
  8. from users.constants import TOKEN_CHARSET, TOKEN_DEFAULT_LENGTH
  9. from users.models import Token, User
  10. from utilities.testing import create_test_user
  11. class TokenTestCase(TestCase):
  12. """
  13. Test class for testing the functionality of the Token model.
  14. """
  15. @classmethod
  16. def setUpTestData(cls):
  17. """
  18. Set up test data for the Token model.
  19. """
  20. cls.user = create_test_user('User 1')
  21. def test_is_active(self):
  22. """
  23. Test the is_active property.
  24. """
  25. # Token with enabled status and no expiration date
  26. token = Token(user=self.user, enabled=True, expires=None)
  27. self.assertTrue(token.is_active)
  28. # Token with disabled status
  29. token.enabled = False
  30. self.assertFalse(token.is_active)
  31. # Token with enabled status and future expiration
  32. future_date = timezone.now() + timedelta(days=1)
  33. token = Token(user=self.user, enabled=True, expires=future_date)
  34. self.assertTrue(token.is_active)
  35. # Token with past expiration
  36. token.expires = timezone.now() - timedelta(days=1)
  37. self.assertFalse(token.is_active)
  38. # Token with disabled status and past expiration
  39. past_date = timezone.now() - timedelta(days=1)
  40. token = Token(user=self.user, enabled=False, expires=past_date)
  41. self.assertFalse(token.is_active)
  42. def test_is_expired(self):
  43. """
  44. Test the is_expired property.
  45. """
  46. # Token with no expiration
  47. token = Token(user=self.user, expires=None)
  48. self.assertFalse(token.is_expired)
  49. # Token with future expiration
  50. token.expires = timezone.now() + timedelta(days=1)
  51. self.assertFalse(token.is_expired)
  52. # Token with past expiration
  53. token.expires = timezone.now() - timedelta(days=1)
  54. self.assertTrue(token.is_expired)
  55. def test_cannot_create_token_with_past_expiration(self):
  56. """
  57. Test that creating a token with an expiration date in the past raises a ValidationError.
  58. """
  59. past_date = timezone.now() - timedelta(days=1)
  60. token = Token(user=self.user, expires=past_date)
  61. with self.assertRaises(ValidationError) as cm:
  62. token.clean()
  63. self.assertIn('expires', cm.exception.error_dict)
  64. def test_can_update_existing_expired_token(self):
  65. """
  66. Test that updating an already expired token does NOT raise a ValidationError.
  67. """
  68. # Create a valid token first with an expiration date in the past
  69. # bypasses the clean() method
  70. token = Token.objects.create(user=self.user)
  71. token.expires = timezone.now() - timedelta(days=1)
  72. token.save()
  73. # Try to update the description
  74. token.description = 'New Description'
  75. try:
  76. token.clean()
  77. token.save()
  78. except ValidationError:
  79. self.fail('Updating an expired token should not raise ValidationError')
  80. token.refresh_from_db()
  81. self.assertEqual(token.description, 'New Description')
  82. @override_settings(API_TOKEN_PEPPERS={})
  83. def test_v2_without_peppers_configured(self):
  84. """
  85. Attempting to save a v2 token without API_TOKEN_PEPPERS defined should raise a ValidationError.
  86. """
  87. token = Token(version=TokenVersionChoices.V2)
  88. with self.assertRaises(ValidationError):
  89. token.clean()
  90. def test_generate_uses_csprng(self):
  91. """
  92. Regression: Token.generate() must use secrets.choice (CSPRNG), not random.choice
  93. (Mersenne Twister). Verify that the call is routed through the secrets module.
  94. """
  95. with patch('users.models.tokens.secrets.choice', wraps=secrets.choice) as mock_choice:
  96. value = Token.generate()
  97. self.assertEqual(mock_choice.call_count, TOKEN_DEFAULT_LENGTH,
  98. "secrets.choice must be called once per token character")
  99. self.assertEqual(len(value), TOKEN_DEFAULT_LENGTH)
  100. self.assertTrue(all(c in TOKEN_CHARSET for c in value),
  101. "Generated token must only contain characters from TOKEN_CHARSET")
  102. def test_generate_length_parameter(self):
  103. """
  104. Token.generate(length=N) returns a string of exactly N characters from TOKEN_CHARSET.
  105. """
  106. for length in (8, 20, TOKEN_DEFAULT_LENGTH, 64):
  107. value = Token.generate(length=length)
  108. self.assertEqual(len(value), length, f"Expected length {length}, got {len(value)}")
  109. self.assertTrue(all(c in TOKEN_CHARSET for c in value))
  110. class UserConfigTestCase(TestCase):
  111. @classmethod
  112. def setUpTestData(cls):
  113. user = User.objects.create_user(username='testuser')
  114. user.config.data = {
  115. 'a': True,
  116. 'b': {
  117. 'foo': 101,
  118. 'bar': 102,
  119. },
  120. 'c': {
  121. 'foo': {
  122. 'x': 201,
  123. },
  124. 'bar': {
  125. 'y': 202,
  126. },
  127. 'baz': {
  128. 'z': 203,
  129. }
  130. }
  131. }
  132. user.config.save()
  133. def test_get(self):
  134. userconfig = User.objects.get(username='testuser').config
  135. # Retrieve root and nested values
  136. self.assertEqual(userconfig.get('a'), True)
  137. self.assertEqual(userconfig.get('b.foo'), 101)
  138. self.assertEqual(userconfig.get('c.baz.z'), 203)
  139. # Invalid values should return None
  140. self.assertIsNone(userconfig.get('invalid'))
  141. self.assertIsNone(userconfig.get('a.invalid'))
  142. self.assertIsNone(userconfig.get('b.foo.invalid'))
  143. self.assertIsNone(userconfig.get('b.foo.x.invalid'))
  144. # Invalid values with a provided default should return the default
  145. self.assertEqual(userconfig.get('invalid', 'DEFAULT'), 'DEFAULT')
  146. self.assertEqual(userconfig.get('a.invalid', 'DEFAULT'), 'DEFAULT')
  147. self.assertEqual(userconfig.get('b.foo.invalid', 'DEFAULT'), 'DEFAULT')
  148. self.assertEqual(userconfig.get('b.foo.x.invalid', 'DEFAULT'), 'DEFAULT')
  149. def test_all(self):
  150. userconfig = User.objects.get(username='testuser').config
  151. flattened_data = {
  152. 'a': True,
  153. 'b.foo': 101,
  154. 'b.bar': 102,
  155. 'c.foo.x': 201,
  156. 'c.bar.y': 202,
  157. 'c.baz.z': 203,
  158. }
  159. # Retrieve a flattened dictionary containing all config data
  160. self.assertEqual(userconfig.all(), flattened_data)
  161. def test_set(self):
  162. userconfig = User.objects.get(username='testuser').config
  163. # Overwrite existing values
  164. userconfig.set('a', 'abc')
  165. userconfig.set('c.foo.x', 'abc')
  166. self.assertEqual(userconfig.data['a'], 'abc')
  167. self.assertEqual(userconfig.data['c']['foo']['x'], 'abc')
  168. # Create new values
  169. userconfig.set('d', 'abc')
  170. userconfig.set('b.baz', 'abc')
  171. self.assertEqual(userconfig.data['d'], 'abc')
  172. self.assertEqual(userconfig.data['b']['baz'], 'abc')
  173. # Set a value and commit to the database
  174. userconfig.set('a', 'def', commit=True)
  175. userconfig.refresh_from_db()
  176. self.assertEqual(userconfig.data['a'], 'def')
  177. # Attempt to change a branch node to a leaf node
  178. with self.assertRaises(TypeError):
  179. userconfig.set('b', 1)
  180. # Attempt to change a leaf node to a branch node
  181. with self.assertRaises(TypeError):
  182. userconfig.set('a.x', 1)
  183. def test_clear(self):
  184. userconfig = User.objects.get(username='testuser').config
  185. # Clear existing values
  186. userconfig.clear('a')
  187. userconfig.clear('b.foo')
  188. self.assertTrue('a' not in userconfig.data)
  189. self.assertTrue('foo' not in userconfig.data['b'])
  190. self.assertEqual(userconfig.data['b']['bar'], 102)
  191. # Clear a non-existing value; should fail silently
  192. userconfig.clear('invalid')
  193. @override_settings(MAINTENANCE_MODE=True)
  194. def test_commit_skipped_in_maintenance_mode(self):
  195. userconfig = User.objects.get(username='testuser').config
  196. # The change is applied, but not saved
  197. userconfig.set('a', 'def', commit=True)
  198. userconfig.clear('b.foo', commit=True)
  199. self.assertEqual(userconfig.data['a'], 'def')
  200. self.assertNotIn('foo', userconfig.data['b'])
  201. userconfig.refresh_from_db()
  202. self.assertEqual(userconfig.data['a'], True)
  203. self.assertEqual(userconfig.data['b']['foo'], 101)
  204. class RestrictedQuerySetTestCase(TestCase):
  205. """
  206. Test the is_active handling of RestrictedQuerySet.restrict()'s superuser bypass.
  207. """
  208. @classmethod
  209. def setUpTestData(cls):
  210. # Token uses a plain RestrictedQuerySet manager, so its objects() exercises restrict()
  211. # directly without requiring any object permissions to be configured.
  212. cls.token = Token.objects.create(user=create_test_user('Token Owner'))
  213. def test_active_superuser_bypasses_restriction(self):
  214. user = User.objects.create(username='active_su', is_superuser=True, is_active=True)
  215. self.assertIn(self.token, Token.objects.restrict(user, 'view'))
  216. def test_inactive_superuser_does_not_bypass_restriction(self):
  217. """
  218. A deactivated superuser must not bypass restrict(). Without an explicit
  219. view permission they receive an empty queryset, mirroring the is_active
  220. guard in ObjectPermissionMixin.has_perm.
  221. """
  222. user = User.objects.create(username='inactive_su', is_superuser=True, is_active=False)
  223. self.assertNotIn(self.token, Token.objects.restrict(user, 'view'))
  224. self.assertEqual(Token.objects.restrict(user, 'view').count(), 0)