diff --git a/app/exceptions.py b/app/exceptions.py index 343d77d..98adb7f 100644 --- a/app/exceptions.py +++ b/app/exceptions.py @@ -25,6 +25,11 @@ class IncorrectAuthDataException(BlackPhoenixException): detail = "Введены не верные данные" +class IncorrectPasswordException(BlackPhoenixException): + status_code = status.HTTP_401_UNAUTHORIZED + detail = "Введён не верные пароль" + + class IncorrectTokenFormatException(BlackPhoenixException): status_code = status.HTTP_401_UNAUTHORIZED detail = "Некорректный формат токена" diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 25fc77b..d450db6 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -14,7 +14,7 @@ from app.users.chat.models import Chats, UsersXChats, Messages from app.main import app as fastapi_app -@pytest.fixture(autouse=True, scope='session') +@pytest.fixture(autouse=True, scope='module') async def prepare_database(): assert settings.MODE == "TEST" diff --git a/app/tests/integration_tests/api_test.py b/app/tests/integration_tests/api_test.py deleted file mode 100644 index f0a0c22..0000000 --- a/app/tests/integration_tests/api_test.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest -from httpx import AsyncClient - - -@pytest.mark.parametrize("email,username,password,date_of_birth,status_code", [ - ("sosi@lox.com", "sobakasutulaya", "12311231", "1990-01-01", 200), - ("sosi@lox.com", "sobakasutulaya", "12311231", "1990-01-01", 409), - ("lox@sosi.com", "sobakasutulaya", "12311231", "1990-01-01", 409), - ("lox@sosi.com", "sobakastroinaya", "3228", "1990-01-01", 422), - ("lox@sosi.com", "sobakastroinaya", "asdwdawdasd", "2030-01-01", 422), - ("lox@sosi.com", "sobakastroinaya", "asdwdawdasd", "2000-01-01", 200), - -]) -async def test_register_user(email, username, password, date_of_birth, status_code, ac: AsyncClient): - response = await ac.post("/users/register", json={ - "email": email, - "username": username, - "password": password, - "date_of_birth": date_of_birth - }) - - assert response.status_code == status_code - print(response) diff --git a/app/tests/integration_tests/users_api_test.py b/app/tests/integration_tests/users_api_test.py new file mode 100644 index 0000000..1ec5cb3 --- /dev/null +++ b/app/tests/integration_tests/users_api_test.py @@ -0,0 +1,65 @@ +import pytest +from httpx import AsyncClient + + +async def test_get_users(ac: AsyncClient): + response = await ac.get("/users") + assert response.status_code == 200 + assert type(response.json()) == list + assert len(response.json()) == 3 + + +@pytest.mark.parametrize("email,username,password,date_of_birth,status_code", [ + ("sosi@lox.com", "sobakasutulaya", "12311231", "1990-01-01", 200), + ("sosi@lox.com", "sobakasutulaya", "12311231", "1990-01-01", 409), + ("lox@sosi.com", "sobakasutulaya", "12311231", "1990-01-01", 409), + ("lox@sosi.com", "sobakastroinaya", "3228", "1990-01-01", 422), + ("lox@sosi.com", "sobakastroinaya", "asdwdawdasd", "2030-01-01", 422), + ("lox@sosi.com", "sobakastroinaya", "asdwdawdasd", "2000-01-01", 200), +]) +async def test_register_user(email, username, password, date_of_birth, status_code, ac: AsyncClient): + response = await ac.post("/users/register", json={ + "email": email, + "username": username, + "password": password, + "date_of_birth": date_of_birth + }) + + assert response.status_code == status_code + + +@pytest.mark.parametrize("email_or_username,password,status_code", [ + ("sosi@lox.com", "12311231", 200), + ("sobakasutulaya", "12311231", 200), + ("sobakastroinaya", "12311231", 401), + ("sosi@lox.com", "123123", 401), + ("urec@urec.com", "12311231", 200), + ("sobakasutulaya", 12311231, 422), + (12311231, "sobakasutulaya", 422), +]) +async def test_login_user(email_or_username: str, password: str, status_code: int, ac: AsyncClient): + response = await ac.post("/users/login", json={ + "email_or_username": email_or_username, + "password": password + }) + assert response.status_code == status_code + if status_code == 200: + assert "access_token" in response.json() + assert "black_phoenix_access_token" in response.cookies + + +async def test_get_user(ac: AsyncClient): + await ac.post("/users/login", json={ + "email_or_username": "urec@urec.com", + "password": "12311231" + }) + response = await ac.get("/users/me") + assert response.status_code == 200 + assert response.json()["email"] == "urec@urec.com" + assert response.json()["black_phoenix"] == False + + +async def test_logout_user(ac: AsyncClient): + response = await ac.post("/users/logout") + assert response.status_code == 200 + assert "black_phoenix_access_token" not in response.cookies diff --git a/app/users/router.py b/app/users/router.py index 22dcea1..eb17eb4 100644 --- a/app/users/router.py +++ b/app/users/router.py @@ -1,4 +1,6 @@ -from fastapi import APIRouter, Response, Depends, UploadFile +from typing import Annotated + +from fastapi import APIRouter, Response, Depends, UploadFile, File, Body, Form from fastapi.responses import RedirectResponse from starlette import status @@ -12,7 +14,7 @@ from app.users.auth import get_password_hash, authenticate_user_by_email, \ from app.users.dao import UserDAO, UserCodesDAO from app.users.dependencies import get_current_user from app.users.models import Users -from app.users.schemas import SUserLogin, SUserRegister, SUser, SUserName, SUserPassword +from app.users.schemas import SUserLogin, SUserRegister, SUser, SUserPassword, SUserRename, SUserAvatar from app.tasks.tasks import send_registration_confirmation_email router = APIRouter( @@ -82,7 +84,9 @@ async def read_users_me(current_user: Users = Depends(get_current_user)): @router.patch("/rename", response_model=str) -async def rename_user(new_username: SUserName, current_user: Users = Depends(get_current_user)): +async def rename_user(new_username: SUserRename, current_user: Users = Depends(get_current_user)): + if not verify_password(new_username.password, current_user.hashed_password): + raise IncorrectPasswordException existing_user = await UserDAO.find_one_or_none(username=new_username.username) if existing_user: raise UsernameAlreadyInUseException @@ -91,7 +95,13 @@ async def rename_user(new_username: SUserName, current_user: Users = Depends(get @router.patch("/change_avatar", response_model=str) -async def change_avatar(new_avatar: UploadFile, current_user: Users = Depends(get_current_user)): +async def change_avatar( + new_avatar: Annotated[UploadFile, File()], + password: Annotated[SUserAvatar, Body()], + current_user: Users = Depends(get_current_user) +): + if not verify_password(password.password, current_user.hashed_password): + raise IncorrectPasswordException new_avatar_url = await upload_file_returning_str(new_avatar, "upload_avatar") if await UserDAO.change_data(current_user.id, avatar_image=new_avatar_url): return new_avatar_url diff --git a/app/users/schemas.py b/app/users/schemas.py index a8e820d..b5b7772 100644 --- a/app/users/schemas.py +++ b/app/users/schemas.py @@ -1,7 +1,8 @@ +import json from datetime import date, timedelta from pydantic_core import PydanticCustomError -from pydantic import BaseModel, EmailStr, ConfigDict, field_validator +from pydantic import BaseModel, EmailStr, ConfigDict, field_validator, model_validator from fastapi import Query @@ -20,7 +21,7 @@ class SUserRegister(BaseModel): @classmethod def validate_date_of_birth(cls, input_date): if date.today() - input_date < timedelta(days=365 * 16): - date_of_birth = date.today() - timedelta(days=365*16) + date_of_birth = date.today() - timedelta(days=365 * 16) raise PydanticCustomError( "date_input_error", "date of birth might be earlier than {date_of_birth}", @@ -41,8 +42,20 @@ class SUser(BaseModel): date_of_registration: date -class SUserName(BaseModel): +class SUserRename(BaseModel): username: str = Query(None, min_length=2, max_length=30) + password: str + + +class SUserAvatar(BaseModel): + password: str + + @model_validator(mode="before") + @classmethod + def auto_loads_json_string(cls, data): + if isinstance(data, str) and data.startswith("{"): + data = json.loads(data) + return data class SUserPassword(BaseModel):