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.
This commit is contained in:
@@ -5,6 +5,7 @@ Covers Pydantic validators for:
|
||||
- Slug validation (lines 26, 28, 30, 32, 62-70)
|
||||
- Name validation (lines 40, 77)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -20,19 +21,13 @@ class TestOrganizationBaseValidators:
|
||||
|
||||
def test_valid_organization_base(self):
|
||||
"""Test that valid data passes validation."""
|
||||
org = OrganizationBase(
|
||||
name="Test Organization",
|
||||
slug="test-org"
|
||||
)
|
||||
org = OrganizationBase(name="Test Organization", slug="test-org")
|
||||
assert org.name == "Test Organization"
|
||||
assert org.slug == "test-org"
|
||||
|
||||
def test_slug_none_returns_none(self):
|
||||
"""Test that None slug is allowed (covers line 26)."""
|
||||
org = OrganizationBase(
|
||||
name="Test Organization",
|
||||
slug=None
|
||||
)
|
||||
org = OrganizationBase(name="Test Organization", slug=None)
|
||||
assert org.slug is None
|
||||
|
||||
def test_slug_invalid_characters_rejected(self):
|
||||
@@ -40,57 +35,46 @@ class TestOrganizationBaseValidators:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationBase(
|
||||
name="Test Organization",
|
||||
slug="Test_Org!" # Uppercase and special chars
|
||||
slug="Test_Org!", # Uppercase and special chars
|
||||
)
|
||||
errors = exc_info.value.errors()
|
||||
assert any("lowercase letters, numbers, and hyphens" in str(e['msg']) for e in errors)
|
||||
assert any(
|
||||
"lowercase letters, numbers, and hyphens" in str(e["msg"]) for e in errors
|
||||
)
|
||||
|
||||
def test_slug_starts_with_hyphen_rejected(self):
|
||||
"""Test slug starting with hyphen is rejected (covers line 30)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationBase(
|
||||
name="Test Organization",
|
||||
slug="-test-org"
|
||||
)
|
||||
OrganizationBase(name="Test Organization", slug="-test-org")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("cannot start or end with a hyphen" in str(e['msg']) for e in errors)
|
||||
assert any("cannot start or end with a hyphen" in str(e["msg"]) for e in errors)
|
||||
|
||||
def test_slug_ends_with_hyphen_rejected(self):
|
||||
"""Test slug ending with hyphen is rejected (covers line 30)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationBase(
|
||||
name="Test Organization",
|
||||
slug="test-org-"
|
||||
)
|
||||
OrganizationBase(name="Test Organization", slug="test-org-")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("cannot start or end with a hyphen" in str(e['msg']) for e in errors)
|
||||
assert any("cannot start or end with a hyphen" in str(e["msg"]) for e in errors)
|
||||
|
||||
def test_slug_consecutive_hyphens_rejected(self):
|
||||
"""Test slug with consecutive hyphens is rejected (covers line 32)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationBase(
|
||||
name="Test Organization",
|
||||
slug="test--org"
|
||||
)
|
||||
OrganizationBase(name="Test Organization", slug="test--org")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("cannot contain consecutive hyphens" in str(e['msg']) for e in errors)
|
||||
assert any(
|
||||
"cannot contain consecutive hyphens" in str(e["msg"]) for e in errors
|
||||
)
|
||||
|
||||
def test_name_whitespace_only_rejected(self):
|
||||
"""Test whitespace-only name is rejected (covers line 40)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationBase(
|
||||
name=" ",
|
||||
slug="test-org"
|
||||
)
|
||||
OrganizationBase(name=" ", slug="test-org")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("name cannot be empty" in str(e['msg']) for e in errors)
|
||||
assert any("name cannot be empty" in str(e["msg"]) for e in errors)
|
||||
|
||||
def test_name_trimmed(self):
|
||||
"""Test that name is trimmed."""
|
||||
org = OrganizationBase(
|
||||
name=" Test Organization ",
|
||||
slug="test-org"
|
||||
)
|
||||
org = OrganizationBase(name=" Test Organization ", slug="test-org")
|
||||
assert org.name == "Test Organization"
|
||||
|
||||
|
||||
@@ -99,22 +83,18 @@ class TestOrganizationCreateValidators:
|
||||
|
||||
def test_valid_organization_create(self):
|
||||
"""Test that valid data passes validation."""
|
||||
org = OrganizationCreate(
|
||||
name="Test Organization",
|
||||
slug="test-org"
|
||||
)
|
||||
org = OrganizationCreate(name="Test Organization", slug="test-org")
|
||||
assert org.name == "Test Organization"
|
||||
assert org.slug == "test-org"
|
||||
|
||||
def test_slug_validation_inherited(self):
|
||||
"""Test that slug validation is inherited from base."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationCreate(
|
||||
name="Test",
|
||||
slug="Invalid_Slug!"
|
||||
)
|
||||
OrganizationCreate(name="Test", slug="Invalid_Slug!")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("lowercase letters, numbers, and hyphens" in str(e['msg']) for e in errors)
|
||||
assert any(
|
||||
"lowercase letters, numbers, and hyphens" in str(e["msg"]) for e in errors
|
||||
)
|
||||
|
||||
|
||||
class TestOrganizationUpdateValidators:
|
||||
@@ -122,10 +102,7 @@ class TestOrganizationUpdateValidators:
|
||||
|
||||
def test_valid_organization_update(self):
|
||||
"""Test that valid update data passes validation."""
|
||||
org = OrganizationUpdate(
|
||||
name="Updated Name",
|
||||
slug="updated-slug"
|
||||
)
|
||||
org = OrganizationUpdate(name="Updated Name", slug="updated-slug")
|
||||
assert org.name == "Updated Name"
|
||||
assert org.slug == "updated-slug"
|
||||
|
||||
@@ -139,35 +116,39 @@ class TestOrganizationUpdateValidators:
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationUpdate(slug="Test_Org!")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("lowercase letters, numbers, and hyphens" in str(e['msg']) for e in errors)
|
||||
assert any(
|
||||
"lowercase letters, numbers, and hyphens" in str(e["msg"]) for e in errors
|
||||
)
|
||||
|
||||
def test_update_slug_starts_with_hyphen_rejected(self):
|
||||
"""Test update slug starting with hyphen is rejected (covers line 66)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationUpdate(slug="-test-org")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("cannot start or end with a hyphen" in str(e['msg']) for e in errors)
|
||||
assert any("cannot start or end with a hyphen" in str(e["msg"]) for e in errors)
|
||||
|
||||
def test_update_slug_ends_with_hyphen_rejected(self):
|
||||
"""Test update slug ending with hyphen is rejected (covers line 66)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationUpdate(slug="test-org-")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("cannot start or end with a hyphen" in str(e['msg']) for e in errors)
|
||||
assert any("cannot start or end with a hyphen" in str(e["msg"]) for e in errors)
|
||||
|
||||
def test_update_slug_consecutive_hyphens_rejected(self):
|
||||
"""Test update slug with consecutive hyphens is rejected (covers line 68)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationUpdate(slug="test--org")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("cannot contain consecutive hyphens" in str(e['msg']) for e in errors)
|
||||
assert any(
|
||||
"cannot contain consecutive hyphens" in str(e["msg"]) for e in errors
|
||||
)
|
||||
|
||||
def test_update_name_whitespace_only_rejected(self):
|
||||
"""Test whitespace-only name in update is rejected (covers line 77)."""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
OrganizationUpdate(name=" ")
|
||||
errors = exc_info.value.errors()
|
||||
assert any("name cannot be empty" in str(e['msg']) for e in errors)
|
||||
assert any("name cannot be empty" in str(e["msg"]) for e in errors)
|
||||
|
||||
def test_update_name_none_allowed(self):
|
||||
"""Test that None name is allowed in update."""
|
||||
|
||||
@@ -1,80 +1,177 @@
|
||||
# tests/schemas/test_user_schemas.py
|
||||
import pytest
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.schemas.users import UserBase, UserCreate
|
||||
|
||||
|
||||
class TestPhoneNumberValidation:
|
||||
"""Tests for phone number validation in user schemas"""
|
||||
|
||||
def test_valid_swiss_numbers(self):
|
||||
"""Test valid Swiss phone numbers are accepted"""
|
||||
# International format
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+41791234567")
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+41791234567",
|
||||
)
|
||||
assert user.phone_number == "+41791234567"
|
||||
|
||||
# Local format
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="0791234567")
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="0791234567",
|
||||
)
|
||||
assert user.phone_number == "0791234567"
|
||||
|
||||
# With formatting characters
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+41 79 123 45 67")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "+41791234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+41 79 123 45 67",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "+41791234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="079 123 45 67")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "0791234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="079 123 45 67",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "0791234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+41-79-123-45-67")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "+41791234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+41-79-123-45-67",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "+41791234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="079-123-45-67")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "0791234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="079-123-45-67",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "0791234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+41 (79) 123 45 67")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "+41791234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+41 (79) 123 45 67",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "+41791234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="079 (123) 45 67")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "0791234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="079 (123) 45 67",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "0791234567"
|
||||
|
||||
def test_valid_italian_numbers(self):
|
||||
"""Test valid Italian phone numbers are accepted"""
|
||||
# International format
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+393451234567")
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+393451234567",
|
||||
)
|
||||
assert user.phone_number == "+393451234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+39345123456")
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+39345123456",
|
||||
)
|
||||
assert user.phone_number == "+39345123456"
|
||||
|
||||
# Local format
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="03451234567")
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="03451234567",
|
||||
)
|
||||
assert user.phone_number == "03451234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="0345123456789")
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="0345123456789",
|
||||
)
|
||||
assert user.phone_number == "0345123456789"
|
||||
|
||||
# With formatting characters
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+39 345 123 4567")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "+393451234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+39 345 123 4567",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "+393451234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="0345 123 4567")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "03451234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="0345 123 4567",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "03451234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+39-345-123-4567")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "+393451234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+39-345-123-4567",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "+393451234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="0345-123-4567")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "03451234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="0345-123-4567",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "03451234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="+39 (345) 123 4567")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "+393451234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+39 (345) 123 4567",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "+393451234567"
|
||||
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number="0345 (123) 4567")
|
||||
assert re.sub(r'[\s\-\(\)]', '', user.phone_number) == "03451234567"
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="0345 (123) 4567",
|
||||
)
|
||||
assert re.sub(r"[\s\-\(\)]", "", user.phone_number) == "03451234567"
|
||||
|
||||
def test_none_phone_number(self):
|
||||
"""Test that None is accepted as a valid value (optional phone number)"""
|
||||
user = UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number=None)
|
||||
user = UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number=None,
|
||||
)
|
||||
assert user.phone_number is None
|
||||
|
||||
def test_invalid_phone_numbers(self):
|
||||
@@ -83,17 +180,14 @@ class TestPhoneNumberValidation:
|
||||
# Too short
|
||||
"+12",
|
||||
"012",
|
||||
|
||||
# Invalid characters
|
||||
"+41xyz123456",
|
||||
"079abc4567",
|
||||
"123-abc-7890",
|
||||
"+1(800)CALL-NOW",
|
||||
|
||||
# Completely invalid formats
|
||||
"++4412345678", # Double plus
|
||||
# Note: "()+41123456" becomes "+41123456" after cleaning, which is valid
|
||||
|
||||
# Empty string
|
||||
"",
|
||||
# Spaces only
|
||||
@@ -102,7 +196,12 @@ class TestPhoneNumberValidation:
|
||||
|
||||
for number in invalid_numbers:
|
||||
with pytest.raises(ValidationError):
|
||||
UserBase(email="test@example.com", first_name="Test", last_name="User", phone_number=number)
|
||||
UserBase(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number=number,
|
||||
)
|
||||
|
||||
def test_phone_validation_in_user_create(self):
|
||||
"""Test that phone validation also works in UserCreate schema"""
|
||||
@@ -112,7 +211,7 @@ class TestPhoneNumberValidation:
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
password="Password123!",
|
||||
phone_number="+41791234567"
|
||||
phone_number="+41791234567",
|
||||
)
|
||||
assert user.phone_number == "+41791234567"
|
||||
|
||||
@@ -123,5 +222,5 @@ class TestPhoneNumberValidation:
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
password="Password123!",
|
||||
phone_number="invalid-number"
|
||||
)
|
||||
phone_number="invalid-number",
|
||||
)
|
||||
|
||||
@@ -7,12 +7,13 @@ Covers all edge cases in validation functions:
|
||||
- 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_email_format,
|
||||
validate_slug,
|
||||
)
|
||||
|
||||
@@ -108,12 +109,14 @@ class TestPhoneNumberValidator:
|
||||
validate_phone_number("+123456789012345") # 15 digits after +
|
||||
|
||||
def test_multiple_plus_symbols_rejected(self):
|
||||
"""Test phone number with multiple + symbols.
|
||||
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"):
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user