Files
devx/tests/unit/test_utils_crypto.py
T
emil f44b321f37
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 39s
DEVX-129: test: cover crypto.py line 37 (retry on leading dash)
2026-07-13 01:05:20 +00:00

85 lines
2.6 KiB
Python

"""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 == ""