diff --git a/tests/test_api_tokens.py b/tests/test_api_tokens.py index 331631b7..5415b9c7 100644 --- a/tests/test_api_tokens.py +++ b/tests/test_api_tokens.py @@ -534,6 +534,42 @@ class TestTokenUtils: tokens = {generate_api_token() for _ in range(100)} assert len(tokens) == 100 + @pytest.mark.unit + def test_generate_api_token_length(self): + """Generated tokens should have the exact expected length based on TOKEN_BYTES.""" + import math + from app.api.api_tokens import generate_api_token, TOKEN_PREFIX, TOKEN_BYTES + + # base64url encoding of N bytes without padding: ceil(N * 4 / 3) characters + expected_b64_len = math.ceil(TOKEN_BYTES * 4 / 3) + expected_total_len = len(TOKEN_PREFIX) + expected_b64_len + + token = generate_api_token() + assert len(token) == expected_total_len + + @pytest.mark.unit + def test_generate_api_token_charset(self): + """Generated tokens should only contain URL-safe base64 characters and the prefix.""" + import re + from app.api.api_tokens import generate_api_token, TOKEN_PREFIX + + token = generate_api_token() + # Check it starts with prefix and the rest is base64url chars ([A-Za-z0-9_-]) + pattern = f"^{re.escape(TOKEN_PREFIX)}[A-Za-z0-9_\\-]+$" + assert re.match(pattern, token) is not None + + @pytest.mark.unit + def test_generate_api_token_uses_secrets(self): + """Generated tokens should use secrets.token_urlsafe with the correct number of bytes.""" + from unittest.mock import patch + from app.api.api_tokens import generate_api_token, TOKEN_BYTES, TOKEN_PREFIX + + with patch("app.api.api_tokens.secrets.token_urlsafe", return_value="mocked_token") as mock_secrets: + token = generate_api_token() + mock_secrets.assert_called_once_with(TOKEN_BYTES) + assert token == f"{TOKEN_PREFIX}mocked_token" + + @pytest.mark.unit def test_hash_token_deterministic(self): """Hashing the same token should always produce the same result."""