Skip to content

Commit 130a70d

Browse files
test: Improve test coverage
1 parent 42ea374 commit 130a70d

File tree

5 files changed

+331
-15
lines changed

5 files changed

+331
-15
lines changed

Diff for: knox/crypto.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,54 @@
77

88

99
def create_token_string() -> str:
10+
"""
11+
Creates a secure random token string using hexadecimal encoding.
12+
13+
The token length is determined by knox_settings.AUTH_TOKEN_CHARACTER_LENGTH.
14+
Since each byte is represented by 2 hexadecimal characters, the number of
15+
random bytes generated is half the desired character length.
16+
17+
Returns:
18+
str: A hexadecimal string of length AUTH_TOKEN_CHARACTER_LENGTH containing
19+
random bytes.
20+
"""
1021
return binascii.hexlify(
1122
generate_bytes(int(knox_settings.AUTH_TOKEN_CHARACTER_LENGTH / 2))
1223
).decode()
1324

1425

1526
def make_hex_compatible(token: str) -> bytes:
1627
"""
28+
Converts a string token into a hex-compatible bytes object.
29+
1730
We need to make sure that the token, that is send is hex-compatible.
1831
When a token prefix is used, we cannot guarantee that.
32+
33+
Args:
34+
token (str): The token string to convert.
35+
36+
Returns:
37+
bytes: The hex-compatible bytes representation of the token.
1938
"""
2039
return binascii.unhexlify(binascii.hexlify(bytes(token, 'utf-8')))
2140

2241

2342
def hash_token(token: str) -> str:
2443
"""
2544
Calculates the hash of a token.
26-
Token must contain an even number of hex digits or
27-
a binascii.Error exception will be raised.
45+
46+
Uses the hash algorithm specified in knox_settings.SECURE_HASH_ALGORITHM.
47+
The token is first converted to a hex-compatible format before hashing.
48+
49+
Args:
50+
token (str): The token string to hash.
51+
52+
Returns:
53+
str: The hexadecimal representation of the token's hash digest.
54+
55+
Example:
56+
>>> hash_token("abc123")
57+
'a123f...' # The actual hash will be longer
2858
"""
2959
digest = hash_func()
3060
digest.update(make_hex_compatible(token))

Diff for: tests/test_crypto.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from django.test import TestCase
2+
from unittest.mock import patch
3+
from knox.settings import knox_settings
4+
from knox.crypto import create_token_string, make_hex_compatible, hash_token
5+
6+
7+
class CryptoUtilsTestCase(TestCase):
8+
def test_create_token_string(self):
9+
"""
10+
Verify token string creation has correct length and contains only hex characters.
11+
"""
12+
with patch('os.urandom') as mock_urandom:
13+
mock_urandom.return_value = b'abcdef1234567890'
14+
expected_length = knox_settings.AUTH_TOKEN_CHARACTER_LENGTH
15+
token = create_token_string()
16+
self.assertEqual(len(token), expected_length)
17+
hex_chars = set('0123456789abcdef')
18+
self.assertTrue(all(c in hex_chars for c in token.lower()))
19+
20+
def test_make_hex_compatible_with_valid_input(self):
21+
"""
22+
Ensure standard strings are correctly converted to hex-compatible bytes.
23+
"""
24+
test_token = "test123"
25+
result = make_hex_compatible(test_token)
26+
self.assertIsInstance(result, bytes)
27+
expected = b'test123'
28+
self.assertEqual(result, expected)
29+
30+
def test_make_hex_compatible_with_empty_string(self):
31+
"""
32+
Verify empty string input returns empty bytes.
33+
"""
34+
test_token = ""
35+
result = make_hex_compatible(test_token)
36+
self.assertEqual(result, b'')
37+
38+
def test_make_hex_compatible_with_special_characters(self):
39+
"""
40+
Check hex compatibility conversion handles special characters correctly.
41+
"""
42+
test_token = "test@#$%"
43+
result = make_hex_compatible(test_token)
44+
self.assertIsInstance(result, bytes)
45+
expected = b'test@#$%'
46+
self.assertEqual(result, expected)
47+
48+
def test_hash_token_with_valid_token(self):
49+
"""
50+
Verify hash output is correct length and contains valid hex characters.
51+
"""
52+
test_token = "abcdef1234567890"
53+
result = hash_token(test_token)
54+
self.assertIsInstance(result, str)
55+
self.assertEqual(len(result), 128)
56+
hex_chars = set('0123456789abcdef')
57+
self.assertTrue(all(c in hex_chars for c in result.lower()))
58+

Diff for: tests/test_models.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from django.test import TestCase, override_settings
2+
from django.contrib.auth import get_user_model
3+
from django.utils import timezone
4+
from django.core.exceptions import ImproperlyConfigured
5+
from datetime import timedelta
6+
from freezegun import freeze_time
7+
8+
from knox.settings import CONSTANTS, knox_settings
9+
from knox.models import AuthToken, get_token_model
10+
11+
class AuthTokenTests(TestCase):
12+
"""
13+
Auth token model tests.
14+
"""
15+
16+
def setUp(self):
17+
self.User = get_user_model()
18+
self.user = self.User.objects.create_user(
19+
username='testuser',
20+
password='testpass123'
21+
)
22+
23+
def test_token_creation(self):
24+
"""
25+
Test that tokens are created correctly with expected format.
26+
"""
27+
token_creation = timezone.now()
28+
with freeze_time(token_creation):
29+
instance, token = AuthToken.objects.create(user=self.user)
30+
self.assertIsNotNone(token)
31+
self.assertTrue(token.startswith(knox_settings.TOKEN_PREFIX))
32+
self.assertEqual(
33+
len(instance.token_key),
34+
CONSTANTS.TOKEN_KEY_LENGTH,
35+
)
36+
self.assertEqual(instance.user, self.user)
37+
self.assertEqual(
38+
instance.expiry,
39+
token_creation + timedelta(hours=10)
40+
)
41+
42+
def test_token_creation_with_expiry(self):
43+
"""
44+
Test token creation with explicit expiry time.
45+
"""
46+
expiry_time = timedelta(hours=10)
47+
before_creation = timezone.now()
48+
instance, _ = AuthToken.objects.create(
49+
user=self.user,
50+
expiry=expiry_time
51+
)
52+
self.assertIsNotNone(instance.expiry)
53+
self.assertTrue(before_creation < instance.expiry)
54+
self.assertTrue(
55+
(instance.expiry - before_creation - expiry_time).total_seconds() < 1
56+
)
57+
58+
def test_token_string_representation(self):
59+
"""
60+
Test the string representation of AuthToken.
61+
"""
62+
instance, _ = AuthToken.objects.create(user=self.user)
63+
expected_str = f'{instance.digest} : {self.user}'
64+
self.assertEqual(str(instance), expected_str)
65+
66+
def test_multiple_tokens_for_user(self):
67+
"""
68+
Test that a user can have multiple valid tokens.
69+
"""
70+
token1, _ = AuthToken.objects.create(user=self.user)
71+
token2, _ = AuthToken.objects.create(user=self.user)
72+
user_tokens = self.user.auth_token_set.all()
73+
self.assertEqual(user_tokens.count(), 2)
74+
self.assertNotEqual(token1.digest, token2.digest)
75+
76+
def test_token_with_custom_prefix(self):
77+
"""
78+
Test token creation with custom prefix.
79+
"""
80+
custom_prefix = "TEST_"
81+
instance, token = AuthToken.objects.create(
82+
user=self.user,
83+
prefix=custom_prefix
84+
)
85+
self.assertTrue(token.startswith(custom_prefix))
86+
self.assertTrue(instance.token_key.startswith(custom_prefix))

Diff for: tests/test_settings.py

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from datetime import timedelta
2+
from unittest import mock
3+
import hashlib
4+
from django.test import override_settings
5+
from django.core.signals import setting_changed
6+
from django.conf import settings
7+
8+
from knox.settings import (
9+
CONSTANTS,
10+
knox_settings,
11+
reload_api_settings,
12+
IMPORT_STRINGS
13+
)
14+
15+
class TestKnoxSettings:
16+
@override_settings(REST_KNOX={
17+
'AUTH_TOKEN_CHARACTER_LENGTH': 32,
18+
'TOKEN_TTL': timedelta(hours=5),
19+
'AUTO_REFRESH': True,
20+
})
21+
def test_override_settings(self):
22+
"""
23+
Test that settings can be overridden.
24+
"""
25+
assert knox_settings.AUTH_TOKEN_CHARACTER_LENGTH == 32
26+
assert knox_settings.TOKEN_TTL == timedelta(hours=5)
27+
assert knox_settings.AUTO_REFRESH is True
28+
# Default values should remain unchanged
29+
assert knox_settings.AUTH_HEADER_PREFIX == 'Token'
30+
31+
def test_constants_immutability(self):
32+
"""
33+
Test that CONSTANTS cannot be modified.
34+
"""
35+
with self.assertRaises(Exception):
36+
CONSTANTS.TOKEN_KEY_LENGTH = 20
37+
38+
with self.assertRaises(Exception):
39+
CONSTANTS.DIGEST_LENGTH = 256
40+
41+
def test_constants_values(self):
42+
"""
43+
Test that CONSTANTS have correct values.
44+
"""
45+
assert CONSTANTS.TOKEN_KEY_LENGTH == 15
46+
assert CONSTANTS.DIGEST_LENGTH == 128
47+
assert CONSTANTS.MAXIMUM_TOKEN_PREFIX_LENGTH == 10
48+
49+
def test_reload_api_settings(self):
50+
"""
51+
Test settings reload functionality.
52+
"""
53+
new_settings = {
54+
'TOKEN_TTL': timedelta(hours=2),
55+
'AUTH_HEADER_PREFIX': 'Bearer',
56+
}
57+
58+
reload_api_settings(
59+
setting='REST_KNOX',
60+
value=new_settings
61+
)
62+
63+
assert knox_settings.TOKEN_TTL == timedelta(hours=2)
64+
assert knox_settings.AUTH_HEADER_PREFIX == 'Bearer'
65+
66+
def test_token_prefix_length_validation(self):
67+
"""
68+
Test that TOKEN_PREFIX length is validated.
69+
"""
70+
with self.assertRaises(ValueError, match="Illegal TOKEN_PREFIX length"):
71+
reload_api_settings(
72+
setting='REST_KNOX',
73+
value={'TOKEN_PREFIX': 'x' * 11} # Exceeds MAXIMUM_TOKEN_PREFIX_LENGTH
74+
)
75+
76+
def test_import_strings(self):
77+
"""
78+
Test that import strings are properly handled.
79+
"""
80+
assert 'SECURE_HASH_ALGORITHM' in IMPORT_STRINGS
81+
assert 'USER_SERIALIZER' in IMPORT_STRINGS
82+
83+
@override_settings(REST_KNOX={
84+
'SECURE_HASH_ALGORITHM': 'hashlib.md5'
85+
})
86+
def test_hash_algorithm_import(self):
87+
"""
88+
Test that hash algorithm is properly imported.
89+
"""
90+
assert knox_settings.SECURE_HASH_ALGORITHM == hashlib.md5
91+
92+
def test_setting_changed_signal(self):
93+
"""
94+
Test that setting_changed signal properly triggers reload.
95+
"""
96+
new_settings = {
97+
'TOKEN_TTL': timedelta(hours=3),
98+
}
99+
100+
setting_changed.send(
101+
sender=None,
102+
setting='REST_KNOX',
103+
value=new_settings
104+
)
105+
106+
assert knox_settings.TOKEN_TTL == timedelta(hours=3)
107+
108+
@mock.patch('django.conf.settings')
109+
def test_custom_token_model(self, mock_settings):
110+
"""
111+
Test custom token model setting.
112+
"""
113+
custom_model = 'custom_app.CustomToken'
114+
mock_settings.KNOX_TOKEN_MODEL = custom_model
115+
116+
# Reload settings
117+
reload_api_settings(
118+
setting='REST_KNOX',
119+
value={}
120+
)
121+
122+
assert knox_settings.TOKEN_MODEL == custom_model

0 commit comments

Comments
 (0)