Inject every models variations and DB model in DB adapters (#84)

* Inject every model variations in router and DB model in DB adapters

* Update documentation and import Tortoise in db module

* Use path operation decorator dependencies for superuser routes
This commit is contained in:
François Voron
2020-01-04 15:36:34 +01:00
committed by GitHub
parent c903b30161
commit 104a6c6bf5
29 changed files with 501 additions and 269 deletions

View File

@@ -31,6 +31,7 @@ Add quickly a registration and authentication system to your [FastAPI](https://f
* [X] Customizable database backend
* [X] SQLAlchemy async backend included thanks to [encode/databases](https://www.encode.io/databases/)
* [X] MongoDB async backend included thanks to [mongodb/motor](https://github.com/mongodb/motor)
* [X] [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/) backend included
* [X] Multiple customizable authentication backends
* [X] JWT authentication backend included
* [X] Cookie authentication backend included

View File

@@ -6,7 +6,7 @@
Let's create a MongoDB connection and instantiate a collection.
```py hl_lines="5 6 7 8"
```py hl_lines="23 24 25 26"
{!./src/db_mongodb.py!}
```
@@ -16,10 +16,12 @@ You can choose any name for the database and the collection.
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. Create it like this.
```py hl_lines="14"
```py hl_lines="32"
{!./src/db_mongodb.py!}
```
Notice that we pass a reference to your [`UserDB` model](../model.md).
!!! info
The database adapter will automatically create a [unique index](https://docs.mongodb.com/manual/core/index-unique/) on `id` and `email`.

View File

@@ -24,7 +24,7 @@ For the sake of this tutorial from now on, we'll use a simple SQLite databse.
Let's create a `metadata` object and declare our User table.
```py hl_lines="4 14 15"
```py hl_lines="5 32 33"
{!./src/db_sqlalchemy.py!}
```
@@ -34,7 +34,7 @@ As you can see, **FastAPI Users** provides a mixin that will include base fields
We'll now create an SQLAlchemy enigne and ask it to create all the defined tables.
```py hl_lines="18 19 20 21 22"
```py hl_lines="36 37 38 39 40"
{!./src/db_sqlalchemy.py!}
```
@@ -45,11 +45,15 @@ We'll now create an SQLAlchemy enigne and ask it to create all the defined table
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. Create it like this.
```py hl_lines="24 25"
```py hl_lines="42 43"
{!./src/db_sqlalchemy.py!}
```
Notice that we declare the `users` variable, which is the actual SQLAlchemy table behind the table class. We also use our `database` instance, which allows us to do asynchronous request to the database.
Notice that we pass it three things:
* A reference to your [`UserDB` model](../model.md).
* The `users` variable, which is the actual SQLAlchemy table behind the table class.
* A `database` instance, which allows us to do asynchronous request to the database.
## Next steps

View File

@@ -22,29 +22,31 @@ For the sake of this tutorial from now on, we'll use a simple SQLite databse.
## Setup User table
Let's declare our User model.
Let's declare our User ORM model.
```py hl_lines="9 10"
```py hl_lines="26 27"
{!./src/db_tortoise.py!}
```
As you can see, **FastAPI Users** provides a mixin that will include base fields for our User table. You can of course add you own fields there to fit to your needs!
As you can see, **FastAPI Users** provides an abstract model that will include base fields for our User table. You can of course add you own fields there to fit to your needs!
## Create the database adapter
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. Create it like this.
```py hl_lines="13"
```py hl_lines="30"
{!./src/db_tortoise.py!}
```
Notice that we pass a reference to your [`UserDB` model](../model.md).
## Register Tortoise
For using Tortoise ORM we must register our models and database.
Tortoise ORM supports integration with Starlette/FastAPI out-of-the-box. It will automatically bind startup and shutdown events.
```py hl_lines="16"
```py hl_lines="33"
{!./src/db_tortoise.py!}
```

View File

@@ -7,15 +7,34 @@
* `is_active` (`bool`) Whether or not the user is active. If not, login and forgot password requests will be denied. Default to `True`.
* `is_active` (`bool`) Whether or not the user is a superuser. Useful to implement administration logic. Default to `False`.
## Use the model
## Define your models
The model is exposed as a Pydantic model mixin.
There are four Pydantic models variations provided as mixins:
* `BaseUser`, which provides the basic fields and validation ;
* `BaseCreateUser`, dedicated to user registration, which makes the `email` compulsory and adds a compulsory `password` field ;
* `BaseUpdateUser`, dedicated to user profile update, which adds an optional `password` field ;
* `BaseUserDB`, which is a representation of the user in database, adding a `hashed_password` field.
You should define each of those variations, inheriting from each mixin:
```py
from fastapi_users import BaseUser
from fastapi_users import models
class User(BaseUser):
class User(models.BaseUser):
pass
class UserCreate(User, models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
```

View File

@@ -1,6 +1,6 @@
# Router
We're almost there! The last step is to configure the `FastAPIUsers` object that will wire the database adapter, the authentication class and the user model to expose the FastAPI router.
We're almost there! The last step is to configure the `FastAPIUsers` object that will wire the database adapter, the authentication class and the user models to expose the FastAPI router.
## Configure `FastAPIUsers`
@@ -9,6 +9,9 @@ Configure `FastAPIUsers` object with all the elements we defined before. More pr
* `db`: Database adapter instance.
* `auth_backends`: List of authentication backends. See [Authentication](./authentication/index.md).
* `user_model`: Pydantic model of a user.
* `user_create_model`: Pydantic model for creating a user.
* `user_update_model`: Pydantic model for updating a user.
* `user_db_model`: Pydantic model of a DB representation of a user.
* `reset_password_token_secret`: Secret to encode reset password token.
* `reset_password_token_lifetime_seconds`: Lifetime of reset password token in seconds. Default to one hour.
@@ -19,6 +22,9 @@ fastapi_users = FastAPIUsers(
user_db,
auth_backends,
User,
UserCreate,
UserUpdate,
UserDB,
SECRET,
)
```

View File

@@ -1,7 +1,25 @@
import motor.motor_asyncio
from fastapi import FastAPI
from fastapi_users import models
from fastapi_users.db import MongoDBUserDatabase
class User(models.BaseUser):
pass
class UserCreate(User, models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
DATABASE_URL = "mongodb://localhost:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(DATABASE_URL)
db = client["database_name"]
@@ -11,4 +29,4 @@ collection = db["users"]
app = FastAPI()
user_db = MongoDBUserDatabase(collection)
user_db = MongoDBUserDatabase(UserDB, collection)

View File

@@ -1,9 +1,27 @@
import databases
import sqlalchemy
from fastapi import FastAPI
from fastapi_users import models
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
class User(models.BaseUser):
pass
class UserCreate(User, models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
DATABASE_URL = "sqlite:///./test.db"
database = databases.Database(DATABASE_URL)
@@ -22,7 +40,7 @@ engine = sqlalchemy.create_engine(
Base.metadata.create_all(engine)
users = UserTable.__table__
user_db = SQLAlchemyUserDatabase(database, users)
user_db = SQLAlchemyUserDatabase(UserDB, database, users)
app = FastAPI()

View File

@@ -1,16 +1,33 @@
from fastapi import FastAPI
from fastapi_users.db.tortoise import BaseUserModel, TortoiseUserDatabase
from tortoise import Model
from fastapi_users import models
from fastapi_users.db import TortoiseBaseUserModel, TortoiseUserDatabase
from tortoise.contrib.starlette import register_tortoise
class User(models.BaseUser):
pass
class UserCreate(User, models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
DATABASE_URL = "sqlite://./test.db"
class UserModel(BaseUserModel, Model):
class UserModel(TortoiseBaseUserModel):
pass
user_db = TortoiseUserDatabase(UserModel)
user_db = TortoiseUserDatabase(UserDB, UserModel)
app = FastAPI()
register_tortoise(app, modules={"models": ["path_to_your_package"]})

View File

@@ -1,6 +1,6 @@
import motor.motor_asyncio
from fastapi import FastAPI
from fastapi_users import BaseUser, FastAPIUsers
from fastapi_users import FastAPIUsers, models
from fastapi_users.authentication import JWTAuthentication
from fastapi_users.db import MongoDBUserDatabase
@@ -8,24 +8,35 @@ DATABASE_URL = "mongodb://localhost:27017"
SECRET = "SECRET"
class User(models.BaseUser):
pass
class UserCreate(User, models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
client = motor.motor_asyncio.AsyncIOMotorClient(DATABASE_URL)
db = client["database_name"]
collection = db["users"]
user_db = MongoDBUserDatabase(collection)
class User(BaseUser):
pass
user_db = MongoDBUserDatabase(UserDB, collection)
auth_backends = [
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
]
app = FastAPI()
fastapi_users = FastAPIUsers(user_db, auth_backends, User, SECRET)
fastapi_users = FastAPIUsers(
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
)
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])

View File

@@ -1,7 +1,7 @@
import databases
import sqlalchemy
from fastapi import FastAPI
from fastapi_users import BaseUser, FastAPIUsers
from fastapi_users import FastAPIUsers, models
from fastapi_users.authentication import JWTAuthentication
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
@@ -10,8 +10,23 @@ DATABASE_URL = "sqlite:///./test.db"
SECRET = "SECRET"
database = databases.Database(DATABASE_URL)
class User(models.BaseUser):
pass
class UserCreate(User, models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
database = databases.Database(DATABASE_URL)
Base: DeclarativeMeta = declarative_base()
@@ -22,15 +37,10 @@ class UserTable(Base, SQLAlchemyBaseUserTable):
engine = sqlalchemy.create_engine(
DATABASE_URL, connect_args={"check_same_thread": False}
)
Base.metadata.create_all(engine)
users = UserTable.__table__
user_db = SQLAlchemyUserDatabase(database, users)
class User(BaseUser):
pass
user_db = SQLAlchemyUserDatabase(UserDB, database, users)
auth_backends = [
@@ -38,7 +48,9 @@ auth_backends = [
]
app = FastAPI()
fastapi_users = FastAPIUsers(user_db, auth_backends, User, SECRET)
fastapi_users = FastAPIUsers(
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
)
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])

View File

@@ -1,28 +1,44 @@
from fastapi import FastAPI
from fastapi_users import BaseUser, FastAPIUsers
from fastapi_users import FastAPIUsers, models
from fastapi_users.authentication import JWTAuthentication
from fastapi_users.db.tortoise import BaseUserModel, TortoiseUserDatabase
from tortoise import Model
from fastapi_users.db import TortoiseBaseUserModel, TortoiseUserDatabase
from tortoise.contrib.starlette import register_tortoise
DATABASE_URL = "sqlite://./test.db"
SECRET = "SECRET"
class UserModel(BaseUserModel, Model):
class User(models.BaseUser):
pass
class User(BaseUser):
class UserCreate(User, models.BaseUserCreate):
pass
auth = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
user_db = TortoiseUserDatabase(UserModel)
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
class UserModel(TortoiseBaseUserModel):
pass
user_db = TortoiseUserDatabase(UserDB, UserModel)
app = FastAPI()
register_tortoise(app, db_url=DATABASE_URL, modules={"models": ["test"]})
fastapi_users = FastAPIUsers(user_db, auth, User, SECRET)
auth_backends = [
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
]
fastapi_users = FastAPIUsers(
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
)
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])

View File

@@ -2,5 +2,5 @@
__version__ = "0.4.1"
from fastapi_users import models # noqa: F401
from fastapi_users.fastapi_users import FastAPIUsers # noqa: F401
from fastapi_users.models import BaseUser # noqa: F401

View File

@@ -12,3 +12,11 @@ try:
)
except ImportError: # pragma: no cover
pass
try:
from fastapi_users.db.tortoise import ( # noqa: F401
TortoiseBaseUserModel,
TortoiseUserDatabase,
)
except ImportError: # pragma: no cover
pass

View File

@@ -1,41 +1,50 @@
from typing import List, Optional
from typing import Generic, List, Optional, Type
from fastapi.security import OAuth2PasswordRequestForm
from fastapi_users import password
from fastapi_users.models import BaseUserDB
from fastapi_users.models import UD
class BaseUserDatabase:
"""Base adapter for retrieving, creating and updating users from a database."""
class BaseUserDatabase(Generic[UD]):
"""
Base adapter for retrieving, creating and updating users from a database.
async def list(self) -> List[BaseUserDB]:
:param user_db_model: Pydantic model of a DB representation of a user.
"""
user_db_model: Type[UD]
def __init__(self, user_db_model: Type[UD]):
self.user_db_model = user_db_model
async def list(self) -> List[UD]:
"""List all users."""
raise NotImplementedError()
async def get(self, id: str) -> Optional[BaseUserDB]:
async def get(self, id: str) -> Optional[UD]:
"""Get a single user by id."""
raise NotImplementedError()
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
async def get_by_email(self, email: str) -> Optional[UD]:
"""Get a single user by email."""
raise NotImplementedError()
async def create(self, user: BaseUserDB) -> BaseUserDB:
async def create(self, user: UD) -> UD:
"""Create a user."""
raise NotImplementedError()
async def update(self, user: BaseUserDB) -> BaseUserDB:
async def update(self, user: UD) -> UD:
"""Update a user."""
raise NotImplementedError()
async def delete(self, user: BaseUserDB) -> None:
async def delete(self, user: UD) -> None:
"""Delete a user."""
raise NotImplementedError()
async def authenticate(
self, credentials: OAuth2PasswordRequestForm
) -> Optional[BaseUserDB]:
) -> Optional[UD]:
"""
Authenticate and return a user following an email and a password.

View File

@@ -1,43 +1,45 @@
from typing import List, Optional
from typing import List, Optional, Type
from motor.motor_asyncio import AsyncIOMotorCollection
from fastapi_users.db.base import BaseUserDatabase
from fastapi_users.models import BaseUserDB
from fastapi_users.models import UD
class MongoDBUserDatabase(BaseUserDatabase):
class MongoDBUserDatabase(BaseUserDatabase[UD]):
"""
Database adapter for MongoDB.
:param user_db_model: Pydantic model of a DB representation of a user.
:param collection: Collection instance from `motor`.
"""
collection: AsyncIOMotorCollection
def __init__(self, collection: AsyncIOMotorCollection):
def __init__(self, user_db_model: Type[UD], collection: AsyncIOMotorCollection):
super().__init__(user_db_model)
self.collection = collection
self.collection.create_index("id", unique=True)
self.collection.create_index("email", unique=True)
async def list(self) -> List[BaseUserDB]:
return [BaseUserDB(**user) async for user in self.collection.find()]
async def list(self) -> List[UD]:
return [self.user_db_model(**user) async for user in self.collection.find()]
async def get(self, id: str) -> Optional[BaseUserDB]:
async def get(self, id: str) -> Optional[UD]:
user = await self.collection.find_one({"id": id})
return BaseUserDB(**user) if user else None
return self.user_db_model(**user) if user else None
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
async def get_by_email(self, email: str) -> Optional[UD]:
user = await self.collection.find_one({"email": email})
return BaseUserDB(**user) if user else None
return self.user_db_model(**user) if user else None
async def create(self, user: BaseUserDB) -> BaseUserDB:
async def create(self, user: UD) -> UD:
await self.collection.insert_one(user.dict())
return user
async def update(self, user: BaseUserDB) -> BaseUserDB:
async def update(self, user: UD) -> UD:
await self.collection.replace_one({"id": user.id}, user.dict())
return user
async def delete(self, user: BaseUserDB) -> None:
async def delete(self, user: UD) -> None:
await self.collection.delete_one({"id": user.id})

View File

@@ -1,10 +1,10 @@
from typing import List, Optional
from typing import List, Optional, Type
from databases import Database
from sqlalchemy import Boolean, Column, String, Table
from fastapi_users.db.base import BaseUserDatabase
from fastapi_users.models import BaseUserDB
from fastapi_users.models import UD
class SQLAlchemyBaseUserTable:
@@ -19,10 +19,11 @@ class SQLAlchemyBaseUserTable:
is_superuser = Column(Boolean, default=False, nullable=False)
class SQLAlchemyUserDatabase(BaseUserDatabase):
class SQLAlchemyUserDatabase(BaseUserDatabase[UD]):
"""
Database adapter for SQLAlchemy.
:param user_db_model: Pydantic model of a DB representation of a user.
:param database: `Database` instance from `encode/databases`.
:param users: SQLAlchemy users table instance.
"""
@@ -30,37 +31,38 @@ class SQLAlchemyUserDatabase(BaseUserDatabase):
database: Database
users: Table
def __init__(self, database: Database, users: Table):
def __init__(self, user_db_model: Type[UD], database: Database, users: Table):
super().__init__(user_db_model)
self.database = database
self.users = users
async def list(self) -> List[BaseUserDB]:
async def list(self) -> List[UD]:
query = self.users.select()
users = await self.database.fetch_all(query)
return [BaseUserDB(**user) for user in users]
return [self.user_db_model(**user) for user in users]
async def get(self, id: str) -> Optional[BaseUserDB]:
async def get(self, id: str) -> Optional[UD]:
query = self.users.select().where(self.users.c.id == id)
user = await self.database.fetch_one(query)
return BaseUserDB(**user) if user else None
return self.user_db_model(**user) if user else None
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
async def get_by_email(self, email: str) -> Optional[UD]:
query = self.users.select().where(self.users.c.email == email)
user = await self.database.fetch_one(query)
return BaseUserDB(**user) if user else None
return self.user_db_model(**user) if user else None
async def create(self, user: BaseUserDB) -> BaseUserDB:
async def create(self, user: UD) -> UD:
query = self.users.insert().values(**user.dict())
await self.database.execute(query)
return user
async def update(self, user: BaseUserDB) -> BaseUserDB:
async def update(self, user: UD) -> UD:
query = (
self.users.update().where(self.users.c.id == user.id).values(**user.dict())
)
await self.database.execute(query)
return user
async def delete(self, user: BaseUserDB) -> None:
async def delete(self, user: UD) -> None:
query = self.users.delete().where(self.users.c.id == user.id)
await self.database.execute(query)

View File

@@ -3,11 +3,11 @@ from typing import List, Optional, Type
from tortoise import Model, fields
from tortoise.exceptions import DoesNotExist
from fastapi_users.db import BaseUserDatabase
from fastapi_users.models import BaseUserDB
from fastapi_users.db.base import BaseUserDatabase
from fastapi_users.models import UD
class BaseUserModel:
class TortoiseBaseUserModel(Model):
id = fields.CharField(pk=True, generated=False, max_length=255)
email = fields.CharField(index=True, unique=True, null=False, max_length=255)
hashed_password = fields.CharField(null=False, max_length=255)
@@ -15,44 +15,51 @@ class BaseUserModel:
is_superuser = fields.BooleanField(default=False, null=False)
class Meta:
table = "user"
abstract = True
class TortoiseUserDatabase(BaseUserDatabase):
class TortoiseUserDatabase(BaseUserDatabase[UD]):
"""
Database adapter for Tortoise ORM.
model: Type[Model]
:param user_db_model: Pydantic model of a DB representation of a user.
:param model: Tortoise ORM model.
"""
def __init__(self, model: Type[Model]):
model: Type[TortoiseBaseUserModel]
def __init__(self, user_db_model: Type[UD], model: Type[TortoiseBaseUserModel]):
super().__init__(user_db_model)
self.model = model
async def list(self) -> List[BaseUserDB]:
async def list(self) -> List[UD]:
users = await self.model.all()
return [BaseUserDB.from_orm(user) for user in users]
return [self.user_db_model.from_orm(user) for user in users]
async def get(self, id: str) -> Optional[BaseUserDB]:
async def get(self, id: str) -> Optional[UD]:
try:
user = await self.model.get(id=id)
return BaseUserDB.from_orm(user)
return self.user_db_model.from_orm(user)
except DoesNotExist:
return None
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
async def get_by_email(self, email: str) -> Optional[UD]:
try:
user = await self.model.get(email=email)
return BaseUserDB.from_orm(user)
return self.user_db_model.from_orm(user)
except DoesNotExist:
return None
async def create(self, user: BaseUserDB) -> BaseUserDB:
async def create(self, user: UD) -> UD:
model = self.model(**user.dict())
await model.save()
return user
async def update(self, user: BaseUserDB) -> BaseUserDB:
async def update(self, user: UD) -> UD:
user_dict = user.dict()
user_dict.pop("id") # Tortoise complains if we pass the PK again
await self.model.filter(id=user.id).update(**user_dict)
return user
async def delete(self, user: BaseUserDB) -> None:
async def delete(self, user: UD) -> None:
await self.model.filter(id=user.id).delete()

View File

@@ -1,8 +1,8 @@
from typing import Callable, Sequence, Type
from fastapi_users import models
from fastapi_users.authentication import Authenticator, BaseAuthentication
from fastapi_users.db import BaseUserDatabase
from fastapi_users.models import BaseUser
from fastapi_users.router import Event, UserRouter, get_user_router
@@ -13,6 +13,9 @@ class FastAPIUsers:
:param db: Database adapter instance.
:param auth_backends: List of authentication backends.
:param user_model: Pydantic model of a user.
:param user_create_model: Pydantic model for creating a user.
:param user_update_model: Pydantic model for updating a user.
:param user_db_model: Pydantic model of a DB representation of a user.
:param reset_password_token_secret: Secret to encode reset password token.
:param reset_password_token_lifetime_seconds: Lifetime of reset password token.
@@ -28,7 +31,10 @@ class FastAPIUsers:
self,
db: BaseUserDatabase,
auth_backends: Sequence[BaseAuthentication],
user_model: Type[BaseUser],
user_model: Type[models.BaseUser],
user_create_model: Type[models.BaseUserCreate],
user_update_model: Type[models.BaseUserUpdate],
user_db_model: Type[models.BaseUserDB],
reset_password_token_secret: str,
reset_password_token_lifetime_seconds: int = 3600,
):
@@ -37,6 +43,9 @@ class FastAPIUsers:
self.router = get_user_router(
self.db,
user_model,
user_create_model,
user_update_model,
user_db_model,
self.authenticator,
reset_password_token_secret,
reset_password_token_lifetime_seconds,

View File

@@ -1,5 +1,5 @@
import uuid
from typing import Optional, Type
from typing import Optional, TypeVar
import pydantic
from pydantic import BaseModel, EmailStr
@@ -25,9 +25,6 @@ class BaseUser(BaseModel):
def create_update_dict_superuser(self):
return self.dict(exclude_unset=True, exclude={"id"})
class Config:
orm_mode = True
class BaseUserCreate(BaseUser):
email: EmailStr
@@ -39,23 +36,11 @@ class BaseUserUpdate(BaseUser):
class BaseUserDB(BaseUser):
id: str
hashed_password: str
class Config:
orm_mode = True
class Models:
"""Generate models inheriting from the custom User model."""
def __init__(self, user_model: Type[BaseUser]):
class UserCreate(user_model, BaseUserCreate): # type: ignore
pass
class UserUpdate(user_model, BaseUserUpdate): # type: ignore
pass
class UserDB(user_model, BaseUserDB): # type: ignore
pass
self.User = user_model
self.UserCreate = UserCreate
self.UserUpdate = UserUpdate
self.UserDB = UserDB
UD = TypeVar("UD", bound=BaseUserDB)

View File

@@ -1,7 +1,7 @@
import asyncio
import typing
from collections import defaultdict
from enum import Enum, auto
from typing import Any, Callable, DefaultDict, Dict, List, Type, cast
import jwt
from fastapi import APIRouter, Body, Depends, HTTPException
@@ -10,9 +10,9 @@ from pydantic import EmailStr
from starlette import status
from starlette.responses import Response
from fastapi_users import models
from fastapi_users.authentication import Authenticator, BaseAuthentication
from fastapi_users.db import BaseUserDatabase
from fastapi_users.models import BaseUser, Models
from fastapi_users.password import get_password_hash
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
@@ -29,13 +29,13 @@ class Event(Enum):
class UserRouter(APIRouter):
event_handlers: typing.DefaultDict[Event, typing.List[typing.Callable]]
event_handlers: DefaultDict[Event, List[Callable]]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.event_handlers = defaultdict(list)
def add_event_handler(self, event_type: Event, func: typing.Callable) -> None:
def add_event_handler(self, event_type: Event, func: Callable) -> None:
self.event_handlers[event_type].append(func)
async def run_handlers(self, event_type: Event, *args, **kwargs) -> None:
@@ -65,30 +65,30 @@ def _add_login_route(
def get_user_router(
user_db: BaseUserDatabase,
user_model: typing.Type[BaseUser],
user_db: BaseUserDatabase[models.BaseUserDB],
user_model: Type[models.BaseUser],
user_create_model: Type[models.BaseUserCreate],
user_update_model: Type[models.BaseUserUpdate],
user_db_model: Type[models.BaseUserDB],
authenticator: Authenticator,
reset_password_token_secret: str,
reset_password_token_lifetime_seconds: int = 3600,
) -> UserRouter:
"""Generate a router with the authentication routes."""
router = UserRouter()
models = Models(user_model)
reset_password_token_audience = "fastapi-users:reset"
get_current_active_user = authenticator.get_current_active_user
get_current_superuser = authenticator.get_current_superuser
async def _get_or_404(id: str) -> models.UserDB: # type: ignore
async def _get_or_404(id: str) -> models.BaseUserDB:
user = await user_db.get(id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return user
async def _update_user(
user: models.UserDB, update_dict: typing.Dict[str, typing.Any] # type: ignore
):
async def _update_user(user: models.BaseUserDB, update_dict: Dict[str, Any]):
for field in update_dict:
if field == "password":
hashed_password = get_password_hash(update_dict[field])
@@ -101,9 +101,10 @@ def get_user_router(
_add_login_route(router, user_db, auth_backend)
@router.post(
"/register", response_model=models.User, status_code=status.HTTP_201_CREATED
"/register", response_model=user_model, status_code=status.HTTP_201_CREATED
)
async def register(user: models.UserCreate): # type: ignore
async def register(user: user_create_model): # type: ignore
user = cast(models.BaseUserCreate, user) # Prevent mypy complain
existing_user = await user_db.get_by_email(user.email)
if existing_user is not None:
@@ -113,7 +114,7 @@ def get_user_router(
)
hashed_password = get_password_hash(user.password)
db_user = models.UserDB(
db_user = user_db_model(
**user.create_update_dict(), hashed_password=hashed_password
)
created_user = await user_db.create(db_user)
@@ -168,48 +169,60 @@ def get_user_router(
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
)
@router.get("/me", response_model=models.User)
@router.get("/me", response_model=user_model)
async def me(
user: models.UserDB = Depends(get_current_active_user), # type: ignore
user: user_db_model = Depends(get_current_active_user), # type: ignore
):
return user
@router.patch("/me", response_model=models.User)
@router.patch("/me", response_model=user_model)
async def update_me(
updated_user: models.UserUpdate, # type: ignore
user: models.UserDB = Depends(get_current_active_user), # type: ignore
updated_user: user_update_model, # type: ignore
user: user_db_model = Depends(get_current_active_user), # type: ignore
):
updated_user = cast(
models.BaseUserUpdate, updated_user,
) # Prevent mypy complain
updated_user_data = updated_user.create_update_dict()
return await _update_user(user, updated_user_data)
@router.get("/", response_model=typing.List[models.User]) # type: ignore
async def list_users(
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
):
@router.get(
"/",
response_model=List[user_model], # type: ignore
dependencies=[Depends(get_current_superuser)],
)
async def list_users():
return await user_db.list()
@router.get("/{id}", response_model=models.User)
async def get_user(
id: str,
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
):
@router.get(
"/{id}",
response_model=user_model,
dependencies=[Depends(get_current_superuser)],
)
async def get_user(id: str,):
return await _get_or_404(id)
@router.patch("/{id}", response_model=models.User)
@router.patch(
"/{id}",
response_model=user_model,
dependencies=[Depends(get_current_superuser)],
)
async def update_user(
id: str,
updated_user: models.UserUpdate, # type: ignore
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
id: str, updated_user: user_update_model, # type: ignore
):
updated_user = cast(
models.BaseUserUpdate, updated_user,
) # Prevent mypy complain
user = await _get_or_404(id)
updated_user_data = updated_user.create_update_dict_superuser()
return await _update_user(user, updated_user_data)
@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
id: str,
superuser: models.UserDB = Depends(get_current_superuser), # type: ignore
):
@router.delete(
"/{id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(get_current_superuser)],
)
async def delete_user(id: str):
user = await _get_or_404(id)
await user_db.delete(user)
return None

View File

@@ -8,6 +8,7 @@ from starlette.requests import Request
from starlette.responses import Response
from starlette.testclient import TestClient
from fastapi_users import models
from fastapi_users.authentication import Authenticator, BaseAuthentication
from fastapi_users.db import BaseUserDatabase
from fastapi_users.models import BaseUserDB
@@ -18,9 +19,25 @@ angharad_password_hash = get_password_hash("angharad")
viviane_password_hash = get_password_hash("viviane")
class User(models.BaseUser):
first_name: Optional[str]
class UserCreate(User, models.BaseUserCreate):
pass
class UserUpdate(User, models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
@pytest.fixture
def user() -> BaseUserDB:
return BaseUserDB(
def user() -> UserDB:
return UserDB(
id="aaa",
email="king.arthur@camelot.bt",
hashed_password=guinevere_password_hash,
@@ -28,8 +45,8 @@ def user() -> BaseUserDB:
@pytest.fixture
def inactive_user() -> BaseUserDB:
return BaseUserDB(
def inactive_user() -> UserDB:
return UserDB(
id="bbb",
email="percival@camelot.bt",
hashed_password=angharad_password_hash,
@@ -38,8 +55,8 @@ def inactive_user() -> BaseUserDB:
@pytest.fixture
def superuser() -> BaseUserDB:
return BaseUserDB(
def superuser() -> UserDB:
return UserDB(
id="ccc",
email="merlin@camelot.bt",
hashed_password=viviane_password_hash,
@@ -49,11 +66,11 @@ def superuser() -> BaseUserDB:
@pytest.fixture
def mock_user_db(user, inactive_user, superuser) -> BaseUserDatabase:
class MockUserDatabase(BaseUserDatabase):
async def list(self) -> List[BaseUserDB]:
class MockUserDatabase(BaseUserDatabase[UserDB]):
async def list(self) -> List[UserDB]:
return [user, inactive_user, superuser]
async def get(self, id: str) -> Optional[BaseUserDB]:
async def get(self, id: str) -> Optional[UserDB]:
if id == user.id:
return user
if id == inactive_user.id:
@@ -62,7 +79,7 @@ def mock_user_db(user, inactive_user, superuser) -> BaseUserDatabase:
return superuser
return None
async def get_by_email(self, email: str) -> Optional[BaseUserDB]:
async def get_by_email(self, email: str) -> Optional[UserDB]:
if email == user.email:
return user
if email == inactive_user.email:
@@ -71,16 +88,16 @@ def mock_user_db(user, inactive_user, superuser) -> BaseUserDatabase:
return superuser
return None
async def create(self, user: BaseUserDB) -> BaseUserDB:
async def create(self, user: UserDB) -> UserDB:
return user
async def update(self, user: BaseUserDB) -> BaseUserDB:
async def update(self, user: UserDB) -> UserDB:
return user
async def delete(self, user: BaseUserDB) -> None:
async def delete(self, user: UserDB) -> None:
pass
return MockUserDatabase()
return MockUserDatabase(UserDB)
class MockAuthentication(BaseAuthentication):
@@ -139,20 +156,18 @@ def get_test_auth_client(mock_user_db):
authenticator = Authenticator(backends, mock_user_db)
@app.get("/test-current-user")
def test_current_user(
user: BaseUserDB = Depends(authenticator.get_current_user),
):
def test_current_user(user: UserDB = Depends(authenticator.get_current_user),):
return user
@app.get("/test-current-active-user")
def test_current_active_user(
user: BaseUserDB = Depends(authenticator.get_current_active_user),
user: UserDB = Depends(authenticator.get_current_active_user),
):
return user
@app.get("/test-current-superuser")
def test_current_superuser(
user: BaseUserDB = Depends(authenticator.get_current_superuser),
user: UserDB = Depends(authenticator.get_current_superuser),
):
return user

View File

@@ -84,9 +84,7 @@ async def test_get_login_response(cookie_authentication, user):
# so that FastAPI can terminate it properly
assert login_response is None
cookies = [
header for header in response.raw_headers if header[0] == b"set-cookie"
]
cookies = [header for header in response.raw_headers if header[0] == b"set-cookie"]
assert len(cookies) == 1
cookie = cookies[0][1].decode("latin-1")

View File

@@ -2,6 +2,7 @@ import pytest
from fastapi.security import OAuth2PasswordRequestForm
from fastapi_users.db import BaseUserDatabase
from tests.conftest import UserDB
@pytest.fixture
@@ -15,7 +16,7 @@ def create_oauth2_password_request_form():
@pytest.mark.asyncio
@pytest.mark.db
async def test_not_implemented_methods(user):
base_user_db = BaseUserDatabase()
base_user_db = BaseUserDatabase(UserDB)
with pytest.raises(NotImplementedError):
await base_user_db.list()

View File

@@ -5,8 +5,8 @@ import pytest
import pymongo.errors
from fastapi_users.db.mongodb import MongoDBUserDatabase
from fastapi_users.models import BaseUserDB
from fastapi_users.password import get_password_hash
from tests.conftest import UserDB
@pytest.fixture
@@ -24,15 +24,15 @@ async def mongodb_user_db() -> AsyncGenerator[MongoDBUserDatabase, None]:
db = client["test_database"]
collection = db["users"]
yield MongoDBUserDatabase(collection)
yield MongoDBUserDatabase(UserDB, collection)
await collection.drop()
@pytest.mark.asyncio
@pytest.mark.db
async def test_queries(mongodb_user_db):
user = BaseUserDB(
async def test_queries(mongodb_user_db: MongoDBUserDatabase[UserDB]):
user = UserDB(
id="111",
email="lancelot@camelot.bt",
hashed_password=get_password_hash("guinevere"),
@@ -51,11 +51,13 @@ async def test_queries(mongodb_user_db):
# Get by id
id_user = await mongodb_user_db.get(user.id)
assert id_user is not None
assert id_user.id == user_db.id
assert id_user.is_superuser is True
# Get by email
email_user = await mongodb_user_db.get_by_email(user.email)
email_user = await mongodb_user_db.get_by_email(str(user.email))
assert email_user is not None
assert email_user.id == user_db.id
# List
@@ -76,3 +78,21 @@ async def test_queries(mongodb_user_db):
await mongodb_user_db.delete(user)
deleted_user = await mongodb_user_db.get(user.id)
assert deleted_user is None
@pytest.mark.asyncio
@pytest.mark.db
async def test_queries_custom_fields(mongodb_user_db: MongoDBUserDatabase[UserDB]):
"""It should output custom fields in query result."""
user = UserDB(
id="111",
email="lancelot@camelot.bt",
hashed_password=get_password_hash("guinevere"),
first_name="Lancelot",
)
await mongodb_user_db.create(user)
id_user = await mongodb_user_db.get(user.id)
assert id_user is not None
assert id_user.id == user.id
assert id_user.first_name == user.first_name

View File

@@ -4,11 +4,12 @@ from typing import AsyncGenerator
import pytest
import sqlalchemy
from databases import Database
from sqlalchemy import Column, String
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from fastapi_users.db.sqlalchemy import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from fastapi_users.models import BaseUserDB
from fastapi_users.password import get_password_hash
from tests.conftest import UserDB
@pytest.fixture
@@ -16,7 +17,7 @@ async def sqlalchemy_user_db() -> AsyncGenerator[SQLAlchemyUserDatabase, None]:
Base: DeclarativeMeta = declarative_base()
class User(SQLAlchemyBaseUserTable, Base):
pass
first_name = Column(String, nullable=True)
DATABASE_URL = "sqlite:///./test.db"
database = Database(DATABASE_URL)
@@ -28,15 +29,15 @@ async def sqlalchemy_user_db() -> AsyncGenerator[SQLAlchemyUserDatabase, None]:
await database.connect()
yield SQLAlchemyUserDatabase(database, User.__table__)
yield SQLAlchemyUserDatabase(UserDB, database, User.__table__)
Base.metadata.drop_all(engine)
@pytest.mark.asyncio
@pytest.mark.db
async def test_queries(sqlalchemy_user_db):
user = BaseUserDB(
async def test_queries(sqlalchemy_user_db: SQLAlchemyUserDatabase[UserDB]):
user = UserDB(
id="111",
email="lancelot@camelot.bt",
hashed_password=get_password_hash("guinevere"),
@@ -55,11 +56,13 @@ async def test_queries(sqlalchemy_user_db):
# Get by id
id_user = await sqlalchemy_user_db.get(user.id)
assert id_user is not None
assert id_user.id == user_db.id
assert id_user.is_superuser is True
# Get by email
email_user = await sqlalchemy_user_db.get_by_email(user.email)
email_user = await sqlalchemy_user_db.get_by_email(str(user.email))
assert email_user is not None
assert email_user.id == user_db.id
# List
@@ -74,7 +77,7 @@ async def test_queries(sqlalchemy_user_db):
# Exception when inserting non-nullable fields
with pytest.raises(sqlite3.IntegrityError):
wrong_user = BaseUserDB(id="222", hashed_password="aaa")
wrong_user = UserDB(id="222", hashed_password="aaa")
await sqlalchemy_user_db.create(wrong_user)
# Unknown user
@@ -85,3 +88,23 @@ async def test_queries(sqlalchemy_user_db):
await sqlalchemy_user_db.delete(user)
deleted_user = await sqlalchemy_user_db.get(user.id)
assert deleted_user is None
@pytest.mark.asyncio
@pytest.mark.db
async def test_queries_custom_fields(
sqlalchemy_user_db: SQLAlchemyUserDatabase[UserDB],
):
"""It should output custom fields in query result."""
user = UserDB(
id="111",
email="lancelot@camelot.bt",
hashed_password=get_password_hash("guinevere"),
first_name="Lancelot",
)
await sqlalchemy_user_db.create(user)
id_user = await sqlalchemy_user_db.get(user.id)
assert id_user is not None
assert id_user.id == user.id
assert id_user.first_name == user.first_name

View File

@@ -1,16 +1,16 @@
from typing import AsyncGenerator
import pytest
from tortoise import Model
from tortoise.exceptions import IntegrityError
from tortoise import Tortoise
from fastapi_users.db.tortoise import TortoiseUserDatabase, BaseUserModel
from fastapi_users.models import BaseUserDB
from tortoise import Tortoise, fields
from fastapi_users.db.tortoise import TortoiseUserDatabase, TortoiseBaseUserModel
from fastapi_users.password import get_password_hash
from tests.conftest import UserDB
class User(BaseUserModel, Model):
pass
class User(TortoiseBaseUserModel):
first_name = fields.CharField(null=True, max_length=255)
@pytest.fixture
@@ -22,7 +22,7 @@ async def tortoise_user_db() -> AsyncGenerator[TortoiseUserDatabase, None]:
)
await Tortoise.generate_schemas()
yield TortoiseUserDatabase(User)
yield TortoiseUserDatabase(UserDB, User)
await User.all().delete()
await Tortoise.close_connections()
@@ -30,8 +30,8 @@ async def tortoise_user_db() -> AsyncGenerator[TortoiseUserDatabase, None]:
@pytest.mark.asyncio
@pytest.mark.db
async def test_queries(tortoise_user_db: TortoiseUserDatabase):
user = BaseUserDB(
async def test_queries(tortoise_user_db: TortoiseUserDatabase[UserDB]):
user = UserDB(
id="111",
email="lancelot@camelot.bt",
hashed_password=get_password_hash("guinevere"),
@@ -50,11 +50,13 @@ async def test_queries(tortoise_user_db: TortoiseUserDatabase):
# Get by id
id_user = await tortoise_user_db.get(user.id)
assert id_user is not None
assert id_user.id == user_db.id
assert id_user.is_superuser is True
# Get by email
email_user = await tortoise_user_db.get_by_email(user.email)
email_user = await tortoise_user_db.get_by_email(str(user.email))
assert email_user is not None
assert email_user.id == user_db.id
# List
@@ -69,7 +71,7 @@ async def test_queries(tortoise_user_db: TortoiseUserDatabase):
# Exception when inserting non-nullable fields
with pytest.raises(ValueError):
wrong_user = BaseUserDB(id="222", hashed_password="aaa")
wrong_user = UserDB(id="222", hashed_password="aaa")
await tortoise_user_db.create(wrong_user)
# Unknown user
@@ -80,3 +82,21 @@ async def test_queries(tortoise_user_db: TortoiseUserDatabase):
await tortoise_user_db.delete(user)
deleted_user = await tortoise_user_db.get(user.id)
assert deleted_user is None
@pytest.mark.asyncio
@pytest.mark.db
async def test_queries_custom_fields(tortoise_user_db: TortoiseUserDatabase[UserDB]):
"""It should output custom fields in query result."""
user = UserDB(
id="111",
email="lancelot@camelot.bt",
hashed_password=get_password_hash("guinevere"),
first_name="Lancelot",
)
await tortoise_user_db.create(user)
id_user = await tortoise_user_db.get(user.id)
assert id_user is not None
assert id_user.id == user.id
assert id_user.first_name == user.first_name

View File

@@ -4,8 +4,8 @@ from starlette import status
from starlette.testclient import TestClient
from fastapi_users import FastAPIUsers
from fastapi_users.models import BaseUser, BaseUserDB
from fastapi_users.router import Event
from tests.conftest import User, UserCreate, UserUpdate, UserDB
def sync_event_handler():
@@ -18,10 +18,15 @@ async def async_event_handler():
@pytest.fixture(params=[sync_event_handler, async_event_handler])
def fastapi_users(request, mock_user_db, mock_authentication) -> FastAPIUsers:
class User(BaseUser):
pass
fastapi_users = FastAPIUsers(mock_user_db, [mock_authentication], User, "SECRET")
fastapi_users = FastAPIUsers(
mock_user_db,
[mock_authentication],
User,
UserCreate,
UserUpdate,
UserDB,
"SECRET",
)
@fastapi_users.on_after_register()
def on_after_register():
@@ -100,7 +105,7 @@ class TestGetCurrentUser:
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_valid_token(self, test_app_client: TestClient, user: BaseUserDB):
def test_valid_token(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.get(
"/current-user", headers={"Authorization": f"Bearer {user.id}"}
)
@@ -120,7 +125,7 @@ class TestGetCurrentActiveUser:
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_valid_token_inactive_user(
self, test_app_client: TestClient, inactive_user: BaseUserDB
self, test_app_client: TestClient, inactive_user: UserDB
):
response = test_app_client.get(
"/current-active-user",
@@ -128,7 +133,7 @@ class TestGetCurrentActiveUser:
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_valid_token(self, test_app_client: TestClient, user: BaseUserDB):
def test_valid_token(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.get(
"/current-active-user", headers={"Authorization": f"Bearer {user.id}"}
)
@@ -147,16 +152,14 @@ class TestGetCurrentSuperuser:
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_valid_token_regular_user(
self, test_app_client: TestClient, user: BaseUserDB
):
def test_valid_token_regular_user(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.get(
"/current-superuser", headers={"Authorization": f"Bearer {user.id}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_valid_token_superuser(
self, test_app_client: TestClient, superuser: BaseUserDB
self, test_app_client: TestClient, superuser: UserDB
):
response = test_app_client.get(
"/current-superuser", headers={"Authorization": f"Bearer {superuser.id}"}

View File

@@ -8,10 +8,9 @@ from starlette import status
from starlette.testclient import TestClient
from fastapi_users.authentication import Authenticator
from fastapi_users.models import BaseUser, BaseUserDB
from fastapi_users.router import ErrorCode, Event, get_user_router
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
from tests.conftest import MockAuthentication
from tests.conftest import MockAuthentication, User, UserCreate, UserUpdate, UserDB
SECRET = "SECRET"
LIFETIME = 3600
@@ -43,15 +42,21 @@ def event_handler(request):
@pytest.fixture()
def test_app_client(mock_user_db, mock_authentication, event_handler) -> TestClient:
class User(BaseUser):
pass
mock_authentication_bis = MockAuthentication(name="mock-bis")
authenticator = Authenticator(
[mock_authentication, mock_authentication_bis], mock_user_db
)
userRouter = get_user_router(mock_user_db, User, authenticator, SECRET, LIFETIME)
userRouter = get_user_router(
mock_user_db,
User,
UserCreate,
UserUpdate,
UserDB,
authenticator,
SECRET,
LIFETIME,
)
userRouter.add_event_handler(Event.ON_AFTER_REGISTER, event_handler)
userRouter.add_event_handler(Event.ON_AFTER_FORGOT_PASSWORD, event_handler)
@@ -158,9 +163,7 @@ class TestLogin:
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
def test_valid_credentials(
self, path, test_app_client: TestClient, user: BaseUserDB
):
def test_valid_credentials(self, path, test_app_client: TestClient, user: UserDB):
data = {"username": "king.arthur@camelot.bt", "password": "guinevere"}
response = test_app_client.post(path, data=data)
assert response.status_code == status.HTTP_200_OK
@@ -249,7 +252,7 @@ class TestResetPassword:
mock_user_db,
test_app_client: TestClient,
forgot_password_token,
inactive_user: BaseUserDB,
inactive_user: UserDB,
):
mocker.spy(mock_user_db, "update")
@@ -268,7 +271,7 @@ class TestResetPassword:
mock_user_db,
test_app_client: TestClient,
forgot_password_token,
user: BaseUserDB,
user: UserDB,
):
mocker.spy(mock_user_db, "update")
current_hashed_passord = user.hashed_password
@@ -288,15 +291,13 @@ class TestMe:
response = test_app_client.get("/me")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_inactive_user(
self, test_app_client: TestClient, inactive_user: BaseUserDB
):
def test_inactive_user(self, test_app_client: TestClient, inactive_user: UserDB):
response = test_app_client.get(
"/me", headers={"Authorization": f"Bearer {inactive_user.id}"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_active_user(self, test_app_client: TestClient, user: BaseUserDB):
def test_active_user(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.get(
"/me", headers={"Authorization": f"Bearer {user.id}"}
)
@@ -313,15 +314,13 @@ class TestUpdateMe:
response = test_app_client.patch("/me")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_inactive_user(
self, test_app_client: TestClient, inactive_user: BaseUserDB
):
def test_inactive_user(self, test_app_client: TestClient, inactive_user: UserDB):
response = test_app_client.patch(
"/me", headers={"Authorization": f"Bearer {inactive_user.id}"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_empty_body(self, test_app_client: TestClient, user: BaseUserDB):
def test_empty_body(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.patch(
"/me", json={}, headers={"Authorization": f"Bearer {user.id}"}
)
@@ -330,7 +329,7 @@ class TestUpdateMe:
response_json = response.json()
assert response_json["email"] == user.email
def test_valid_body(self, test_app_client: TestClient, user: BaseUserDB):
def test_valid_body(self, test_app_client: TestClient, user: UserDB):
json = {"email": "king.arthur@tintagel.bt"}
response = test_app_client.patch(
"/me", json=json, headers={"Authorization": f"Bearer {user.id}"}
@@ -340,9 +339,7 @@ class TestUpdateMe:
response_json = response.json()
assert response_json["email"] == "king.arthur@tintagel.bt"
def test_valid_body_is_superuser(
self, test_app_client: TestClient, user: BaseUserDB
):
def test_valid_body_is_superuser(self, test_app_client: TestClient, user: UserDB):
json = {"is_superuser": True}
response = test_app_client.patch(
"/me", json=json, headers={"Authorization": f"Bearer {user.id}"}
@@ -352,7 +349,7 @@ class TestUpdateMe:
response_json = response.json()
assert response_json["is_superuser"] is False
def test_valid_body_is_active(self, test_app_client: TestClient, user: BaseUserDB):
def test_valid_body_is_active(self, test_app_client: TestClient, user: UserDB):
json = {"is_active": False}
response = test_app_client.patch(
"/me", json=json, headers={"Authorization": f"Bearer {user.id}"}
@@ -363,7 +360,7 @@ class TestUpdateMe:
assert response_json["is_active"] is True
def test_valid_body_password(
self, mocker, mock_user_db, test_app_client: TestClient, user: BaseUserDB
self, mocker, mock_user_db, test_app_client: TestClient, user: UserDB
):
mocker.spy(mock_user_db, "update")
current_hashed_passord = user.hashed_password
@@ -385,13 +382,13 @@ class TestListUsers:
response = test_app_client.get("/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
def test_regular_user(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.get(
"/", headers={"Authorization": f"Bearer {user.id}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_superuser(self, test_app_client: TestClient, superuser: BaseUserDB):
def test_superuser(self, test_app_client: TestClient, superuser: UserDB):
response = test_app_client.get(
"/", headers={"Authorization": f"Bearer {superuser.id}"}
)
@@ -410,22 +407,20 @@ class TestGetUser:
response = test_app_client.get("/000")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
def test_regular_user(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.get(
"/000", headers={"Authorization": f"Bearer {user.id}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_not_existing_user(
self, test_app_client: TestClient, superuser: BaseUserDB
):
def test_not_existing_user(self, test_app_client: TestClient, superuser: UserDB):
response = test_app_client.get(
"/000", headers={"Authorization": f"Bearer {superuser.id}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_superuser(
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
self, test_app_client: TestClient, user: UserDB, superuser: UserDB
):
response = test_app_client.get(
f"/{user.id}", headers={"Authorization": f"Bearer {superuser.id}"}
@@ -443,22 +438,20 @@ class TestUpdateUser:
response = test_app_client.patch("/000")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
def test_regular_user(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.patch(
"/000", headers={"Authorization": f"Bearer {user.id}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_not_existing_user(
self, test_app_client: TestClient, superuser: BaseUserDB
):
def test_not_existing_user(self, test_app_client: TestClient, superuser: UserDB):
response = test_app_client.patch(
"/000", json={}, headers={"Authorization": f"Bearer {superuser.id}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_empty_body(
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
self, test_app_client: TestClient, user: UserDB, superuser: UserDB
):
response = test_app_client.patch(
f"/{user.id}", json={}, headers={"Authorization": f"Bearer {superuser.id}"}
@@ -469,7 +462,7 @@ class TestUpdateUser:
assert response_json["email"] == user.email
def test_valid_body(
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
self, test_app_client: TestClient, user: UserDB, superuser: UserDB
):
json = {"email": "king.arthur@tintagel.bt"}
response = test_app_client.patch(
@@ -483,7 +476,7 @@ class TestUpdateUser:
assert response_json["email"] == "king.arthur@tintagel.bt"
def test_valid_body_is_superuser(
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
self, test_app_client: TestClient, user: UserDB, superuser: UserDB
):
json = {"is_superuser": True}
response = test_app_client.patch(
@@ -497,7 +490,7 @@ class TestUpdateUser:
assert response_json["is_superuser"] is True
def test_valid_body_is_active(
self, test_app_client: TestClient, user: BaseUserDB, superuser: BaseUserDB
self, test_app_client: TestClient, user: UserDB, superuser: UserDB
):
json = {"is_active": False}
response = test_app_client.patch(
@@ -515,8 +508,8 @@ class TestUpdateUser:
mocker,
mock_user_db,
test_app_client: TestClient,
user: BaseUserDB,
superuser: BaseUserDB,
user: UserDB,
superuser: UserDB,
):
mocker.spy(mock_user_db, "update")
current_hashed_passord = user.hashed_password
@@ -540,15 +533,13 @@ class TestDeleteUser:
response = test_app_client.delete("/000")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_regular_user(self, test_app_client: TestClient, user: BaseUserDB):
def test_regular_user(self, test_app_client: TestClient, user: UserDB):
response = test_app_client.delete(
"/000", headers={"Authorization": f"Bearer {user.id}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_not_existing_user(
self, test_app_client: TestClient, superuser: BaseUserDB
):
def test_not_existing_user(self, test_app_client: TestClient, superuser: UserDB):
response = test_app_client.delete(
"/000", headers={"Authorization": f"Bearer {superuser.id}"}
)
@@ -559,8 +550,8 @@ class TestDeleteUser:
mocker,
mock_user_db,
test_app_client: TestClient,
user: BaseUserDB,
superuser: BaseUserDB,
user: UserDB,
superuser: UserDB,
):
mocker.spy(mock_user_db, "delete")