diff --git a/docs/usage/routes.md b/docs/usage/routes.md index 6f4c8f51..dc368205 100644 --- a/docs/usage/routes.md +++ b/docs/usage/routes.md @@ -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`. diff --git a/fastapi_users/authentication/base.py b/fastapi_users/authentication/base.py index 38c0e9fc..ed649591 100644 --- a/fastapi_users/authentication/base.py +++ b/fastapi_users/authentication/base.py @@ -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() diff --git a/fastapi_users/authentication/cookie.py b/fastapi_users/authentication/cookie.py index 9e316171..738e2de8 100644 --- a/fastapi_users/authentication/cookie.py +++ b/fastapi_users/authentication/cookie.py @@ -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) diff --git a/fastapi_users/authentication/jwt.py b/fastapi_users/authentication/jwt.py index 7bc6988a..a9fff0b7 100644 --- a/fastapi_users/authentication/jwt.py +++ b/fastapi_users/authentication/jwt.py @@ -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} diff --git a/fastapi_users/router/auth.py b/fastapi_users/router/auth.py index 6a62c854..95199f73 100644 --- a/fastapi_users/router/auth.py +++ b/fastapi_users/router/auth.py @@ -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), diff --git a/fastapi_users/router/common.py b/fastapi_users/router/common.py index 4323e4a6..577e79a6 100644 --- a/fastapi_users/router/common.py +++ b/fastapi_users/router/common.py @@ -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" diff --git a/fastapi_users/router/oauth.py b/fastapi_users/router/oauth.py index 1553635d..e15d297c 100644 --- a/fastapi_users/router/oauth.py +++ b/fastapi_users/router/oauth.py @@ -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, diff --git a/fastapi_users/router/register.py b/fastapi_users/router/register.py index b20093c5..567d1a3c 100644 --- a/fastapi_users/router/register.py +++ b/fastapi_users/router/register.py @@ -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, diff --git a/fastapi_users/router/reset.py b/fastapi_users/router/reset.py index fe55364d..e6ebc15a 100644 --- a/fastapi_users/router/reset.py +++ b/fastapi_users/router/reset.py @@ -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(...), diff --git a/fastapi_users/router/users.py b/fastapi_users/router/users.py index 2c3142b2..1e5e5e2b 100644 --- a/fastapi_users/router/users.py +++ b/fastapi_users/router/users.py @@ -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), diff --git a/fastapi_users/router/verify.py b/fastapi_users/router/verify.py index 5eebc53a..d4046040 100644 --- a/fastapi_users/router/verify.py +++ b/fastapi_users/router/verify.py @@ -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), diff --git a/tests/conftest.py b/tests/conftest.py index f7da369b..fd323784 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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(): diff --git a/tests/test_authentication.py b/tests/test_authentication.py index d8ddf9e0..78bdb7db 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -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 diff --git a/tests/test_authentication_base.py b/tests/test_authentication_base.py index fdb0649e..f8de184d 100644 --- a/tests/test_authentication_base.py +++ b/tests/test_authentication_base.py @@ -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() diff --git a/tests/test_authentication_jwt.py b/tests/test_authentication_jwt.py index 89422d9e..8bc97614 100644 --- a/tests/test_authentication_jwt.py +++ b/tests/test_authentication_jwt.py @@ -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() diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 00000000..921d4627 --- /dev/null +++ b/tests/test_openapi.py @@ -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"]