diff --git a/fastapi_users_tortoise/__init__.py b/fastapi_users_tortoise/__init__.py index e2e40f2..089ecd2 100644 --- a/fastapi_users_tortoise/__init__.py +++ b/fastapi_users_tortoise/__init__.py @@ -1,15 +1,30 @@ """FastAPI Users database adapter for Tortoise ORM.""" from typing import Any, Dict, Generic, Optional, Type, TypeVar, cast +import re from uuid import UUID from fastapi_users.db.base import BaseUserDatabase from fastapi_users.models import ID, OAP from tortoise import fields, models -from tortoise.exceptions import DoesNotExist +from tortoise.exceptions import DoesNotExist, ValidationError +from tortoise.timezone import now +from tortoise.validators import RegexValidator __version__ = "0.2.0" +class UsernameValidator(RegexValidator): + def __init__(self): + pattern = r"^[\w.@+-]+\Z" + super().__init__(pattern, 0) + + def __call__(self, value: Any): + if not self.regex.match(value): + raise ValidationError( + "Username must only container letters, numbers, and @/./+/-/_ characters." + ) + + class TortoiseBaseUserAccountModel(models.Model): """Base Tortoise ORM users model definition.""" @@ -19,12 +34,36 @@ class User(TortoiseBaseUserAccountModel[uuid.UUID]): so using generics here to specify the id ID is pointless. """ + username_validator = UsernameValidator() + id: Any - email: str = fields.CharField(index=True, unique=True, null=False, max_length=255) + first_name = fields.CharField(null=True, max_length=150) + last_name = fields.CharField(null=True, max_length=150) + username = fields.CharField( + null=True, unique=True, max_length=50, validators=[username_validator] + ) + email: str = fields.CharField( + index=True, + unique=True, + null=False, + max_length=255, + ) hashed_password: str = fields.CharField(null=False, max_length=1024) is_active = fields.BooleanField(default=True, null=False) is_superuser = fields.BooleanField(default=False, null=False) is_verified = fields.BooleanField(default=False, null=False) + date_joined = fields.DatetimeField(default=now) + + def full_name(self): + """ + return first_name and last_name + """ + full_name = f"{self.first_name} {self.last_name}" + return full_name.strip() + + class PydanticMeta: + computed = ["full_name"] + exclude = ["hashed_password"] class Meta: abstract = True diff --git a/tests/test_users.py b/tests/test_users.py index 9438d6b..9e08af4 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -3,6 +3,7 @@ import pytest from tortoise import Tortoise, fields +from tortoise.exceptions import ValidationError from fastapi_users_tortoise import ( TortoiseBaseUserAccountModelUUID, @@ -37,7 +38,9 @@ async def tortoise_user_db() -> AsyncGenerator[TortoiseUserDatabase[User, UUID], @pytest.fixture -async def tortoise_user_db_oauth() -> AsyncGenerator[TortoiseUserDatabase[User, UUID], None]: +async def tortoise_user_db_oauth() -> AsyncGenerator[ + TortoiseUserDatabase[User, UUID], None +]: DATABASE_URL = "sqlite://./test-tortoise-user-oauth.db" await Tortoise.init( @@ -68,10 +71,22 @@ async def test_queries( assert user.is_active is True assert user.is_superuser is False assert user.email == user_create["email"] + assert user.first_name is None + assert user.last_name is None + assert user.username is None + assert user.date_joined is not None # Update updated_user = await tortoise_user_db.update(user, {"is_superuser": True}) assert updated_user.is_superuser is True + + # update first and last name + first_name = "Lancelot" + last_name = "Camelot" + updated_user = await tortoise_user_db.update(user, {"first_name": first_name, "last_name": last_name}) + assert updated_user.first_name == first_name + assert updated_user.last_name == last_name + assert updated_user.full_name() == f"{first_name} {last_name}" # Get by id id_user = await tortoise_user_db.get(user.id) @@ -236,3 +251,20 @@ async def test_queries_oauth( # Unknown OAuth account unknown_oauth_user = await tortoise_user_db_oauth.get_by_oauth_account("foo", "bar") assert unknown_oauth_user is None + + +@pytest.mark.asyncio +async def test_username_validation(tortoise_user_db: TortoiseUserDatabase[User, UUID]): + user_create = { + "username": "lancee007", + "email": "lancelot@camelot.bt", + "hashed_password": "guinevere", + } + + # Create + user = await tortoise_user_db.create(user_create) + assert user.username == user_create["username"] + + # update to invalid + with pytest.raises(ValidationError): + await tortoise_user_db.update(user, {"username": "lance&lot"}) \ No newline at end of file