|
|
@@ -1,10 +1,13 @@
|
|
|
+import secrets
|
|
|
from datetime import timedelta
|
|
|
+from unittest.mock import patch
|
|
|
|
|
|
from django.core.exceptions import ValidationError
|
|
|
from django.test import TestCase, override_settings
|
|
|
from django.utils import timezone
|
|
|
|
|
|
from users.choices import TokenVersionChoices
|
|
|
+from users.constants import TOKEN_CHARSET, TOKEN_DEFAULT_LENGTH
|
|
|
from users.models import Token, User
|
|
|
from utilities.testing import create_test_user
|
|
|
|
|
|
@@ -104,6 +107,29 @@ class TokenTestCase(TestCase):
|
|
|
with self.assertRaises(ValidationError):
|
|
|
token.clean()
|
|
|
|
|
|
+ def test_generate_uses_csprng(self):
|
|
|
+ """
|
|
|
+ Regression: Token.generate() must use secrets.choice (CSPRNG), not random.choice
|
|
|
+ (Mersenne Twister). Verify that the call is routed through the secrets module.
|
|
|
+ """
|
|
|
+ with patch('users.models.tokens.secrets.choice', wraps=secrets.choice) as mock_choice:
|
|
|
+ value = Token.generate()
|
|
|
+
|
|
|
+ self.assertEqual(mock_choice.call_count, TOKEN_DEFAULT_LENGTH,
|
|
|
+ "secrets.choice must be called once per token character")
|
|
|
+ self.assertEqual(len(value), TOKEN_DEFAULT_LENGTH)
|
|
|
+ self.assertTrue(all(c in TOKEN_CHARSET for c in value),
|
|
|
+ "Generated token must only contain characters from TOKEN_CHARSET")
|
|
|
+
|
|
|
+ def test_generate_length_parameter(self):
|
|
|
+ """
|
|
|
+ Token.generate(length=N) returns a string of exactly N characters from TOKEN_CHARSET.
|
|
|
+ """
|
|
|
+ for length in (8, 20, TOKEN_DEFAULT_LENGTH, 64):
|
|
|
+ value = Token.generate(length=length)
|
|
|
+ self.assertEqual(len(value), length, f"Expected length {length}, got {len(value)}")
|
|
|
+ self.assertTrue(all(c in TOKEN_CHARSET for c in value))
|
|
|
+
|
|
|
|
|
|
class UserConfigTestCase(TestCase):
|
|
|
|