From 658161518a7bc2e27cedabfba7ed6bb678b5604c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Thu, 31 Oct 2019 10:10:53 +0100 Subject: [PATCH] Add error codes on routes (#34) Fix #33 --- docs/usage/routes.md | 18 ++++++++++++++ fastapi_users/router.py | 41 +++++++++++++++++++++++--------- tests/test_authentication_jwt.py | 1 - tests/test_router.py | 10 ++++++-- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/docs/usage/routes.md b/docs/usage/routes.md index 34fcad3b..896097df 100644 --- a/docs/usage/routes.md +++ b/docs/usage/routes.md @@ -31,6 +31,12 @@ Register a new user. Will call the `on_after_register` [event handlers](../confi !!! fail "`400 Bad Request`" A user already exists with this email. + ```json + { + "detail": "REGISTER_USER_ALREADY_EXISTS" + } + ``` + ### `POST /login` Login a user. @@ -52,6 +58,12 @@ Login a user. !!! fail "`400 Bad Request`" Bad credentials or the user is inactive. + ```json + { + "detail": "LOGIN_BAD_CREDENTIALS" + } + ``` + ### `POST /forgot-password` Request a reset password procedure. Will generate a temporary token and call the `on_after_forgot_password` [event handlers](../configuration/router.md#event-handlers) if the user exists. @@ -86,6 +98,12 @@ Reset a password. Requires the token generated by the `/forgot-password` route. !!! fail "`400 Bad Request`" Bad or expired token. + ```json + { + "detail": "RESET_PASSWORD_BAD_TOKEN" + } + ``` + ## Authenticated ### `GET /me` diff --git a/fastapi_users/router.py b/fastapi_users/router.py index 8d17344f..8db4211e 100644 --- a/fastapi_users/router.py +++ b/fastapi_users/router.py @@ -1,7 +1,7 @@ import asyncio import typing from collections import defaultdict -from enum import Enum +from enum import Enum, auto import jwt from fastapi import APIRouter, Body, Depends, HTTPException @@ -17,9 +17,15 @@ from fastapi_users.password import get_password_hash from fastapi_users.utils import JWT_ALGORITHM, generate_jwt +class ErrorCode: + REGISTER_USER_ALREADY_EXISTS = "REGISTER_USER_ALREADY_EXISTS" + LOGIN_BAD_CREDENTIALS = "LOGIN_BAD_CREDENTIALS" + RESET_PASSWORD_BAD_TOKEN = "RESET_PASSWORD_BAD_TOKEN" + + class Event(Enum): - ON_AFTER_REGISTER = 1 - ON_AFTER_FORGOT_PASSWORD = 2 + ON_AFTER_REGISTER = auto() + ON_AFTER_FORGOT_PASSWORD = auto() class UserRouter(APIRouter): @@ -80,7 +86,10 @@ def get_user_router( existing_user = await user_db.get_by_email(user.email) if existing_user is not None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS, + ) hashed_password = get_password_hash(user.password) db_user = models.UserDB( @@ -98,10 +107,11 @@ def get_user_router( ): user = await user_db.authenticate(credentials) - if user is None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) - elif not user.is_active: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + if user is None or not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.LOGIN_BAD_CREDENTIALS, + ) return await auth.get_login_response(user, response) @@ -131,16 +141,25 @@ def get_user_router( ) user_id = data.get("user_id") if user_id is None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN, + ) user = await user_db.get(user_id) if user is None or not user.is_active: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN, + ) user.hashed_password = get_password_hash(password) await user_db.update(user) except jwt.PyJWTError: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN, + ) @router.get("/me", response_model=models.User) async def me( diff --git a/tests/test_authentication_jwt.py b/tests/test_authentication_jwt.py index bee80de9..b4f3f700 100644 --- a/tests/test_authentication_jwt.py +++ b/tests/test_authentication_jwt.py @@ -47,7 +47,6 @@ async def test_get_login_response(jwt_authentication, user): class TestGetCurrentUser: def test_missing_token(self, test_auth_client): response = test_auth_client.get("/test-current-user") - print(response.json()) assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_invalid_token(self, test_auth_client): diff --git a/tests/test_router.py b/tests/test_router.py index 375fae99..ed9220be 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -8,7 +8,7 @@ from starlette import status from starlette.testclient import TestClient from fastapi_users.models import BaseUser, BaseUserDB -from fastapi_users.router import Event, get_user_router +from fastapi_users.router import ErrorCode, Event, get_user_router from fastapi_users.utils import JWT_ALGORITHM, generate_jwt SECRET = "SECRET" @@ -79,6 +79,7 @@ class TestRegister: json = {"email": "king.arthur@camelot.bt", "password": "guinevere"} response = test_app_client.post("/register", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == ErrorCode.REGISTER_USER_ALREADY_EXISTS assert event_handler.called is False def test_valid_body(self, test_app_client: TestClient, event_handler): @@ -141,11 +142,13 @@ class TestLogin: data = {"username": "lancelot@camelot.bt", "password": "guinevere"} response = test_app_client.post("/login", data=data) assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS def test_wrong_password(self, test_app_client: TestClient): data = {"username": "king.arthur@camelot.bt", "password": "percival"} response = test_app_client.post("/login", data=data) assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS def test_valid_credentials(self, test_app_client: TestClient, user: BaseUserDB): data = {"username": "king.arthur@camelot.bt", "password": "guinevere"} @@ -157,6 +160,7 @@ class TestLogin: data = {"username": "percival@camelot.bt", "password": "angharad"} response = test_app_client.post("/login", data=data) assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS class TestForgotPassword: @@ -213,8 +217,8 @@ class TestResetPassword: def test_invalid_token(self, test_app_client: TestClient): json = {"token": "foo", "password": "guinevere"} response = test_app_client.post("/reset-password", json=json) - print(response.json(), response.status_code) assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN def test_valid_token_missing_user_id_payload( self, mocker, mock_user_db, test_app_client: TestClient, forgot_password_token @@ -224,6 +228,7 @@ class TestResetPassword: json = {"token": forgot_password_token(), "password": "holygrail"} response = test_app_client.post("/reset-password", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN assert mock_user_db.update.called is False def test_inactive_user( @@ -242,6 +247,7 @@ class TestResetPassword: } response = test_app_client.post("/reset-password", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN assert mock_user_db.update.called is False def test_existing_user(