"""Unit tests for devx.utils.crypto.""" from __future__ import annotations import re from unittest.mock import patch from devx.utils.crypto import ( _DIGITS, _LOWER, _SYMBOLS, _UPPER, generate_hex_secret, generate_password, generate_secret, ) class TestGenerateSecret: def test_returns_url_safe_string(self) -> None: secret = generate_secret() assert isinstance(secret, str) assert len(secret) > 0 # URL-safe base64 characters only assert re.match(r"^[A-Za-z0-9_-]+$", secret) def test_never_starts_with_dash(self) -> None: for _ in range(50): secret = generate_secret() assert not secret.startswith("-") def test_url_safe_no_plus_slash(self) -> None: # token_urlsafe uses base64url which has no + or / for _ in range(50): secret = generate_secret() assert "+" not in secret assert "/" not in secret def test_retries_on_leading_dash(self) -> None: """When token_urlsafe returns a value starting with '-', it retries.""" # First call returns a dash-prefixed value, second returns a clean one with patch("devx.utils.crypto.secrets.token_urlsafe", side_effect=["-bad-value", "good-value"]): secret = generate_secret() assert secret == "good-value" class TestGeneratePassword: def test_default_length(self) -> None: pw = generate_password() assert len(pw) == 32 def test_custom_length(self) -> None: pw = generate_password(length=64) assert len(pw) == 64 def test_contains_all_char_classes(self) -> None: pw = generate_password(length=32) assert any(c in _UPPER for c in pw), "Missing uppercase" assert any(c in _LOWER for c in pw), "Missing lowercase" assert any(c in _DIGITS for c in pw), "Missing digits" assert any(c in _SYMBOLS for c in pw), "Missing symbols" def test_first_char_alphanumeric(self) -> None: for _ in range(50): pw = generate_password() assert pw[0] not in _SYMBOLS, f"First char '{pw[0]}' is a symbol" def test_minimum_length_4(self) -> None: pw = generate_password(length=4) assert len(pw) == 4 class TestGenerateHexSecret: def test_returns_hex_string(self) -> None: secret = generate_hex_secret(length=32) assert re.match(r"^[0-9a-f]+$", secret) def test_correct_length(self) -> None: secret = generate_hex_secret(length=20) assert len(secret) == 20 def test_empty_for_zero(self) -> None: secret = generate_hex_secret(length=0) assert secret == ""