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:
Matyáš Richter
2021-11-23 13:13:51 +01:00
committed by GitHub
parent 1f5ce51df2
commit c759bb6915
16 changed files with 546 additions and 35 deletions

View File

@@ -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`.

View File

@@ -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()

View File

@@ -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)

View File

@@ -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}

View File

@@ -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),

View File

@@ -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"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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(...),

View File

@@ -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),

View File

@@ -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),

View File

@@ -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():

View File

@@ -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

View File

@@ -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()

View File

@@ -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
View 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"]