mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2026-03-13 07:49:55 +08:00
Extending generated OpenAPI docs (#799)
* Added login endpoint docs
* make format
* Changed login route into multiple examples.
* Added reset password router docs
* Updated /{id} routes for user
* Updated /me routes
* Fixed user already exists response description
* Updated the /register route
* Updated verify routes
* Updated oauth2 endpoints.
* Applied `make format`
* Renamed Authentication methods for getting their openapi schemas
- `get_login_responses_success` -> `get_openapi_login_responses_success`
- `get_logout_responses_success` -> `get_openapi_logout_responses_success`
* Fixed flake8 errors
* Not using `Final` to keep python37 compatibility
Co-authored-by: François Voron <fvoron@gmail.com>
This commit is contained in:
@@ -90,7 +90,7 @@ Register a new user. Will call the `on_after_register` [handler](../configuratio
|
||||
```json
|
||||
{
|
||||
"detail": {
|
||||
"code": "RESET_PASSWORD_INVALID_PASSWORD",
|
||||
"code": "REGISTER_INVALID_PASSWORD",
|
||||
"reason": "Password should be at least 3 characters"
|
||||
}
|
||||
}
|
||||
@@ -182,15 +182,6 @@ Verify a user. Requires the token generated by the `/request-verify-token` route
|
||||
|
||||
!!! fail "`422 Validation Error`"
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
Expired token.
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "VERIFY_USER_TOKEN_EXPIRED"
|
||||
}
|
||||
```
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
Bad token, not existing user or not the e-mail currently set for the user.
|
||||
|
||||
@@ -252,6 +243,18 @@ Depending on the situation, several things can happen:
|
||||
* A new user is created and linked to the OAuth account.
|
||||
* The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md).
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
Invalid token.
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
User is inactive.
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "LOGIN_BAD_CREDENTIALS"
|
||||
}
|
||||
```
|
||||
|
||||
## Users router
|
||||
|
||||
### `GET /me`
|
||||
@@ -309,6 +312,16 @@ Update the current authenticated active user.
|
||||
}
|
||||
```
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
A user with this email already exists.
|
||||
```json
|
||||
{
|
||||
"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"
|
||||
}
|
||||
```
|
||||
|
||||
!!! fail "`422 Validation Error`"
|
||||
|
||||
### `GET /{user_id}`
|
||||
|
||||
Return the user with id `user_id`.
|
||||
@@ -377,6 +390,14 @@ Update the user with id `user_id`.
|
||||
}
|
||||
```
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
A user with this email already exists.
|
||||
```json
|
||||
{
|
||||
"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"
|
||||
}
|
||||
```
|
||||
|
||||
### `DELETE /{user_id}`
|
||||
|
||||
Delete the user with id `user_id`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Optional, TypeVar
|
||||
from typing import Any, Dict, Generic, Optional, TypeVar
|
||||
|
||||
from fastapi import Response
|
||||
from fastapi.security.base import SecurityBase
|
||||
@@ -42,6 +42,11 @@ class BaseAuthentication(Generic[T, models.UC, models.UD]):
|
||||
) -> Any:
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_login_responses_success() -> Dict[str, Any]:
|
||||
"""Return a dictionary to use for the openapi responses route parameter."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_logout_response(
|
||||
self,
|
||||
user: models.UD,
|
||||
@@ -49,3 +54,8 @@ class BaseAuthentication(Generic[T, models.UC, models.UD]):
|
||||
user_manager: BaseUserManager[models.UC, models.UD],
|
||||
) -> Any:
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_logout_responses_success() -> Dict[str, Any]:
|
||||
"""Return a dictionary to use for the openapi responses route parameter."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Any, Generic, List, Optional
|
||||
from typing import Any, Dict, Generic, List, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Response
|
||||
from fastapi import Response, status
|
||||
from fastapi.security import APIKeyCookie
|
||||
from pydantic import UUID4
|
||||
|
||||
@@ -113,6 +113,9 @@ class CookieAuthentication(
|
||||
# so that FastAPI can terminate it properly
|
||||
return None
|
||||
|
||||
def get_openapi_login_responses_success(self) -> Dict[str, Any]:
|
||||
return {status.HTTP_200_OK: {"model": None}}
|
||||
|
||||
async def get_logout_response(
|
||||
self,
|
||||
user: models.UD,
|
||||
@@ -123,6 +126,9 @@ class CookieAuthentication(
|
||||
self.cookie_name, path=self.cookie_path, domain=self.cookie_domain
|
||||
)
|
||||
|
||||
def get_openapi_logout_responses_success(self) -> Dict[str, Any]:
|
||||
return {status.HTTP_200_OK: {"model": None}}
|
||||
|
||||
async def _generate_token(self, user: models.UD) -> str:
|
||||
data = {"user_id": str(user.id), "aud": self.token_audience}
|
||||
return generate_jwt(data, self.secret, self.lifetime_seconds)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import Any, Generic, List, Optional
|
||||
from typing import Any, Dict, Generic, List, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Response
|
||||
from fastapi import Response, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from pydantic import UUID4
|
||||
from pydantic import UUID4, BaseModel
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication.base import BaseAuthentication
|
||||
@@ -11,6 +11,11 @@ from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
|
||||
from fastapi_users.manager import BaseUserManager, UserNotExists
|
||||
|
||||
|
||||
class JWTLoginResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class JWTAuthentication(
|
||||
Generic[models.UC, models.UD], BaseAuthentication[str, models.UC, models.UD]
|
||||
):
|
||||
@@ -74,7 +79,27 @@ class JWTAuthentication(
|
||||
user_manager: BaseUserManager[models.UC, models.UD],
|
||||
) -> Any:
|
||||
token = await self._generate_token(user)
|
||||
return {"access_token": token, "token_type": "bearer"}
|
||||
return JWTLoginResponse(access_token=token, token_type="bearer")
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_login_responses_success() -> Dict[str, Any]:
|
||||
return {
|
||||
status.HTTP_200_OK: {
|
||||
"model": JWTLoginResponse,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1"
|
||||
"c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2Z"
|
||||
"DMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS"
|
||||
"11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ."
|
||||
"M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI",
|
||||
"token_type": "bearer",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async def _generate_token(self, user: models.UD) -> str:
|
||||
data = {"user_id": str(user.id), "aud": self.token_audience}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator, BaseAuthentication
|
||||
from fastapi_users.manager import BaseUserManager, UserManagerDependency
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.common import ErrorCode, ErrorModel
|
||||
|
||||
|
||||
class LoginBadCredentials(BaseModel):
|
||||
detail: ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
|
||||
class LoginUserNotVerified(BaseModel):
|
||||
detail: ErrorCode.LOGIN_USER_NOT_VERIFIED
|
||||
|
||||
|
||||
def get_auth_router(
|
||||
@@ -19,7 +28,32 @@ def get_auth_router(
|
||||
active=True, verified=requires_verification
|
||||
)
|
||||
|
||||
@router.post("/login", name="auth:login")
|
||||
login_responses = {
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
ErrorCode.LOGIN_BAD_CREDENTIALS: {
|
||||
"summary": "Bad credentials or the user is inactive.",
|
||||
"value": {"detail": ErrorCode.LOGIN_BAD_CREDENTIALS},
|
||||
},
|
||||
ErrorCode.LOGIN_USER_NOT_VERIFIED: {
|
||||
"summary": "The user is not verified.",
|
||||
"value": {"detail": ErrorCode.LOGIN_USER_NOT_VERIFIED},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
**backend.get_openapi_login_responses_success(),
|
||||
}
|
||||
|
||||
@router.post(
|
||||
"/login",
|
||||
name="auth:login",
|
||||
responses=login_responses,
|
||||
)
|
||||
async def login(
|
||||
response: Response,
|
||||
credentials: OAuth2PasswordRequestForm = Depends(),
|
||||
@@ -40,8 +74,16 @@ def get_auth_router(
|
||||
return await backend.get_login_response(user, response, user_manager)
|
||||
|
||||
if backend.logout:
|
||||
logout_responses = {
|
||||
**{
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user."
|
||||
}
|
||||
},
|
||||
**backend.get_openapi_logout_responses_success(),
|
||||
}
|
||||
|
||||
@router.post("/logout", name="auth:logout")
|
||||
@router.post("/logout", name="auth:logout", responses=logout_responses)
|
||||
async def logout(
|
||||
response: Response,
|
||||
user=Depends(get_current_user),
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
from typing import Dict, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ErrorModel(BaseModel):
|
||||
detail: Union[str, Dict[str, str]]
|
||||
|
||||
|
||||
class ErrorCodeReasonModel(BaseModel):
|
||||
code: str
|
||||
reason: str
|
||||
|
||||
|
||||
class ErrorCode:
|
||||
REGISTER_INVALID_PASSWORD = "REGISTER_INVALID_PASSWORD"
|
||||
REGISTER_USER_ALREADY_EXISTS = "REGISTER_USER_ALREADY_EXISTS"
|
||||
|
||||
@@ -10,7 +10,7 @@ from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
|
||||
from fastapi_users.manager import BaseUserManager, UserManagerDependency
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.common import ErrorCode, ErrorModel
|
||||
|
||||
STATE_TOKEN_AUDIENCE = "fastapi-users:oauth-state"
|
||||
|
||||
@@ -76,7 +76,31 @@ def get_oauth_router(
|
||||
|
||||
return {"authorization_url": authorization_url}
|
||||
|
||||
@router.get("/callback", name=f"oauth:{oauth_client.name}-callback")
|
||||
@router.get(
|
||||
"/callback",
|
||||
name=f"oauth:{oauth_client.name}-callback",
|
||||
description="The response varies based on the"
|
||||
"`authentication_backend` used on the `/authorize` endpoint.",
|
||||
responses={
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
"jwt_decode": {
|
||||
"summary": "Invalid token.",
|
||||
"value": None,
|
||||
},
|
||||
ErrorCode.LOGIN_BAD_CREDENTIALS: {
|
||||
"summary": "Password validation failed.",
|
||||
"value": {"detail": ErrorCode.LOGIN_BAD_CREDENTIALS},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
async def callback(
|
||||
request: Request,
|
||||
response: Response,
|
||||
|
||||
@@ -9,7 +9,7 @@ from fastapi_users.manager import (
|
||||
UserAlreadyExists,
|
||||
UserManagerDependency,
|
||||
)
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.common import ErrorCode, ErrorModel
|
||||
|
||||
|
||||
def get_register_router(
|
||||
@@ -25,6 +25,33 @@ def get_register_router(
|
||||
response_model=user_model,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
name="register:register",
|
||||
responses={
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
ErrorCode.REGISTER_USER_ALREADY_EXISTS: {
|
||||
"summary": "A user with this email already exists.",
|
||||
"value": {
|
||||
"detail": ErrorCode.REGISTER_USER_ALREADY_EXISTS
|
||||
},
|
||||
},
|
||||
ErrorCode.REGISTER_INVALID_PASSWORD: {
|
||||
"summary": "Password validation failed.",
|
||||
"value": {
|
||||
"detail": {
|
||||
"code": ErrorCode.REGISTER_INVALID_PASSWORD,
|
||||
"reason": "Password should be"
|
||||
"at least 3 characters",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
async def register(
|
||||
request: Request,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Request, status
|
||||
from pydantic import EmailStr
|
||||
|
||||
@@ -10,7 +12,32 @@ from fastapi_users.manager import (
|
||||
UserManagerDependency,
|
||||
UserNotExists,
|
||||
)
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.common import ErrorCode, ErrorModel
|
||||
|
||||
RESET_PASSWORD_RESPONSES: Dict[int, Dict[str, Any]] = {
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
ErrorCode.RESET_PASSWORD_BAD_TOKEN: {
|
||||
"summary": "Bad or expired token.",
|
||||
"value": {"detail": ErrorCode.RESET_PASSWORD_BAD_TOKEN},
|
||||
},
|
||||
ErrorCode.RESET_PASSWORD_INVALID_PASSWORD: {
|
||||
"summary": "Password validation failed.",
|
||||
"value": {
|
||||
"detail": {
|
||||
"code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD,
|
||||
"reason": "Password should be at least 3 characters",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_reset_password_router(
|
||||
@@ -41,7 +68,11 @@ def get_reset_password_router(
|
||||
|
||||
return None
|
||||
|
||||
@router.post("/reset-password", name="reset:reset_password")
|
||||
@router.post(
|
||||
"/reset-password",
|
||||
name="reset:reset_password",
|
||||
responses=RESET_PASSWORD_RESPONSES,
|
||||
)
|
||||
async def reset_password(
|
||||
request: Request,
|
||||
token: str = Body(...),
|
||||
|
||||
@@ -12,7 +12,7 @@ from fastapi_users.manager import (
|
||||
UserManagerDependency,
|
||||
UserNotExists,
|
||||
)
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.common import ErrorCode, ErrorModel
|
||||
|
||||
|
||||
def get_users_router(
|
||||
@@ -42,7 +42,16 @@ def get_users_router(
|
||||
except UserNotExists:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@router.get("/me", response_model=user_model, name="users:current_user")
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=user_model,
|
||||
name="users:current_user",
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user.",
|
||||
},
|
||||
},
|
||||
)
|
||||
async def me(
|
||||
user: user_db_model = Depends(get_current_active_user), # type: ignore
|
||||
):
|
||||
@@ -53,6 +62,36 @@ def get_users_router(
|
||||
response_model=user_model,
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
name="users:current_user",
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user.",
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS: {
|
||||
"summary": "A user with this email already exists.",
|
||||
"value": {
|
||||
"detail": ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS
|
||||
},
|
||||
},
|
||||
ErrorCode.UPDATE_USER_INVALID_PASSWORD: {
|
||||
"summary": "Password validation failed.",
|
||||
"value": {
|
||||
"detail": {
|
||||
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
||||
"reason": "Password should be"
|
||||
"at least 3 characters",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
async def update_me(
|
||||
request: Request,
|
||||
@@ -83,6 +122,17 @@ def get_users_router(
|
||||
response_model=user_model,
|
||||
dependencies=[Depends(get_current_superuser)],
|
||||
name="users:user",
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user.",
|
||||
},
|
||||
status.HTTP_403_FORBIDDEN: {
|
||||
"description": "Not a superuser.",
|
||||
},
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"description": "The user does not exist.",
|
||||
},
|
||||
},
|
||||
)
|
||||
async def get_user(user=Depends(get_user_or_404)):
|
||||
return user
|
||||
@@ -92,6 +142,42 @@ def get_users_router(
|
||||
response_model=user_model,
|
||||
dependencies=[Depends(get_current_superuser)],
|
||||
name="users:user",
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user.",
|
||||
},
|
||||
status.HTTP_403_FORBIDDEN: {
|
||||
"description": "Not a superuser.",
|
||||
},
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"description": "The user does not exist.",
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS: {
|
||||
"summary": "A user with this email already exists.",
|
||||
"value": {
|
||||
"detail": ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS
|
||||
},
|
||||
},
|
||||
ErrorCode.UPDATE_USER_INVALID_PASSWORD: {
|
||||
"summary": "Password validation failed.",
|
||||
"value": {
|
||||
"detail": {
|
||||
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
||||
"reason": "Password should be"
|
||||
"at least 3 characters",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
async def update_user(
|
||||
user_update: user_update_model, # type: ignore
|
||||
@@ -123,6 +209,17 @@ def get_users_router(
|
||||
response_class=Response,
|
||||
dependencies=[Depends(get_current_superuser)],
|
||||
name="users:user",
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Missing token or inactive user.",
|
||||
},
|
||||
status.HTTP_403_FORBIDDEN: {
|
||||
"description": "Not a superuser.",
|
||||
},
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"description": "The user does not exist.",
|
||||
},
|
||||
},
|
||||
)
|
||||
async def delete_user(
|
||||
user=Depends(get_user_or_404),
|
||||
|
||||
@@ -12,7 +12,7 @@ from fastapi_users.manager import (
|
||||
UserManagerDependency,
|
||||
UserNotExists,
|
||||
)
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.common import ErrorCode, ErrorModel
|
||||
|
||||
|
||||
def get_verify_router(
|
||||
@@ -39,7 +39,33 @@ def get_verify_router(
|
||||
|
||||
return None
|
||||
|
||||
@router.post("/verify", response_model=user_model, name="verify:verify")
|
||||
@router.post(
|
||||
"/verify",
|
||||
response_model=user_model,
|
||||
name="verify:verify",
|
||||
responses={
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorModel,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"examples": {
|
||||
ErrorCode.VERIFY_USER_BAD_TOKEN: {
|
||||
"summary": "Bad token, not existing user or"
|
||||
"not the e-mail currently set for the user.",
|
||||
"value": {"detail": ErrorCode.VERIFY_USER_BAD_TOKEN},
|
||||
},
|
||||
ErrorCode.VERIFY_USER_ALREADY_VERIFIED: {
|
||||
"summary": "The user is already verified.",
|
||||
"value": {
|
||||
"detail": ErrorCode.VERIFY_USER_ALREADY_VERIFIED
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
async def verify(
|
||||
request: Request,
|
||||
token: str = Body(..., embed=True),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
from typing import Any, AsyncGenerator, Callable, Generic, Optional, Type, Union
|
||||
from typing import Any, AsyncGenerator, Callable, Dict, Generic, Optional, Type, Union
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
@@ -462,6 +462,14 @@ class MockAuthentication(BaseAuthentication[str, UserCreate, UserDB]):
|
||||
):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_login_responses_success() -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_logout_responses_success() -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_authentication():
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
from typing import AsyncGenerator, Callable, Generic, List, Optional, Sequence
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
Callable,
|
||||
Dict,
|
||||
Generic,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -34,6 +43,14 @@ class BackendNone(
|
||||
) -> Optional[models.UD]:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_login_responses_success() -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_logout_responses_success() -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
class BackendUser(
|
||||
Generic[models.UC, models.UD], BaseAuthentication[str, models.UC, models.UD]
|
||||
@@ -50,6 +67,14 @@ class BackendUser(
|
||||
) -> Optional[models.UD]:
|
||||
return self.user
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_login_responses_success() -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_openapi_logout_responses_success() -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -29,3 +29,15 @@ async def test_get_login_response(base_authentication, user, user_manager):
|
||||
async def test_get_logout_response(base_authentication, user, user_manager):
|
||||
with pytest.raises(NotImplementedError):
|
||||
await base_authentication.get_logout_response(user, Response(), user_manager)
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
def test_get_login_response_success(base_authentication, user, user_manager):
|
||||
with pytest.raises(NotImplementedError):
|
||||
base_authentication.get_openapi_login_responses_success()
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
def test_get_logout_response_success(base_authentication, user, user_manager):
|
||||
with pytest.raises(NotImplementedError):
|
||||
base_authentication.get_openapi_logout_responses_success()
|
||||
|
||||
@@ -77,12 +77,12 @@ async def test_get_login_response(jwt_authentication, user, user_manager):
|
||||
user, Response(), user_manager
|
||||
)
|
||||
|
||||
assert "access_token" in login_response
|
||||
assert login_response["token_type"] == "bearer"
|
||||
assert login_response.token_type == "bearer"
|
||||
|
||||
token = login_response["access_token"]
|
||||
decoded = decode_jwt(
|
||||
token, jwt_authentication.secret, audience=["fastapi-users:auth"]
|
||||
login_response.access_token,
|
||||
jwt_authentication.secret,
|
||||
audience=["fastapi-users:auth"],
|
||||
)
|
||||
assert decoded["user_id"] == str(user.id)
|
||||
|
||||
@@ -92,3 +92,10 @@ async def test_get_login_response(jwt_authentication, user, user_manager):
|
||||
async def test_get_logout_response(jwt_authentication, user, user_manager):
|
||||
with pytest.raises(NotImplementedError):
|
||||
await jwt_authentication.get_logout_response(user, Response(), user_manager)
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_logout_response_success(jwt_authentication, user, user_manager):
|
||||
with pytest.raises(NotImplementedError):
|
||||
await jwt_authentication.get_openapi_logout_responses_success()
|
||||
|
||||
136
tests/test_openapi.py
Normal file
136
tests/test_openapi.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from httpx_oauth.clients.google import GoogleOAuth2
|
||||
|
||||
import fastapi_users.authentication
|
||||
from fastapi_users import models
|
||||
from fastapi_users.fastapi_users import FastAPIUsers
|
||||
|
||||
app = FastAPI()
|
||||
jwt_authentication = fastapi_users.authentication.JWTAuthentication(
|
||||
secret="", lifetime_seconds=3600
|
||||
)
|
||||
cookie_authentication = fastapi_users.authentication.CookieAuthentication(secret="")
|
||||
users = FastAPIUsers(
|
||||
# dummy get_user_manager
|
||||
lambda: None,
|
||||
[cookie_authentication, jwt_authentication],
|
||||
models.BaseUser,
|
||||
models.BaseUserCreate,
|
||||
models.BaseUserUpdate,
|
||||
models.BaseUserDB,
|
||||
)
|
||||
app.include_router(users.get_verify_router())
|
||||
app.include_router(users.get_register_router())
|
||||
app.include_router(users.get_users_router())
|
||||
app.include_router(users.get_reset_password_router())
|
||||
app.include_router(
|
||||
users.get_oauth_router(
|
||||
GoogleOAuth2(client_id="1234", client_secret="4321"), state_secret="secret"
|
||||
)
|
||||
)
|
||||
app.include_router(users.get_auth_router(jwt_authentication), prefix="/jwt")
|
||||
app.include_router(users.get_auth_router(cookie_authentication), prefix="/cookie")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def get_openapi_dict():
|
||||
return app.openapi()
|
||||
|
||||
|
||||
def test_openapi_generated_ok():
|
||||
assert TestClient(app).get("/openapi.json").status_code == 200
|
||||
|
||||
|
||||
class TestLogin:
|
||||
def test_jwt_login_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/jwt/login"]["post"]
|
||||
assert list(route["responses"].keys()) == ["200", "400", "422"]
|
||||
|
||||
def test_jwt_login_200_body(self, get_openapi_dict):
|
||||
"""Check if example is up to date."""
|
||||
example = get_openapi_dict["paths"]["/jwt/login"]["post"]["responses"]["200"][
|
||||
"content"
|
||||
]["application/json"]["example"]
|
||||
assert (
|
||||
example.keys()
|
||||
== fastapi_users.authentication.jwt.JWTLoginResponse.schema()[
|
||||
"properties"
|
||||
].keys()
|
||||
)
|
||||
|
||||
def test_cookie_login_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/cookie/login"]["post"]
|
||||
assert ["200", "400", "422"] == list(route["responses"].keys())
|
||||
|
||||
|
||||
class TestLogout:
|
||||
def test_cookie_logout_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/cookie/logout"]["post"]
|
||||
assert list(route["responses"].keys()) == ["200", "401"]
|
||||
|
||||
|
||||
class TestReset:
|
||||
def test_reset_password_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/reset-password"]["post"]
|
||||
assert list(route["responses"].keys()) == ["200", "400", "422"]
|
||||
|
||||
def test_forgot_password_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/forgot-password"]["post"]
|
||||
assert list(route["responses"].keys()) == ["202", "422"]
|
||||
|
||||
|
||||
class TestUsers:
|
||||
def test_patch_id_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/{id}"]["patch"]
|
||||
assert list(route["responses"].keys()) == [
|
||||
"200",
|
||||
"401",
|
||||
"403",
|
||||
"404",
|
||||
"400",
|
||||
"422",
|
||||
]
|
||||
|
||||
def test_delete_id_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/{id}"]["delete"]
|
||||
assert list(route["responses"].keys()) == ["204", "401", "403", "404", "422"]
|
||||
|
||||
def test_get_id_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/{id}"]["get"]
|
||||
assert list(route["responses"].keys()) == ["200", "401", "403", "404", "422"]
|
||||
|
||||
def test_patch_me_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/me"]["patch"]
|
||||
assert list(route["responses"].keys()) == ["200", "401", "400", "422"]
|
||||
|
||||
def test_get_me_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/me"]["get"]
|
||||
assert list(route["responses"].keys()) == ["200", "401"]
|
||||
|
||||
|
||||
class TestRegister:
|
||||
def test_register_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/register"]["post"]
|
||||
assert list(route["responses"].keys()) == ["201", "400", "422"]
|
||||
|
||||
|
||||
class TestVerify:
|
||||
def test_verify_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/verify"]["post"]
|
||||
assert list(route["responses"].keys()) == ["200", "400", "422"]
|
||||
|
||||
def test_request_verify_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/request-verify-token"]["post"]
|
||||
assert list(route["responses"].keys()) == ["202", "422"]
|
||||
|
||||
|
||||
class TestOAuth2:
|
||||
def test_google_authorize_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/authorize"]["get"]
|
||||
assert list(route["responses"].keys()) == ["200", "422"]
|
||||
|
||||
def test_google_callback_status_codes(self, get_openapi_dict):
|
||||
route = get_openapi_dict["paths"]["/callback"]["get"]
|
||||
assert list(route["responses"].keys()) == ["200", "400", "422"]
|
||||
Reference in New Issue
Block a user