Files
fast-next-template/backend/tests/schemas/test_validators.py
Felipe Cardoso c589b565f0 Add pyproject.toml for consolidated project configuration and replace Black, isort, and Flake8 with Ruff
- Introduced `pyproject.toml` to centralize backend tool configurations (e.g., Ruff, mypy, coverage, pytest).
- Replaced Black, isort, and Flake8 with Ruff for linting, formatting, and import sorting.
- Updated `requirements.txt` to include Ruff and remove replaced tools.
- Added `Makefile` to streamline development workflows with commands for linting, formatting, type-checking, testing, and cleanup.
2025-11-10 11:55:15 +01:00

220 lines
8.6 KiB
Python

"""
Tests for schema validators (app/schemas/validators.py).
Covers all edge cases in validation functions:
- validate_password_strength
- validate_phone_number (lines 115, 119)
- validate_email_format (line 148)
- validate_slug (lines 170-183)
"""
import pytest
from app.schemas.validators import (
validate_email_format,
validate_password_strength,
validate_phone_number,
validate_slug,
)
class TestPasswordStrengthValidator:
"""Test password strength validation."""
def test_valid_strong_password(self):
"""Test that a strong password passes validation."""
password = "MySecureP@ss123"
result = validate_password_strength(password)
assert result == password
def test_password_too_short(self):
"""Test that password shorter than 12 characters is rejected."""
with pytest.raises(ValueError, match="at least 12 characters long"):
validate_password_strength("Short1!")
def test_common_password_rejected(self):
"""Test that common passwords are rejected."""
# "password1234" is in COMMON_PASSWORDS and is 12 chars
# Common password check happens before character type checks
with pytest.raises(ValueError, match="too common"):
validate_password_strength("password1234")
def test_password_missing_lowercase(self):
"""Test that password without lowercase is rejected."""
with pytest.raises(ValueError, match="at least one lowercase letter"):
validate_password_strength("ALLUPPERCASE123!")
def test_password_missing_uppercase(self):
"""Test that password without uppercase is rejected."""
with pytest.raises(ValueError, match="at least one uppercase letter"):
validate_password_strength("alllowercase123!")
def test_password_missing_digit(self):
"""Test that password without digit is rejected."""
with pytest.raises(ValueError, match="at least one digit"):
validate_password_strength("NoDigitsHere!")
def test_password_missing_special_char(self):
"""Test that password without special character is rejected."""
with pytest.raises(ValueError, match="at least one special character"):
validate_password_strength("NoSpecialChar123")
class TestPhoneNumberValidator:
"""Test phone number validation."""
def test_valid_international_format(self):
"""Test valid international phone number."""
result = validate_phone_number("+12345678901")
assert result == "+12345678901"
def test_valid_local_format(self):
"""Test valid local phone number."""
result = validate_phone_number("0123456789")
assert result == "0123456789"
def test_valid_with_formatting(self):
"""Test phone number with formatting characters."""
result = validate_phone_number("+1 (555) 123-4567")
assert result == "+15551234567"
def test_none_returns_none(self):
"""Test that None input returns None."""
result = validate_phone_number(None)
assert result is None
def test_empty_string_rejected(self):
"""Test that empty string is rejected."""
with pytest.raises(ValueError, match="cannot be empty"):
validate_phone_number("")
def test_whitespace_only_rejected(self):
"""Test that whitespace-only string is rejected."""
with pytest.raises(ValueError, match="cannot be empty"):
validate_phone_number(" ")
def test_invalid_prefix_rejected(self):
"""Test that invalid prefix is rejected."""
with pytest.raises(ValueError, match="must start with \\+ or 0"):
validate_phone_number("12345678901")
def test_too_short_rejected(self):
"""Test that too-short phone number is rejected."""
with pytest.raises(ValueError, match="must start with \\+ or 0"):
validate_phone_number("+1234567") # Only 7 digits after +
def test_too_long_rejected(self):
"""Test that too-long phone number is rejected."""
with pytest.raises(ValueError, match="must start with \\+ or 0"):
validate_phone_number("+123456789012345") # 15 digits after +
def test_multiple_plus_symbols_rejected(self):
r"""Test phone number with multiple + symbols.
Note: Line 115 is defensive code - the regex check at line 110 catches this first.
The regex ^(?:\+[0-9]{8,14}|0[0-9]{8,14})$ only allows + at the start.
"""
with pytest.raises(
ValueError, match="must start with \\+ or 0 followed by 8-14 digits"
):
validate_phone_number("+1234+5678901")
def test_non_digit_after_prefix_rejected(self):
"""Test phone number with non-digit characters after prefix.
Note: Line 119 is defensive code - the regex check at line 110 catches this first.
The regex requires all digits after the prefix.
"""
with pytest.raises(ValueError, match="must start with \\+ or 0"):
validate_phone_number("+123abc45678")
class TestEmailFormatValidator:
"""Test email format validation."""
def test_valid_email_lowercase(self):
"""Test valid lowercase email."""
result = validate_email_format("test@example.com")
assert result == "test@example.com"
def test_email_normalized_to_lowercase(self):
"""Test email is normalized to lowercase (covers line 148)."""
result = validate_email_format("Test@Example.COM")
assert result == "test@example.com"
def test_email_with_uppercase_domain(self):
"""Test email with uppercase domain is normalized."""
result = validate_email_format("user@EXAMPLE.COM")
assert result == "user@example.com"
class TestSlugValidator:
"""Test slug validation."""
def test_valid_slug_lowercase_letters(self):
"""Test valid slug with lowercase letters."""
result = validate_slug("test-slug")
assert result == "test-slug"
def test_valid_slug_with_numbers(self):
"""Test valid slug with numbers."""
result = validate_slug("test-123")
assert result == "test-123"
def test_valid_slug_minimal_length(self):
"""Test valid slug with minimal length (2 characters)."""
result = validate_slug("ab")
assert result == "ab"
def test_empty_slug_rejected(self):
"""Test empty slug is rejected (covers line 170)."""
with pytest.raises(ValueError, match="at least 2 characters long"):
validate_slug("")
def test_single_character_slug_rejected(self):
"""Test single character slug is rejected (covers line 170)."""
with pytest.raises(ValueError, match="at least 2 characters long"):
validate_slug("a")
def test_slug_too_long_rejected(self):
"""Test slug longer than 50 characters is rejected (covers line 173)."""
long_slug = "a" * 51
with pytest.raises(ValueError, match="at most 50 characters long"):
validate_slug(long_slug)
def test_slug_max_length_accepted(self):
"""Test slug with exactly 50 characters is accepted."""
max_slug = "a" * 50
result = validate_slug(max_slug)
assert result == max_slug
def test_slug_starts_with_hyphen_rejected(self):
"""Test slug starting with hyphen is rejected (covers line 177)."""
with pytest.raises(ValueError, match="cannot start or end with a hyphen"):
validate_slug("-test")
def test_slug_ends_with_hyphen_rejected(self):
"""Test slug ending with hyphen is rejected (covers line 177)."""
with pytest.raises(ValueError, match="cannot start or end with a hyphen"):
validate_slug("test-")
def test_slug_consecutive_hyphens_rejected(self):
"""Test slug with consecutive hyphens is rejected (covers line 177)."""
with pytest.raises(ValueError, match="cannot contain consecutive hyphens"):
validate_slug("test--slug")
def test_slug_uppercase_letters_rejected(self):
"""Test slug with uppercase letters is rejected (covers line 177)."""
with pytest.raises(ValueError, match="only contain lowercase letters"):
validate_slug("Test-Slug")
def test_slug_special_characters_rejected(self):
"""Test slug with special characters is rejected (covers line 177)."""
with pytest.raises(ValueError, match="only contain lowercase letters"):
validate_slug("test_slug")
def test_slug_spaces_rejected(self):
"""Test slug with spaces is rejected (covers line 177)."""
with pytest.raises(ValueError, match="only contain lowercase letters"):
validate_slug("test slug")