From dc1c710ba7c16831e0e8420902d6d32693185266 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Sat, 20 Jan 2024 22:07:09 +0700 Subject: [PATCH 01/44] resolve profile.py --- app/api/endpoints/profile.py | 43 ------------------------------------ 1 file changed, 43 deletions(-) diff --git a/app/api/endpoints/profile.py b/app/api/endpoints/profile.py index 52f6564..44c9f27 100644 --- a/app/api/endpoints/profile.py +++ b/app/api/endpoints/profile.py @@ -1,6 +1,5 @@ import re -<<<<<<< HEAD from fastapi import (APIRouter, Depends, File, HTTPException, Response, UploadFile, status) from fastapi_filter import FilterDepends @@ -15,22 +14,10 @@ from app.services.utils import (Pagination, add_response_headers, create_filename, get_pagination_params, paginated, remove_content, save_content) -======= -from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.db import get_async_session -from app.core.user import current_user -from app.crud import profile_crud -from app.models import Profile, User -from app.schemas.profile import ProfileRead, ProfileUpdate -from app.services.utils import create_filename, remove_content, save_content ->>>>>>> feat/test_auth router = APIRouter() -<<<<<<< HEAD @router.get('/', response_model=list[ProfileRead], response_model_exclude_none=True, dependencies=[Depends(current_superuser)]) @@ -51,13 +38,6 @@ async def get_all_profiles( @router.get('/me', response_model=ProfileRead, response_model_exclude_none=True) -======= -@router.get( - '/', - response_model=ProfileRead, - dependencies=[Depends(current_user)] -) ->>>>>>> feat/test_auth async def get_current_user_profile( session: AsyncSession = Depends(get_async_session), user: User = Depends(current_user) @@ -69,14 +49,7 @@ async def get_current_user_profile( ) -<<<<<<< HEAD @router.get('/me/photo') -======= -@router.get( - '/photo', - dependencies=[Depends(current_user)] -) ->>>>>>> feat/test_auth async def get_user_photo( user: User = Depends(current_user), session: AsyncSession = Depends(get_async_session) @@ -85,15 +58,7 @@ async def get_user_photo( return await profile_crud.get_user_photo(user.id, session) -<<<<<<< HEAD @router.patch('/me', response_model=ProfileRead) -======= -@router.patch( - '/', - response_model=ProfileRead, - dependencies=[Depends(current_user)] -) ->>>>>>> feat/test_auth async def update_profile( profile: ProfileUpdate, user: User = Depends(current_user), @@ -103,15 +68,7 @@ async def update_profile( return await profile_crud.update(_profile, profile, session) -<<<<<<< HEAD @router.patch('/me/update_photo', response_model=ProfileRead) -======= -@router.patch( - '/update_photo', - response_model=ProfileRead, - dependencies=[Depends(current_user)] -) ->>>>>>> feat/test_auth async def update_photo( file: UploadFile = File(...), user: User = Depends(current_user), From ba91414c9b3c700b9e3491feb20834020a49f8b0 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Sun, 21 Jan 2024 04:54:18 +0700 Subject: [PATCH 02/44] tests profile --- app/models/profile.py | 4 +- tests/fixtures/user.py | 35 +++++++++++++ tests/test_profile.py | 116 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 tests/test_profile.py diff --git a/app/models/profile.py b/app/models/profile.py index 5adedfa..f8688d5 100644 --- a/app/models/profile.py +++ b/app/models/profile.py @@ -48,4 +48,6 @@ class Profile(Base): ) def __repr__(self): - return self.first_name + if self.first_name: + return self.first_name + return str(self.user_id) diff --git a/tests/fixtures/user.py b/tests/fixtures/user.py index 6a1392c..abffedf 100644 --- a/tests/fixtures/user.py +++ b/tests/fixtures/user.py @@ -59,3 +59,38 @@ async def auth_client( access_token = response.json().get('access_token') new_client.headers.update({'Authorization': f'Bearer {access_token}'}) yield new_client + + +@pytest_asyncio.fixture +async def superuser( + prepare_database: FastAPI, + db_session: AsyncSessionLocalTest +): + """Фикстура суперюзера.""" + hashed_password = bcrypt.hash('admin') + superuser = User( + email='admin@admin.com', + hashed_password=hashed_password, + role='admin', + username='admin', + is_superuser=True + ) + db_session.add(superuser) + await db_session.commit() + await db_session.refresh(superuser) + yield superuser + + +@pytest_asyncio.fixture +async def auth_superuser( + new_client, + register_client +) -> AsyncGenerator | TestClient: + """Фикстура для суперюзера, вошедшего в систему.""" + response = new_client.post( + '/auth/jwt/login', + data={'username': 'admin@admin.com', 'password': 'admin'}) + assert response.status_code == status.HTTP_200_OK + access_token = response.json().get('access_token') + new_client.headers.update({'Authorization': f'Bearer {access_token}'}) + yield new_client diff --git a/tests/test_profile.py b/tests/test_profile.py new file mode 100644 index 0000000..baa8eb6 --- /dev/null +++ b/tests/test_profile.py @@ -0,0 +1,116 @@ +from typing import AsyncGenerator + +from fastapi import status, Response +from fastapi.testclient import TestClient + +from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME + +from app.crud.profile import profile_crud +from app.crud.user import user_crud + +REGISTRATION_SCHEMA = { + 'email': USER_EMAIL, + 'password': USER_PASSWORD, + 'role': 'user', + 'username': USER_USERNAME, +} + + +class TestCreateProfile: + async def test_create_profile_with_create_user( + self, new_client, db_session + ): + """Тест создания профиля при регистрации пользователя.""" + profiles = await profile_crud.get_multi(db_session) + assert len(profiles) == 0 + users = await user_crud.get_multi(db_session) + assert len(users) == 0 + response: Response = new_client.post( + '/auth/register', json=REGISTRATION_SCHEMA + ) + assert response.status_code == status.HTTP_201_CREATED + profiles = await profile_crud.get_multi(db_session) + print(profiles) + assert len(profiles) == 1 + users = await user_crud.get_multi(db_session) + assert len(users) == 1 + + +class TestSuperuser: + async def test_get_all_profiles_superuser( + self, + db_session: AsyncGenerator, + superuser: AsyncGenerator, + # new_client: AsyncGenerator | TestClient, + auth_superuser: AsyncGenerator | TestClient + ): + """Тест получения всех профилей суперюзером.""" + # response = new_client.post( + # '/auth/jwt/login', + # data={'username': 'admin@admin.com', 'password': 'admin'}, + # ) + # response: Response = new_client.post( + # '/auth/register', json=REGISTRATION_SCHEMA + # ) + # print(response) + users = await user_crud.get_multi(db_session) + print(users) + profiles = await profile_crud.get_multi(db_session) + print(profiles) + response: Response = auth_superuser.get( + '/profiles/' + ) + print(response) + assert response.status_code == status.HTTP_200_OK + + async def test_3(self): + """Тест запрета получения профилей простым пользователем.""" + ... + + async def test_4(self): + """Тест фильтрации профилей.""" + ... + + async def test_5(self): + """Тест пагинации профилей""" + ... + + async def test_6(self): + """Тест получения своего профиля текущим юзером.""" + ... + + async def test_7(self): + """Тест запрета получения чужого профиля текущим юзером.""" + ... + + async def test_8(self): + """Тест получения фото своего профиля юзером.""" + ... + + async def test_9(self): + """Тест запрета получения фото чужого профиля.""" + ... + + async def test_10(self): + """Тест апдейта своего профиля.""" + ... + + async def test_11(self): + """Тест запрета апдейта чужого профиля.""" + ... + + async def test_12(self): + """Тест апдейта фото своего профиля.""" + ... + + async def test_13(self): + """Тест запрета апдейта фото чужого профиля.""" + ... + + async def test_14(self): + """Тест запрета создания профиля без создания юзера.""" + ... + + async def test_15(self): + """Тест запрета удаления профиля.""" + ... From fc7629d09822a670f9de30a7e650bed7bc9ba521 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Wed, 24 Jan 2024 21:35:34 +0700 Subject: [PATCH 03/44] fix gitignore, test.fixtures.user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В фикстуре auth_superuser заменил regicter_client на superuser --- .gitignore | 1 + tests/fixtures/user.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8901851..04bda06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ test.db +test.db-journal .vscode/ *.jpg diff --git a/tests/fixtures/user.py b/tests/fixtures/user.py index abffedf..8737abb 100644 --- a/tests/fixtures/user.py +++ b/tests/fixtures/user.py @@ -84,7 +84,7 @@ async def superuser( @pytest_asyncio.fixture async def auth_superuser( new_client, - register_client + superuser ) -> AsyncGenerator | TestClient: """Фикстура для суперюзера, вошедшего в систему.""" response = new_client.post( From 0b414f2dc972c82182d01286aa14e2db32b2d3b9 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 01:56:50 +0700 Subject: [PATCH 04/44] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=20=D1=81=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=B3=D0=B5=D1=80=D0=BD=D0=B5=D1=80=D0=B8=D1=82=205?= =?UTF-8?q?=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/profile.py | 2 +- tests/conftest.py | 1 + tests/fixtures/profile.py | 91 ++++++++++++++++++++++++++++++++++++ tests/fixtures/user.py | 23 ++++++++- tests/test_profile.py | 63 ++++++++++++++++--------- 5 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 tests/fixtures/profile.py diff --git a/app/api/endpoints/profile.py b/app/api/endpoints/profile.py index 44c9f27..b365c95 100644 --- a/app/api/endpoints/profile.py +++ b/app/api/endpoints/profile.py @@ -97,7 +97,7 @@ def create_profile(): ) -@router.delete('/{obj_id}', deprecated=True) +@router.delete('/{obj_id}', deprecated=True, status_code=405) def delete_profile(obg_id: str): """Удалить объект""" raise HTTPException( diff --git a/tests/conftest.py b/tests/conftest.py index e1b5cb1..4c8c647 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ pytest_plugins = [ 'tests.fixtures.user', + 'tests.fixtures.profile', ] engine_test = create_async_engine( diff --git a/tests/fixtures/profile.py b/tests/fixtures/profile.py new file mode 100644 index 0000000..56c2ee5 --- /dev/null +++ b/tests/fixtures/profile.py @@ -0,0 +1,91 @@ +from typing import AsyncGenerator + +import pytest_asyncio +from fastapi import FastAPI +from passlib.hash import bcrypt +from sqlalchemy import select + +from app.models import User, Profile +from tests.conftest import AsyncSessionLocalTest + + +@pytest_asyncio.fixture +async def moc_users( + db_session: AsyncSessionLocalTest +) -> AsyncGenerator: + """Фикстура заполнения базы юзерами с профилями.""" + hashed_password = bcrypt.hash('qwerty') + moc_users = [ + User( + email=f'user_{i}@example.com', + hashed_password=hashed_password, + role='user', + username=f'user_{i}' + ) for i in range(1, 6) + ] + db_session.add_all(moc_users) + await db_session.commit() + moc_users = await db_session.execute(select(User)) + moc_users = moc_users.scalars().all() + moc_profiles = [ + Profile( + first_name=f'name_{user.username}', + last_name=f'surname_{user.username}', + age=i + 20, + user_id=user.id + ) for i, user in enumerate(moc_users) + ] + db_session.add_all(moc_profiles) + await db_session.commit() + + +# @pytest_asyncio.fixture +# async def user_1( +# prepare_database: FastAPI, +# db_session: AsyncSessionLocalTest +# ) -> AsyncGenerator: +# """Фикстура зарегистрированного клиента.""" +# hashed_password = bcrypt.hash('qwerty') +# user_1 = User( +# email='user_1@example.com', +# hashed_password=hashed_password, +# role='user', +# username='user_1' +# ) +# db_session.add(user_1) +# await db_session.commit() +# await db_session.refresh(user_1) +# profile_1 = Profile( +# first_name='user_1_fn', +# last_name='user_1_ln', +# age=25, +# user_id=user_1.id +# ) +# db_session.add(profile_1) +# await db_session.commit() + + +# @pytest_asyncio.fixture +# async def user_2( +# prepare_database: FastAPI, +# db_session: AsyncSessionLocalTest +# ) -> AsyncGenerator: +# """Фикстура зарегистрированного клиента.""" +# hashed_password = bcrypt.hash('qwerty') +# user_2 = User( +# email='user_2@example.com', +# hashed_password=hashed_password, +# role='user', +# username='user_2' +# ) +# db_session.add(user_2) +# await db_session.commit() +# await db_session.refresh(user_2) +# profile_2 = Profile( +# first_name='user_2_fn', +# last_name='user_2_ln', +# age=47, +# user_id=user_2.id +# ) +# db_session.add(profile_2) +# await db_session.commit() diff --git a/tests/fixtures/user.py b/tests/fixtures/user.py index 8737abb..37e573e 100644 --- a/tests/fixtures/user.py +++ b/tests/fixtures/user.py @@ -63,7 +63,6 @@ async def auth_client( @pytest_asyncio.fixture async def superuser( - prepare_database: FastAPI, db_session: AsyncSessionLocalTest ): """Фикстура суперюзера.""" @@ -81,10 +80,30 @@ async def superuser( yield superuser +# @pytest_asyncio.fixture +# async def user_1( +# prepare_database: FastAPI, +# db_session: AsyncSessionLocalTest +# ) -> AsyncGenerator: +# """Фикстура зарегистрированного клиента.""" +# hashed_password = bcrypt.hash('qwerty') +# user_1 = User( +# email='user_1@example.com', +# hashed_password=hashed_password, +# role='user', +# username='user_1' +# ) +# db_session.add(user_1) +# await db_session.commit() +# await db_session.refresh(user_1) +# yield user_1 + + @pytest_asyncio.fixture async def auth_superuser( new_client, - superuser + superuser, + # user_1, ) -> AsyncGenerator | TestClient: """Фикстура для суперюзера, вошедшего в систему.""" response = new_client.post( diff --git a/tests/test_profile.py b/tests/test_profile.py index baa8eb6..76a4c96 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -7,6 +7,7 @@ from app.crud.profile import profile_crud from app.crud.user import user_crud +from app.models import Profile, User REGISTRATION_SCHEMA = { 'email': USER_EMAIL, @@ -30,7 +31,6 @@ async def test_create_profile_with_create_user( ) assert response.status_code == status.HTTP_201_CREATED profiles = await profile_crud.get_multi(db_session) - print(profiles) assert len(profiles) == 1 users = await user_crud.get_multi(db_session) assert len(users) == 1 @@ -39,37 +39,47 @@ async def test_create_profile_with_create_user( class TestSuperuser: async def test_get_all_profiles_superuser( self, + moc_users, + # user_1, + # user_2, db_session: AsyncGenerator, - superuser: AsyncGenerator, - # new_client: AsyncGenerator | TestClient, auth_superuser: AsyncGenerator | TestClient ): """Тест получения всех профилей суперюзером.""" - # response = new_client.post( - # '/auth/jwt/login', - # data={'username': 'admin@admin.com', 'password': 'admin'}, - # ) - # response: Response = new_client.post( - # '/auth/register', json=REGISTRATION_SCHEMA - # ) - # print(response) - users = await user_crud.get_multi(db_session) - print(users) profiles = await profile_crud.get_multi(db_session) - print(profiles) response: Response = auth_superuser.get( '/profiles/' ) print(response) assert response.status_code == status.HTTP_200_OK + assert len(response.json()) == len(profiles) - async def test_3(self): + async def test_forbidden_get_all_profiles_for_user( + self, + moc_users, + # user_1, + # user_2, + db_session: AsyncGenerator, + auth_client: AsyncGenerator | TestClient, + ): """Тест запрета получения профилей простым пользователем.""" - ... + response = auth_client.get( + '/profiles/' + ) + print(response) + assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_4(self): + async def test_4( + self, + moc_users, + db_session, + auth_superuser + ): """Тест фильтрации профилей.""" - ... + users = await user_crud.get_multi(db_session) + print(users) + profiles = await profile_crud.get_multi(db_session) + print(profiles) async def test_5(self): """Тест пагинации профилей""" @@ -111,6 +121,17 @@ async def test_14(self): """Тест запрета создания профиля без создания юзера.""" ... - async def test_15(self): - """Тест запрета удаления профиля.""" - ... + # async def test_forbidden_delete_profile( + # self, + # user_1, + # db_session, + # auth_superuser: AsyncGenerator | TestClient + # ): + # """Тест запрета удаления профиля.""" + # users = await user_crud.get_multi(db_session) + # user: User = users[0] + # profile: Profile = await profile_crud.get_users_obj(user.id, db_session) + # response = auth_superuser.delete( + # f'/profiles/{profile.id}/' + # ) + # assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED From fc9c8e26e4e85831b86266697cedfcc1d1be2ac5 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 02:56:37 +0700 Subject: [PATCH 05/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_profile.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/tests/test_profile.py b/tests/test_profile.py index 76a4c96..1bdddfe 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -2,8 +2,10 @@ from fastapi import status, Response from fastapi.testclient import TestClient +from sqlalchemy import select from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME +from tests.conftest import AsyncSessionLocalTest from app.crud.profile import profile_crud from app.crud.user import user_crud @@ -40,8 +42,6 @@ class TestSuperuser: async def test_get_all_profiles_superuser( self, moc_users, - # user_1, - # user_2, db_session: AsyncGenerator, auth_superuser: AsyncGenerator | TestClient ): @@ -50,15 +50,12 @@ async def test_get_all_profiles_superuser( response: Response = auth_superuser.get( '/profiles/' ) - print(response) assert response.status_code == status.HTTP_200_OK assert len(response.json()) == len(profiles) async def test_forbidden_get_all_profiles_for_user( self, moc_users, - # user_1, - # user_2, db_session: AsyncGenerator, auth_client: AsyncGenerator | TestClient, ): @@ -66,20 +63,33 @@ async def test_forbidden_get_all_profiles_for_user( response = auth_client.get( '/profiles/' ) - print(response) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_4( + async def test_filter_profiles( self, moc_users, db_session, auth_superuser ): """Тест фильтрации профилей.""" - users = await user_crud.get_multi(db_session) - print(users) - profiles = await profile_crud.get_multi(db_session) - print(profiles) + # stmt = select(User).filter(User.username.ilike('%user%')) + # users = await db_session.execute(stmt) + # users = users.scalars().all() + # print(users) + # profiles = await profile_crud.get_multi(db_session) + # print(profiles) + response = auth_superuser.get( + '/profiles/?age__gte=22&age__lte=23' + ) + assert len(response.json()) == 2 + response = auth_superuser.get( + '/profiles/?first_name__ilike=3' + ) + assert len(response.json()) == 1 + response = auth_superuser.get( + '/profiles/?last_name__ilike=4' + ) + assert len(response.json()) == 1 async def test_5(self): """Тест пагинации профилей""" From a363ac8ebe44ef81689d47ef5a6b68b66ec6c444 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 03:10:09 +0700 Subject: [PATCH 06/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=B3=D0=B8=D0=BD=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fixtures/profile.py | 1 - tests/test_profile.py | 21 +++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/fixtures/profile.py b/tests/fixtures/profile.py index 56c2ee5..c11058b 100644 --- a/tests/fixtures/profile.py +++ b/tests/fixtures/profile.py @@ -1,7 +1,6 @@ from typing import AsyncGenerator import pytest_asyncio -from fastapi import FastAPI from passlib.hash import bcrypt from sqlalchemy import select diff --git a/tests/test_profile.py b/tests/test_profile.py index 1bdddfe..a6bbbad 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -91,9 +91,26 @@ async def test_filter_profiles( ) assert len(response.json()) == 1 - async def test_5(self): + async def test_pagination( + self, + moc_users, + auth_superuser + ): """Тест пагинации профилей""" - ... + response = auth_superuser.get( + '/profiles/?limit=2' + ) + result = response.json() + assert len(result) == 2 + assert result[0]['user_id'] == 1 + assert result[1]['user_id'] == 2 + response = auth_superuser.get( + '/profiles/?offset=2&limit=2' + ) + result = response.json() + assert len(result) == 2 + assert result[0]['user_id'] == 3 + assert result[1]['user_id'] == 4 async def test_6(self): """Тест получения своего профиля текущим юзером.""" From afae6f48d8c5b5d756c1a6afe8913f6c27af1caa Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 05:01:11 +0700 Subject: [PATCH 07/44] =?UTF-8?q?=D0=A2=D0=B5=D1=81=D1=82=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B2=D0=BE?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_profile.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/test_profile.py b/tests/test_profile.py index a6bbbad..3120026 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -3,6 +3,7 @@ from fastapi import status, Response from fastapi.testclient import TestClient from sqlalchemy import select +from sqlalchemy.orm import selectinload from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME from tests.conftest import AsyncSessionLocalTest @@ -112,9 +113,31 @@ async def test_pagination( assert result[0]['user_id'] == 3 assert result[1]['user_id'] == 4 - async def test_6(self): + async def test_6( + self, + moc_users, + new_client, + db_session + ): """Тест получения своего профиля текущим юзером.""" - ... + user = await db_session.execute( + select(User).filter(User.id == 1) + .options(selectinload(User.profile)) + ) + user: User = user.scalars().first() + response: Response = new_client.post( + '/auth/jwt/login', + data={'username': user.email, 'password': 'qwerty'}, + ) + assert response.status_code == 200 + access_token = response.json().get('access_token') + new_client.headers.update({'Authorization': f'Bearer {access_token}'}) + response: Response = new_client.get( + '/profiles/me/' + ) + result = response.json() + assert len(result) == 6 + assert result['first_name'] == user.profile.first_name async def test_7(self): """Тест запрета получения чужого профиля текущим юзером.""" From 44814858a6584c7055849fd1d4e676c3eb8e89c7 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 05:27:46 +0700 Subject: [PATCH 08/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D1=82=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=87=D1=83=D0=B6=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=87=D0=BD=D1=8B=D0=BC=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/profile.py | 9 +++++++++ app/crud/profile.py | 9 +++++++++ tests/test_profile.py | 31 +++++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/app/api/endpoints/profile.py b/app/api/endpoints/profile.py index b365c95..637ef6b 100644 --- a/app/api/endpoints/profile.py +++ b/app/api/endpoints/profile.py @@ -49,6 +49,15 @@ async def get_current_user_profile( ) +@router.get('/{profile_id}', response_model=ProfileRead, + dependencies=[Depends(current_superuser)]) +async def get_profile( + profile_id: int, + session: AsyncSession = Depends(get_async_session) +): + return await profile_crud.get(profile_id, session) + + @router.get('/me/photo') async def get_user_photo( user: User = Depends(current_user), diff --git a/app/crud/profile.py b/app/crud/profile.py index aa470b8..df70e2f 100644 --- a/app/crud/profile.py +++ b/app/crud/profile.py @@ -35,6 +35,15 @@ async def get_multi(self, session: AsyncSession): ) return db_objs.scalars().all() + async def get(self, obj_id: int, session: AsyncSession): + profile = await session.execute( + select(self.model) + .options( + selectinload(Profile.achievements) + ) + ) + return profile.scalars().first() + async def get_user_photo( self, user_id: int, diff --git a/tests/test_profile.py b/tests/test_profile.py index 3120026..be91231 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -113,7 +113,7 @@ async def test_pagination( assert result[0]['user_id'] == 3 assert result[1]['user_id'] == 4 - async def test_6( + async def test_get_self_profile( self, moc_users, new_client, @@ -129,7 +129,6 @@ async def test_6( '/auth/jwt/login', data={'username': user.email, 'password': 'qwerty'}, ) - assert response.status_code == 200 access_token = response.json().get('access_token') new_client.headers.update({'Authorization': f'Bearer {access_token}'}) response: Response = new_client.get( @@ -139,9 +138,33 @@ async def test_6( assert len(result) == 6 assert result['first_name'] == user.profile.first_name - async def test_7(self): + async def test_forbidden_get_other_user_profile_for_user( + self, + moc_users, + new_client, + db_session + ): """Тест запрета получения чужого профиля текущим юзером.""" - ... + user = await db_session.execute( + select(User).filter(User.id == 1) + .options(selectinload(User.profile)) + ) + user: User = user.scalars().first() + response: Response = new_client.post( + '/auth/jwt/login', + data={'username': user.email, 'password': 'qwerty'}, + ) + access_token = response.json().get('access_token') + new_client.headers.update({'Authorization': f'Bearer {access_token}'}) + other_user = await db_session.execute( + select(User).filter(User.id == 2) + .options(selectinload(User.profile)) + ) + other_user: User = other_user.scalars().first() + response = new_client.get( + f'/profiles/{other_user.profile.id}' + ) + assert response.status_code == status.HTTP_403_FORBIDDEN async def test_8(self): """Тест получения фото своего профиля юзером.""" From 6d1e2e3ce79d5c1e4c630f898615ca37d085a4c5 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 05:34:58 +0700 Subject: [PATCH 09/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=84=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=20=D1=81=D0=B2=D0=BE=D0=B5=D0=B3=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=84=D0=B8=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_profile.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/test_profile.py b/tests/test_profile.py index be91231..10cfd4a 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -166,9 +166,28 @@ async def test_forbidden_get_other_user_profile_for_user( ) assert response.status_code == status.HTTP_403_FORBIDDEN - async def test_8(self): + async def test_get_photo_from_self_profile( + self, + moc_users, + db_session, + new_client, + ): """Тест получения фото своего профиля юзером.""" - ... + user = await db_session.execute( + select(User).filter(User.id == 1) + .options(selectinload(User.profile)) + ) + user: User = user.scalars().first() + response: Response = new_client.post( + '/auth/jwt/login', + data={'username': user.email, 'password': 'qwerty'}, + ) + access_token = response.json().get('access_token') + new_client.headers.update({'Authorization': f'Bearer {access_token}'}) + response = new_client.get( + '/profiles/me/photo/' + ) + assert response.status_code == status.HTTP_200_OK async def test_9(self): """Тест запрета получения фото чужого профиля.""" From 2a4c024f387261231b4a9d75468cc0e690438998 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 06:21:42 +0700 Subject: [PATCH 10/44] =?UTF-8?q?=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=8E=D0=B7?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=83=D1=8E=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_profile.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/tests/test_profile.py b/tests/test_profile.py index 10cfd4a..f2eba78 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -5,13 +5,12 @@ from sqlalchemy import select from sqlalchemy.orm import selectinload -from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME -from tests.conftest import AsyncSessionLocalTest - from app.crud.profile import profile_crud from app.crud.user import user_crud from app.models import Profile, User +from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME + REGISTRATION_SCHEMA = { 'email': USER_EMAIL, 'password': USER_PASSWORD, @@ -20,6 +19,18 @@ } +async def _get_user( + user_id: int, + db_session, +): + user = await db_session.execute( + select(User).filter(User.id == user_id) + .options(selectinload(User.profile)) + ) + user: User = user.scalars().first() + return user + + class TestCreateProfile: async def test_create_profile_with_create_user( self, new_client, db_session @@ -73,12 +84,6 @@ async def test_filter_profiles( auth_superuser ): """Тест фильтрации профилей.""" - # stmt = select(User).filter(User.username.ilike('%user%')) - # users = await db_session.execute(stmt) - # users = users.scalars().all() - # print(users) - # profiles = await profile_crud.get_multi(db_session) - # print(profiles) response = auth_superuser.get( '/profiles/?age__gte=22&age__lte=23' ) @@ -120,11 +125,7 @@ async def test_get_self_profile( db_session ): """Тест получения своего профиля текущим юзером.""" - user = await db_session.execute( - select(User).filter(User.id == 1) - .options(selectinload(User.profile)) - ) - user: User = user.scalars().first() + user = await _get_user(1, db_session) response: Response = new_client.post( '/auth/jwt/login', data={'username': user.email, 'password': 'qwerty'}, @@ -145,22 +146,14 @@ async def test_forbidden_get_other_user_profile_for_user( db_session ): """Тест запрета получения чужого профиля текущим юзером.""" - user = await db_session.execute( - select(User).filter(User.id == 1) - .options(selectinload(User.profile)) - ) - user: User = user.scalars().first() + user: User = await _get_user(1, db_session) + other_user: User = await _get_user(2, db_session) response: Response = new_client.post( '/auth/jwt/login', data={'username': user.email, 'password': 'qwerty'}, ) access_token = response.json().get('access_token') new_client.headers.update({'Authorization': f'Bearer {access_token}'}) - other_user = await db_session.execute( - select(User).filter(User.id == 2) - .options(selectinload(User.profile)) - ) - other_user: User = other_user.scalars().first() response = new_client.get( f'/profiles/{other_user.profile.id}' ) From 24d418e9c75e438d8257e8b41f616715b22d6ebb Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 25 Jan 2024 06:24:03 +0700 Subject: [PATCH 11/44] refactoring --- tests/test_profile.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_profile.py b/tests/test_profile.py index f2eba78..5136a88 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -166,11 +166,7 @@ async def test_get_photo_from_self_profile( new_client, ): """Тест получения фото своего профиля юзером.""" - user = await db_session.execute( - select(User).filter(User.id == 1) - .options(selectinload(User.profile)) - ) - user: User = user.scalars().first() + user: User = await _get_user(1, db_session) response: Response = new_client.post( '/auth/jwt/login', data={'username': user.email, 'password': 'qwerty'}, From 879aae11d176a2bdb5c4235177e023e245aebd31 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Fri, 26 Jan 2024 04:33:12 +0700 Subject: [PATCH 12/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=B0=D0=BF?= =?UTF-8?q?=D0=B4=D0=B5=D0=B9=D1=82=D0=B0=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8?= =?UTF-8?q?=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/profile.py | 2 +- tests/test_profile.py | 61 +++++++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/app/schemas/profile.py b/app/schemas/profile.py index 71fd9f1..1ae9926 100644 --- a/app/schemas/profile.py +++ b/app/schemas/profile.py @@ -12,7 +12,7 @@ class ProfileRead(BaseModel): age: Optional[int] user_id: int image: Optional[str] - achievements: Optional[list[AchievementRead]] + # achievements: Optional[list[AchievementRead]] class Config: from_attributes = True diff --git a/tests/test_profile.py b/tests/test_profile.py index 5136a88..02f76a2 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -178,41 +178,46 @@ async def test_get_photo_from_self_profile( ) assert response.status_code == status.HTTP_200_OK - async def test_9(self): - """Тест запрета получения фото чужого профиля.""" - ... - - async def test_10(self): + async def test_update_self_profile( + self, + moc_users, + db_session, + new_client: TestClient + ): """Тест апдейта своего профиля.""" - ... - - async def test_11(self): - """Тест запрета апдейта чужого профиля.""" - ... + user: User = await _get_user(1, db_session) + response: Response = new_client.post( + '/auth/jwt/login', + data={'username': user.email, 'password': 'qwerty'}, + ) + access_token = response.json().get('access_token') + new_client.headers.update({'Authorization': f'Bearer {access_token}'}) + data = {'first_name': 'new_first_name'} + response = new_client.patch( + '/profiles/me', + json=data + ) + assert response.status_code == status.HTTP_200_OK + user: User = await _get_user(1, db_session) + assert user.profile.first_name == 'new_first_name' async def test_12(self): """Тест апдейта фото своего профиля.""" ... - async def test_13(self): - """Тест запрета апдейта фото чужого профиля.""" - ... - async def test_14(self): """Тест запрета создания профиля без создания юзера.""" ... - # async def test_forbidden_delete_profile( - # self, - # user_1, - # db_session, - # auth_superuser: AsyncGenerator | TestClient - # ): - # """Тест запрета удаления профиля.""" - # users = await user_crud.get_multi(db_session) - # user: User = users[0] - # profile: Profile = await profile_crud.get_users_obj(user.id, db_session) - # response = auth_superuser.delete( - # f'/profiles/{profile.id}/' - # ) - # assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + async def test_forbidden_delete_profile( + self, + moc_users, + db_session, + auth_superuser: AsyncGenerator | TestClient + ): + """Тест запрета удаления профиля.""" + user = await _get_user(1, db_session) + response = auth_superuser.delete( + f'/profiles/{user.profile.id}' + ) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED From 9e084c3dafe6baafb9b980c96d909cd61914c146 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Fri, 26 Jan 2024 05:22:46 +0700 Subject: [PATCH 13/44] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Тест запрета создания профиля напрямую без создания юзера Тест запрета удаления профиля --- app/api/endpoints/profile.py | 4 ++-- tests/test_profile.py | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/api/endpoints/profile.py b/app/api/endpoints/profile.py index 637ef6b..9902309 100644 --- a/app/api/endpoints/profile.py +++ b/app/api/endpoints/profile.py @@ -106,8 +106,8 @@ def create_profile(): ) -@router.delete('/{obj_id}', deprecated=True, status_code=405) -def delete_profile(obg_id: str): +@router.delete('/{obj_id}', deprecated=True) +def delete_profile(): """Удалить объект""" raise HTTPException( status_code=status.HTTP_405_METHOD_NOT_ALLOWED, diff --git a/tests/test_profile.py b/tests/test_profile.py index 02f76a2..df4d901 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,9 +1,10 @@ from typing import AsyncGenerator -from fastapi import status, Response +from fastapi import status, Response, HTTPException from fastapi.testclient import TestClient from sqlalchemy import select from sqlalchemy.orm import selectinload +import pytest from app.crud.profile import profile_crud from app.crud.user import user_crud @@ -205,9 +206,20 @@ async def test_12(self): """Тест апдейта фото своего профиля.""" ... - async def test_14(self): + async def test_create_profile_deprecated( + self, + new_client: TestClient + ): """Тест запрета создания профиля без создания юзера.""" - ... + response = new_client.post( + '/profiles/', + json={ + 'first_name': 'test_first_name', + 'last_name': 'test_last_name', + 'age': 47 + } + ) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED async def test_forbidden_delete_profile( self, From ead1366335901fa1a7ff48d3ea44bf7412f4f75e Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Sun, 28 Jan 2024 17:22:38 +0700 Subject: [PATCH 14/44] test udate photo --- tests/.env | 6 +++++ tests/conftest.py | 1 + tests/test_profile.py | 56 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 tests/.env diff --git a/tests/.env b/tests/.env new file mode 100644 index 0000000..1ac7e37 --- /dev/null +++ b/tests/.env @@ -0,0 +1,6 @@ +DATABASE_URL=sqlite+aiosqlite:///./fastapi.db +SECRET=MocSecret + +FIRST_SUPERUSER_EMAIL=test@t.t +FIRST_SUPERUSER_PASSWORD=123 +MEDIA_URL =tmp_media/ diff --git a/tests/conftest.py b/tests/conftest.py index 4c8c647..4c171dc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from app.core.db import Base from app.main import app + BASE_DIR = Path(__file__).resolve(strict=True).parent.parent TEST_DB = BASE_DIR / 'test.db' DATABASE_URL_TEST = f'sqlite+aiosqlite:///{str(TEST_DB)}' diff --git a/tests/test_profile.py b/tests/test_profile.py index df4d901..d2f3b8a 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,3 +1,7 @@ +import base64 +import io +from pathlib import Path +from PIL import Image from typing import AsyncGenerator from fastapi import status, Response, HTTPException @@ -9,6 +13,7 @@ from app.crud.profile import profile_crud from app.crud.user import user_crud from app.models import Profile, User +from app.core.config import settings from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME @@ -20,6 +25,15 @@ } +def delete_tmpdir(path: Path): + for sub in path.iterdir(): # type: Path + if sub.is_dir(): + delete_tmpdir(sub) + else: + sub.unlink() + path.rmdir() + + async def _get_user( user_id: int, db_session, @@ -202,9 +216,47 @@ async def test_update_self_profile( user: User = await _get_user(1, db_session) assert user.profile.first_name == 'new_first_name' - async def test_12(self): + async def test_update_photo( + self, + moc_users, + db_session, + new_client: TestClient + ): """Тест апдейта фото своего профиля.""" - ... + user: User = await _get_user(1, db_session) + photo = user.profile.image + tmp_image = Image.new('RGB', (640, 480)) + buffer = io.BytesIO() + Path(settings.base_dir / 'tmp_for_load').mkdir(parents=True, exist_ok=True) + tmp_image.save(settings.base_dir / 'tmp_for_load' / 'img.jpeg', 'jpeg') + tmp_image.save(buffer, format='JPEG') + img_str = base64.b64encode(buffer.getvalue()) + response: Response = new_client.post( + '/auth/jwt/login', + data={'username': user.email, 'password': 'qwerty'}, + ) + access_token = response.json().get('access_token') + new_client.headers.update({'Authorization': f'Bearer {access_token}'}) + with open(settings.base_dir / 'media' / photo, 'rb') as f: + current_photo = base64.b64encode(f.read()) + response = new_client.patch( + '/profiles/me/update_photo', + files={ + 'file': ( + 'img.jpeg', + open(f'{settings.base_dir}/tmp_for_load/img.jpeg', 'rb'), + 'image/jpeg' + ) + } + ) + user: User = await _get_user(1, db_session) + with open(settings.base_dir / 'media' / user.profile.image, 'rb') as f: + new_photo = base64.b64encode(f.read()) + assert new_photo == img_str + assert current_photo != new_photo + path = Path(f'{settings.base_dir}/media/{user.profile.image}') + Path.unlink(path) + delete_tmpdir(settings.base_dir / 'tmp_for_load') async def test_create_profile_deprecated( self, From 5afb149fedfac5961a7ec1603e8fa9323de557e7 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Sun, 28 Jan 2024 17:26:38 +0700 Subject: [PATCH 15/44] isort --- tests/.env | 6 ------ tests/test_profile.py | 13 ++++++------- 2 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 tests/.env diff --git a/tests/.env b/tests/.env deleted file mode 100644 index 1ac7e37..0000000 --- a/tests/.env +++ /dev/null @@ -1,6 +0,0 @@ -DATABASE_URL=sqlite+aiosqlite:///./fastapi.db -SECRET=MocSecret - -FIRST_SUPERUSER_EMAIL=test@t.t -FIRST_SUPERUSER_PASSWORD=123 -MEDIA_URL =tmp_media/ diff --git a/tests/test_profile.py b/tests/test_profile.py index d2f3b8a..77bdd88 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -4,15 +4,14 @@ from PIL import Image from typing import AsyncGenerator -from fastapi import status, Response, HTTPException +from fastapi import status, Response from fastapi.testclient import TestClient from sqlalchemy import select from sqlalchemy.orm import selectinload -import pytest from app.crud.profile import profile_crud from app.crud.user import user_crud -from app.models import Profile, User +from app.models import User from app.core.config import settings from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME @@ -46,7 +45,7 @@ async def _get_user( return user -class TestCreateProfile: +class TestProfile: async def test_create_profile_with_create_user( self, new_client, db_session ): @@ -64,8 +63,6 @@ async def test_create_profile_with_create_user( users = await user_crud.get_multi(db_session) assert len(users) == 1 - -class TestSuperuser: async def test_get_all_profiles_superuser( self, moc_users, @@ -227,7 +224,9 @@ async def test_update_photo( photo = user.profile.image tmp_image = Image.new('RGB', (640, 480)) buffer = io.BytesIO() - Path(settings.base_dir / 'tmp_for_load').mkdir(parents=True, exist_ok=True) + Path( + settings.base_dir / 'tmp_for_load' + ).mkdir(parents=True, exist_ok=True) tmp_image.save(settings.base_dir / 'tmp_for_load' / 'img.jpeg', 'jpeg') tmp_image.save(buffer, format='JPEG') img_str = base64.b64encode(buffer.getvalue()) From 0e4842d5a11bdf62751cfbf2a9243f24138cf8ba Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Sun, 28 Jan 2024 18:18:11 +0700 Subject: [PATCH 16/44] =?UTF-8?q?gitignore=20=D0=B8=20=D0=BF=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B9=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20get=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + app/api/endpoints/group.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/.gitignore b/.gitignore index 9e570b4..dc64120 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.jpg *.PNG infra/.env +test.db # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index cad05f2..3f166aa 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -23,6 +23,18 @@ async def get_all_groups( return await group_crud.get_multi(session) +@router.get( + '/{group_id}', + response_model=GroupRead, + dependencies=[Depends(current_superuser)] +) +async def get_group( + session: AsyncSession = Depends(get_async_session) +): + """Получение группы по id""" + ... + + @router.post( "/", response_model=GroupRead, From 35d5af524870b8f01955489506123ae2ea4d997c Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Sun, 28 Jan 2024 19:20:39 +0700 Subject: [PATCH 17/44] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=BF=D0=BF=D1=8B=20=D1=81=D1=83=D0=BF=D0=B5=D1=80=D1=8E=D0=B7?= =?UTF-8?q?=D0=B5=D1=80=D0=BE=D0=BC=20=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D1=82=20=D0=B4=D0=BB=D1=8F=20=D1=8E=D0=B7=D0=B5=D1=80?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/group.py | 3 ++- tests/test_group.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/test_group.py diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index 3f166aa..796deac 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -38,7 +38,8 @@ async def get_group( @router.post( "/", response_model=GroupRead, - dependencies=[Depends(current_superuser)] + dependencies=[Depends(current_superuser)], + status_code=201 ) async def create_group( group: GroupCreate, session: AsyncSession = Depends(get_async_session) diff --git a/tests/test_group.py b/tests/test_group.py new file mode 100644 index 0000000..6e9a4fc --- /dev/null +++ b/tests/test_group.py @@ -0,0 +1,41 @@ +from fastapi.testclient import TestClient +from sqlalchemy import select, Result, func +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import Group + +GROUP_SCHEME = { + 'name': 'Test Group', + 'description': 'Test Group Description' +} + + +class TestGroup: + async def test_create_group( + self, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Создание группы суперюзером""" + stmt = func.count(Group.id) + groups = await db_session.execute(stmt) + groups = groups.scalar() + response = auth_superuser.post( + '/groups', + json=GROUP_SCHEME + ) + assert response.status_code == 201 + new_groups = await db_session.execute(stmt) + new_groups = new_groups.scalar() + assert new_groups == groups + 1 + + async def test_forbidden_create_group_by_user( + self, + auth_client: TestClient + ): + """Тест запрета создания группы юзером""" + response = auth_client.post( + '/groups', + json=GROUP_SCHEME + ) + assert response.status_code == 403 From 7a023c463502370c90f23aba5c0639add689c02b Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 12:44:36 +0700 Subject: [PATCH 18/44] =?UTF-8?q?=D1=8D=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D1=8B,=20=D0=BA=D1=80=D1=83=D0=B4,=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переписал круд для получения групп с фильтацией по юзеру. Добавил ручку groups/me Добавил ручку патч Для ручки делет установил статус код 204 Добавил схему апдейта группы Добавил фикстуры для групп и тесты для групп --- app/api/endpoints/group.py | 39 ++++++- app/crud/group.py | 16 ++- app/schemas/group.py | 7 +- tests/conftest.py | 1 + tests/fixtures/group.py | 18 ++++ tests/test_group.py | 203 ++++++++++++++++++++++++++++++++++++- 6 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/group.py diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index 796deac..d7b1603 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -5,8 +5,9 @@ from app.core.db import get_async_session from app.core.user import current_superuser, current_user from app.crud import group_crud -from app.schemas.group import GroupCreate, GroupRead +from app.schemas.group import GroupCreate, GroupRead, GroupUpdate from app.services.endpoints_services import delete_obj +from app.models import User router = APIRouter() @@ -14,7 +15,7 @@ @router.get( "/", response_model=list[GroupRead], - dependencies=[Depends(current_user)] + dependencies=[Depends(current_superuser)] ) async def get_all_groups( session: AsyncSession = Depends(get_async_session), @@ -23,16 +24,30 @@ async def get_all_groups( return await group_crud.get_multi(session) +@router.get( + '/me', + response_model=list[GroupRead], + dependencies=[Depends(current_user)] +) +async def get_self_groups( + user: User = Depends(current_user), + session: AsyncSession = Depends(get_async_session) +): + """Получение групп юзером.""" + return await group_crud.get_users_obj(user.id, session) + + @router.get( '/{group_id}', response_model=GroupRead, dependencies=[Depends(current_superuser)] ) async def get_group( + group_id: int, session: AsyncSession = Depends(get_async_session) ): """Получение группы по id""" - ... + return await group_crud.get(group_id, session) @router.post( @@ -49,9 +64,25 @@ async def create_group( return await group_crud.create(obj_in=group, session=session) +@router.patch( + '/{group_id}', + dependencies=[Depends(current_superuser)], + response_model=GroupRead +) +async def update_group( + group_id: int, + group: GroupUpdate, + session: AsyncSession = Depends(get_async_session) +): + """Апдейт группы.""" + _group = await group_crud.get(group_id, session) + return await group_crud.update(_group, group, session) + + @router.delete( "/{obj_id}", - dependencies=[Depends(current_superuser)] + dependencies=[Depends(current_superuser)], + status_code=204 ) async def delete_group( obj_id: int, diff --git a/app/crud/group.py b/app/crud/group.py index 54cda8a..ba358a0 100644 --- a/app/crud/group.py +++ b/app/crud/group.py @@ -1,9 +1,21 @@ +from sqlalchemy.ext.asyncio import AsyncSession from app.crud.base import CRUDBase -from app.models import Group +from app.models import Group, User +from sqlalchemy import select +from sqlalchemy.orm import selectinload class CRUDGroup(CRUDBase): - pass + async def get_users_obj(self, user_id: int, session: AsyncSession): + stmt = ( + select(Group) + # .where(Group.users == user_id) + .options( + selectinload(Group.users) + ).where(Group.users.any(User.id == user_id)) + ) + db_obj = await session.execute(stmt) + return db_obj.scalars().all() group_crud = CRUDGroup(Group) diff --git a/app/schemas/group.py b/app/schemas/group.py index f1b55d3..f260634 100644 --- a/app/schemas/group.py +++ b/app/schemas/group.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field class GroupCreate(BaseModel): @@ -15,3 +15,8 @@ class GroupRead(BaseModel): class Config: from_attributes = True + + +class GroupUpdate(BaseModel): + name: Optional[str] = Field(None,) + description: Optional[str] = Field(None,) diff --git a/tests/conftest.py b/tests/conftest.py index 4c171dc..69545c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ pytest_plugins = [ 'tests.fixtures.user', 'tests.fixtures.profile', + 'tests.fixtures.group' ] engine_test = create_async_engine( diff --git a/tests/fixtures/group.py b/tests/fixtures/group.py new file mode 100644 index 0000000..96edd61 --- /dev/null +++ b/tests/fixtures/group.py @@ -0,0 +1,18 @@ +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import Group + + +@pytest_asyncio.fixture +async def moc_groups( + db_session: AsyncSession +) -> None: + moc_groups = [ + Group( + name=f'Group_{i}', + description=f'Description for Group_{i}' + ) for i in range(1, 6) + ] + db_session.add_all(moc_groups) + await db_session.commit() diff --git a/tests/test_group.py b/tests/test_group.py index 6e9a4fc..49b4e99 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,16 +1,21 @@ from fastapi.testclient import TestClient -from sqlalchemy import select, Result, func +from sqlalchemy import select, func +from sqlalchemy.orm import selectinload from sqlalchemy.ext.asyncio import AsyncSession -from app.models import Group +from app.models import Group, User GROUP_SCHEME = { 'name': 'Test Group', 'description': 'Test Group Description' } +UPDATE_SCHEME = { + 'name': 'Updated name', + 'description': 'Updated description', +} -class TestGroup: +class TestCreateGroup: async def test_create_group( self, db_session: AsyncSession, @@ -39,3 +44,195 @@ async def test_forbidden_create_group_by_user( json=GROUP_SCHEME ) assert response.status_code == 403 + + async def test_forbidden_create_group_nonauth( + self, + new_client: TestClient + ): + """Тест запрета создания группы неавторизованным.""" + response = new_client.post( + '/groups', + json=GROUP_SCHEME + ) + assert response.status_code == 401 + + +class TestGetGroup: + async def test_get_all_groups_superuser( + self, + moc_groups, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Получение всех групп суперюзером.""" + stmt = func.count(Group.id) + groups = await db_session.execute(stmt) + groups = groups.scalar() + response = auth_superuser.get( + '/groups' + ) + assert response.status_code == 200 + assert len(response.json()) == groups + + async def test_forbidden_get_all_groups_user( + self, + moc_groups, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест запрета получения всех групп юзером.""" + response = auth_client.get('/groups') + assert response.status_code == 403 + + async def test_forbidden_get_all_groups_nonauth( + self, + moc_groups, + new_client: TestClient + ): + """Тест запрета получения групп неавторизованным.""" + response = new_client.get('/groups') + assert response.status_code == 401 + + async def test_get_group_by_id_superuser( + self, + moc_groups, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Получение суперюзером группы по id.""" + response = auth_superuser.get( + '/groups/1' + ) + assert response.json()['id'] == 1 + + async def test_forbidden_get_group_by_id_nonauth( + self, + moc_groups, + new_client: TestClient + ): + """Тест запрета получения группы по id неавторизованным.""" + response = new_client.get('/groups/1') + assert response.status_code == 401 + + async def test_get_self_groups_user( + self, + # register_client, + moc_groups, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест получения юзером своих групп.""" + stmt_1 = select(User).options(selectinload(User.groups)) + user = await db_session.execute(stmt_1) + user = user.scalars().first() + stmt = (select(Group).filter(Group.id == 1) + .options(selectinload(Group.users))) + group = await db_session.execute(stmt) + group = group.scalars().first() + group.users.append(user) + await db_session.commit() + await db_session.refresh(group) + user = await db_session.execute(stmt_1) + user = user.scalars().first() + print(group) + print(user) + print(user.groups) + response = auth_client.get( + '/groups/me', + ) + assert response.status_code == 200 + print(response.json()) + print('ok') + + +class TestDeleteGroup: + async def test_delete_group_superuser( + self, + moc_groups, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Тест удаления группы суперюзером.""" + stmt = func.count(Group.id) + groups = await db_session.execute(stmt) + groups = groups.scalar() + response = auth_superuser.delete( + '/groups/1' + ) + assert response.status_code == 204 + groups_after_remove = await db_session.execute(stmt) + groups_after_remove = groups_after_remove.scalar() + assert groups_after_remove == groups - 1 + stmt = select(Group).filter(Group.id == 1) + removed_group = await db_session.execute(stmt) + removed_group = removed_group.scalars().first() + assert removed_group is None + + async def test_forbidden_delete_group_user( + self, + moc_groups, + auth_client: TestClient + ): + """Тест запрета удаления группы юзером.""" + response = auth_client.delete( + '/groups/1' + ) + assert response.status_code == 403 + + async def test_formidden_delete_group_nonauth( + self, + new_client: TestClient + ): + """Тест запрета удаления группы неавторизованным пользователем.""" + response = new_client.delete( + '/groups/1' + ) + assert response.status_code == 401 + + +class TestUpdateGroup: + async def test_update_group_superuser( + self, + moc_groups, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Тест апдейта группы суперюзером.""" + stmt = select(Group).filter(Group.id == 1) + group = await db_session.execute(stmt) + group = group.scalars().first() + assert group.id == 1 + assert group.name != UPDATE_SCHEME['name'] + assert group.description != UPDATE_SCHEME['description'] + response = auth_superuser.patch( + '/groups/1', + json=UPDATE_SCHEME + ) + assert response.status_code == 200 + upated_group = await db_session.execute(stmt) + upated_group = upated_group.scalars().first() + assert group.id == upated_group.id + assert upated_group.name == UPDATE_SCHEME['name'] + assert upated_group.description == UPDATE_SCHEME['description'] + + async def test_forbidden_update_group_user( + self, + auth_client: TestClient + ): + """Тест запрета апдейта группы юзером.""" + response = auth_client.patch( + '/groups/1', + json=UPDATE_SCHEME + ) + assert response.status_code == 403 + + async def test_forbidden_update_group_nonauth( + self, + new_client: TestClient + ): + """Тест запрета апдейта неавторизованным пользователем.""" + response = new_client.patch( + '/groups/1', + json=UPDATE_SCHEME + ) + assert response.status_code == 401 From 204482f334457a0258736c62036ee91054c7e7bf Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 14:49:55 +0700 Subject: [PATCH 19/44] =?UTF-8?q?=D0=AD=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D1=8B,=20=D0=BA=D1=80=D1=83=D0=B4,=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B8,=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил ручку получения текущим юзером своей группы по id. Переписал метод гет в круде группы, чтобы подтягивались данные relations. В модели юзера исправил отношения с группами. Изменил на лист. Добавил тест запрета доступа юзера к ручке groups/{group_id}. Добавил тест получения юзером своей группы через ручку groups/me/{group_id}, в этом же тесте запрет получения группы в которой не состоит и 404 если группа не существует. --- app/api/endpoints/group.py | 29 ++++++++++++++++-- app/crud/group.py | 12 ++++++++ app/models/user.py | 2 +- tests/test_group.py | 61 ++++++++++++++++++++++++++++++++------ 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index d7b1603..ba873b3 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate @@ -7,7 +7,7 @@ from app.crud import group_crud from app.schemas.group import GroupCreate, GroupRead, GroupUpdate from app.services.endpoints_services import delete_obj -from app.models import User +from app.models import User, Group router = APIRouter() @@ -37,6 +37,31 @@ async def get_self_groups( return await group_crud.get_users_obj(user.id, session) +@router.get( + '/me/{group_id}', + response_model=GroupRead, + dependencies=[Depends(current_user)] +) +async def get_self_group_by_id( + group_id: int, + user: User = Depends(current_user), + session: AsyncSession = Depends(get_async_session) +): + """Получение группы по id юзером.""" + group: Group | None = await group_crud.get(group_id, session) + if group is None: + raise HTTPException( + status_code=404, + detail='Такой группы не существует.' + ) + if user not in group.users: + raise HTTPException( + status_code=403, + detail='Вы не состоите в этой группе.' + ) + return group + + @router.get( '/{group_id}', response_model=GroupRead, diff --git a/app/crud/group.py b/app/crud/group.py index ba358a0..82a3d11 100644 --- a/app/crud/group.py +++ b/app/crud/group.py @@ -6,6 +6,18 @@ class CRUDGroup(CRUDBase): + async def get(self, group_id: int, session: AsyncSession): + stmt = ( + select(Group) + .where(Group.id == group_id) + .options( + selectinload(Group.users) + ) + ) + group = await session.execute(stmt) + group = group.scalars().first() + return group + async def get_users_obj(self, user_id: int, session: AsyncSession): stmt = ( select(Group) diff --git a/app/models/user.py b/app/models/user.py index a5e87df..d931314 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -39,7 +39,7 @@ class User(SQLAlchemyBaseUserTable[int], Base): ) tariff: Mapped[Tariff] = relationship(back_populates="users") profile: Mapped[Profile] = relationship(back_populates="user") - groups: Mapped[Group] = relationship( + groups: Mapped[list[Group]] = relationship( secondary=group_user_association, back_populates="users" ) notifications: Mapped[Notification] = relationship( diff --git a/tests/test_group.py b/tests/test_group.py index 49b4e99..5c9582c 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -114,9 +114,16 @@ async def test_forbidden_get_group_by_id_nonauth( response = new_client.get('/groups/1') assert response.status_code == 401 + async def test_forbidden_get_group_by_id_user( + self, + auth_client: TestClient + ): + """Тест запрета получения группы по id юзером.""" + response = auth_client.get('/groups/1') + assert response.status_code == 403 + async def test_get_self_groups_user( self, - # register_client, moc_groups, db_session: AsyncSession, auth_client: TestClient @@ -130,19 +137,55 @@ async def test_get_self_groups_user( group = await db_session.execute(stmt) group = group.scalars().first() group.users.append(user) + stmt = (select(Group).filter(Group.id == 2) + .options(selectinload(Group.users))) + group_2 = await db_session.execute(stmt) + group_2 = group_2.scalars().first() + group_2.users.append(user) await db_session.commit() - await db_session.refresh(group) - user = await db_session.execute(stmt_1) - user = user.scalars().first() - print(group) - print(user) - print(user.groups) + # await db_session.refresh(group) + # user = await db_session.execute(stmt_1) + # user = user.scalars().first() response = auth_client.get( '/groups/me', ) assert response.status_code == 200 - print(response.json()) - print('ok') + result = response.json() + assert len(result) == 2 + assert result[0]['id'] in (1, 2) + assert result[1]['id'] in (1, 2) + assert result[0]['id'] != result[1]['id'] + + async def test_get_group_by_id_user( + self, + register_client, + moc_groups, + moc_users, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест получения юзером группы по id.""" + stmt_1 = select(User).options(selectinload(User.groups)) + user = await db_session.execute(stmt_1) + user = user.scalars().first() + stmt = (select(Group).filter(Group.id == 1) + .options(selectinload(Group.users))) + group = await db_session.execute(stmt) + group = group.scalars().first() + group.users.append(user) + stmt = (select(Group).filter(Group.id == 2) + .options(selectinload(Group.users))) + group_2 = await db_session.execute(stmt) + group_2 = group_2.scalars().first() + group_2.users.append(user) + await db_session.commit() + response = auth_client.get('groups/me/1') + assert response.status_code == 200 + assert response.json()['id'] == 1 + response = auth_client.get('/groups/me/3') + assert response.status_code == 403 + response = auth_client.get('/groups/me/22') + assert response.status_code == 404 class TestDeleteGroup: From 587c5c8d96bc3bde93a5042d5d3ca95a5d3e7cfc Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 15:45:11 +0700 Subject: [PATCH 20/44] refactoring, isort --- app/api/endpoints/group.py | 2 +- app/crud/group.py | 5 +++-- tests/test_group.py | 40 ++++++++++++++++++-------------------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index ba873b3..8a08350 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -5,9 +5,9 @@ from app.core.db import get_async_session from app.core.user import current_superuser, current_user from app.crud import group_crud +from app.models import Group, User from app.schemas.group import GroupCreate, GroupRead, GroupUpdate from app.services.endpoints_services import delete_obj -from app.models import User, Group router = APIRouter() diff --git a/app/crud/group.py b/app/crud/group.py index 82a3d11..3ce0e3d 100644 --- a/app/crud/group.py +++ b/app/crud/group.py @@ -1,8 +1,9 @@ +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + from app.crud.base import CRUDBase from app.models import Group, User -from sqlalchemy import select -from sqlalchemy.orm import selectinload class CRUDGroup(CRUDBase): diff --git a/tests/test_group.py b/tests/test_group.py index 5c9582c..e3cf36f 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,3 +1,4 @@ +from fastapi import status from fastapi.testclient import TestClient from sqlalchemy import select, func from sqlalchemy.orm import selectinload @@ -29,7 +30,7 @@ async def test_create_group( '/groups', json=GROUP_SCHEME ) - assert response.status_code == 201 + assert response.status_code == status.HTTP_201_CREATED new_groups = await db_session.execute(stmt) new_groups = new_groups.scalar() assert new_groups == groups + 1 @@ -43,7 +44,7 @@ async def test_forbidden_create_group_by_user( '/groups', json=GROUP_SCHEME ) - assert response.status_code == 403 + assert response.status_code == status.HTTP_403_FORBIDDEN async def test_forbidden_create_group_nonauth( self, @@ -54,7 +55,7 @@ async def test_forbidden_create_group_nonauth( '/groups', json=GROUP_SCHEME ) - assert response.status_code == 401 + assert response.status_code == status.HTTP_401_UNAUTHORIZED class TestGetGroup: @@ -71,7 +72,7 @@ async def test_get_all_groups_superuser( response = auth_superuser.get( '/groups' ) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK assert len(response.json()) == groups async def test_forbidden_get_all_groups_user( @@ -82,7 +83,7 @@ async def test_forbidden_get_all_groups_user( ): """Тест запрета получения всех групп юзером.""" response = auth_client.get('/groups') - assert response.status_code == 403 + assert response.status_code == status.HTTP_403_FORBIDDEN async def test_forbidden_get_all_groups_nonauth( self, @@ -91,7 +92,7 @@ async def test_forbidden_get_all_groups_nonauth( ): """Тест запрета получения групп неавторизованным.""" response = new_client.get('/groups') - assert response.status_code == 401 + assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_get_group_by_id_superuser( self, @@ -112,7 +113,7 @@ async def test_forbidden_get_group_by_id_nonauth( ): """Тест запрета получения группы по id неавторизованным.""" response = new_client.get('/groups/1') - assert response.status_code == 401 + assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_forbidden_get_group_by_id_user( self, @@ -120,7 +121,7 @@ async def test_forbidden_get_group_by_id_user( ): """Тест запрета получения группы по id юзером.""" response = auth_client.get('/groups/1') - assert response.status_code == 403 + assert response.status_code == status.HTTP_403_FORBIDDEN async def test_get_self_groups_user( self, @@ -143,13 +144,10 @@ async def test_get_self_groups_user( group_2 = group_2.scalars().first() group_2.users.append(user) await db_session.commit() - # await db_session.refresh(group) - # user = await db_session.execute(stmt_1) - # user = user.scalars().first() response = auth_client.get( '/groups/me', ) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK result = response.json() assert len(result) == 2 assert result[0]['id'] in (1, 2) @@ -180,12 +178,12 @@ async def test_get_group_by_id_user( group_2.users.append(user) await db_session.commit() response = auth_client.get('groups/me/1') - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK assert response.json()['id'] == 1 response = auth_client.get('/groups/me/3') - assert response.status_code == 403 + assert response.status_code == status.HTTP_403_FORBIDDEN response = auth_client.get('/groups/me/22') - assert response.status_code == 404 + assert response.status_code == status.HTTP_404_NOT_FOUND class TestDeleteGroup: @@ -202,7 +200,7 @@ async def test_delete_group_superuser( response = auth_superuser.delete( '/groups/1' ) - assert response.status_code == 204 + assert response.status_code == status.HTTP_204_NO_CONTENT groups_after_remove = await db_session.execute(stmt) groups_after_remove = groups_after_remove.scalar() assert groups_after_remove == groups - 1 @@ -220,7 +218,7 @@ async def test_forbidden_delete_group_user( response = auth_client.delete( '/groups/1' ) - assert response.status_code == 403 + assert response.status_code == status.HTTP_403_FORBIDDEN async def test_formidden_delete_group_nonauth( self, @@ -230,7 +228,7 @@ async def test_formidden_delete_group_nonauth( response = new_client.delete( '/groups/1' ) - assert response.status_code == 401 + assert response.status_code == status.HTTP_401_UNAUTHORIZED class TestUpdateGroup: @@ -251,7 +249,7 @@ async def test_update_group_superuser( '/groups/1', json=UPDATE_SCHEME ) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK upated_group = await db_session.execute(stmt) upated_group = upated_group.scalars().first() assert group.id == upated_group.id @@ -267,7 +265,7 @@ async def test_forbidden_update_group_user( '/groups/1', json=UPDATE_SCHEME ) - assert response.status_code == 403 + assert response.status_code == status.HTTP_403_FORBIDDEN async def test_forbidden_update_group_nonauth( self, @@ -278,4 +276,4 @@ async def test_forbidden_update_group_nonauth( '/groups/1', json=UPDATE_SCHEME ) - assert response.status_code == 401 + assert response.status_code == status.HTTP_401_UNAUTHORIZED From b658d8bfce88b9a10e94c88d2725ba1533125715 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 15:56:24 +0700 Subject: [PATCH 21/44] flake8 --- app/schemas/profile.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/schemas/profile.py b/app/schemas/profile.py index 1ae9926..fdad29a 100644 --- a/app/schemas/profile.py +++ b/app/schemas/profile.py @@ -2,8 +2,6 @@ from pydantic import BaseModel, Field -from app.schemas.achievement import AchievementRead - class ProfileRead(BaseModel): id: int @@ -12,7 +10,6 @@ class ProfileRead(BaseModel): age: Optional[int] user_id: int image: Optional[str] - # achievements: Optional[list[AchievementRead]] class Config: from_attributes = True From 3bd713cb8ca78e3572e64048a17d99b7610ce820 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 16:02:30 +0700 Subject: [PATCH 22/44] isort tests --- tests/conftest.py | 1 - tests/fixtures/profile.py | 2 +- tests/test_group.py | 4 ++-- tests/test_profile.py | 7 +++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 69545c0..9e52daa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,6 @@ from app.core.db import Base from app.main import app - BASE_DIR = Path(__file__).resolve(strict=True).parent.parent TEST_DB = BASE_DIR / 'test.db' DATABASE_URL_TEST = f'sqlite+aiosqlite:///{str(TEST_DB)}' diff --git a/tests/fixtures/profile.py b/tests/fixtures/profile.py index c11058b..c6ce10f 100644 --- a/tests/fixtures/profile.py +++ b/tests/fixtures/profile.py @@ -4,7 +4,7 @@ from passlib.hash import bcrypt from sqlalchemy import select -from app.models import User, Profile +from app.models import Profile, User from tests.conftest import AsyncSessionLocalTest diff --git a/tests/test_group.py b/tests/test_group.py index e3cf36f..4690a7a 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,8 +1,8 @@ from fastapi import status from fastapi.testclient import TestClient -from sqlalchemy import select, func -from sqlalchemy.orm import selectinload +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload from app.models import Group, User diff --git a/tests/test_profile.py b/tests/test_profile.py index 77bdd88..fd7bdc3 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,19 +1,18 @@ import base64 import io from pathlib import Path -from PIL import Image from typing import AsyncGenerator -from fastapi import status, Response +from fastapi import Response, status from fastapi.testclient import TestClient +from PIL import Image from sqlalchemy import select from sqlalchemy.orm import selectinload +from app.core.config import settings from app.crud.profile import profile_crud from app.crud.user import user_crud from app.models import User -from app.core.config import settings - from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME REGISTRATION_SCHEMA = { From 4a55710f4748db2dd2927a0d24f400add56c91cc Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 16:42:17 +0700 Subject: [PATCH 23/44] test create achievements by supruser, fix status code in response --- app/api/endpoints/achievement.py | 5 +++-- tests/test_achievement.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/test_achievement.py diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index c0a80e5..60c764f 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate @@ -26,7 +26,8 @@ async def get_all_achievements( @router.post( "/", response_model=AchievementRead, - dependencies=[Depends(current_superuser)] + dependencies=[Depends(current_superuser)], + status_code=status.HTTP_201_CREATED ) async def create_achievement( achievement: AchievementCreate, diff --git a/tests/test_achievement.py b/tests/test_achievement.py new file mode 100644 index 0000000..b181442 --- /dev/null +++ b/tests/test_achievement.py @@ -0,0 +1,28 @@ +from fastapi import status +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import func + +from app.models import Achievement + +CREATE_SCHEME = { + 'name': 'Achievment name', + 'description': 'Achievment description' +} + + +class TestCreateAchievement: + async def test_create_achievement_superuser( + self, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Тест создания ачивмент.""" + stmt = func.count(Achievement.id) + achievements = await db_session.execute(stmt) + achievements = achievements.scalar() + response = auth_superuser.post( + '/achievements', + json=CREATE_SCHEME + ) + assert response.status_code == status.HTTP_201_CREATED From bca4e5f5f92d9655ee4403aa9071f69e2650e298 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 17:03:18 +0700 Subject: [PATCH 24/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B0=D1=87=D0=B8=D0=B2?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=20=D1=81=20=D0=BF=D1=80=D0=BE=D0=BF?= =?UTF-8?q?=D1=83=D1=89=D0=B5=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_achievement.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index b181442..d085257 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -9,6 +9,9 @@ 'name': 'Achievment name', 'description': 'Achievment description' } +WRONG_CREATE_SCHEME = { + 'description': 'Achievment description' +} class TestCreateAchievement: @@ -26,3 +29,23 @@ async def test_create_achievement_superuser( json=CREATE_SCHEME ) assert response.status_code == status.HTTP_201_CREATED + new_achievements = await db_session.execute(stmt) + new_achievements = new_achievements.scalar() + assert new_achievements == achievements + 1 + + async def test_wrong_create_scheme( + self, + db_session: AsyncSession, + auth_superuser: TestClient + ): + stmt = func.count(Achievement.id) + achievements = await db_session.execute(stmt) + achievements = achievements.scalar() + response = auth_superuser.post( + '/achievements', + json=WRONG_CREATE_SCHEME + ) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + new_achievements = await db_session.execute(stmt) + new_achievements = new_achievements.scalar() + assert new_achievements == achievements From b504d47d1958098ec6625d7041ed0b6019187a1a Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 17:07:42 +0700 Subject: [PATCH 25/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D1=82=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B0=D1=87=D0=B8=D0=B2=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_achievement.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index d085257..698ad63 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -49,3 +49,21 @@ async def test_wrong_create_scheme( new_achievements = await db_session.execute(stmt) new_achievements = new_achievements.scalar() assert new_achievements == achievements + + async def test_forbidden_create_achievemrnt_user( + self, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест запрета создания ачивмент юзером.""" + stmt = func.count(Achievement.id) + achievements = await db_session.execute(stmt) + achievements = achievements.scalar() + response = auth_client.post( + '/achievements', + json=CREATE_SCHEME + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + new_achievements = await db_session.execute(stmt) + new_achievements = new_achievements.scalar() + assert new_achievements == achievements From 30643a1e1548de9324d22259e782f1930c626499 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 17:12:05 +0700 Subject: [PATCH 26/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D1=82=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B0=D1=87=D0=B8=D0=B2=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20=D0=BD=D0=B5=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_achievement.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 698ad63..095e7a2 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -67,3 +67,21 @@ async def test_forbidden_create_achievemrnt_user( new_achievements = await db_session.execute(stmt) new_achievements = new_achievements.scalar() assert new_achievements == achievements + + async def test_forbidden_create_achievemrnt_nonauth( + self, + db_session: AsyncSession, + new_client: TestClient + ): + """Тест запрета создания ачивмент юзером.""" + stmt = func.count(Achievement.id) + achievements = await db_session.execute(stmt) + achievements = achievements.scalar() + response = new_client.post( + '/achievements', + json=CREATE_SCHEME + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + new_achievements = await db_session.execute(stmt) + new_achievements = new_achievements.scalar() + assert new_achievements == achievements From 46b3a6692558ef7647ee06cf17421fd74b4238b2 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 17:40:09 +0700 Subject: [PATCH 27/44] test get all achievements superuser --- tests/conftest.py | 3 ++- tests/fixtures/achievement.py | 18 ++++++++++++++++++ tests/test_achievement.py | 20 ++++++++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/achievement.py diff --git a/tests/conftest.py b/tests/conftest.py index 9e52daa..73670d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,8 @@ pytest_plugins = [ 'tests.fixtures.user', 'tests.fixtures.profile', - 'tests.fixtures.group' + 'tests.fixtures.group', + 'tests.fixtures.achievement' ] engine_test = create_async_engine( diff --git a/tests/fixtures/achievement.py b/tests/fixtures/achievement.py new file mode 100644 index 0000000..0c48cfe --- /dev/null +++ b/tests/fixtures/achievement.py @@ -0,0 +1,18 @@ +import pytest_asyncio +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models import Achievement + + +@pytest_asyncio.fixture +async def moc_achievements( + db_session: AsyncSession +) -> None: + moc_achievements = [ + Achievement( + name=f'Achievement_{i}', + description=f'Description for Achievemetnt_{i}' + ) for i in range(1, 6) + ] + db_session.add_all(moc_achievements) + await db_session.commit() diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 095e7a2..c19db27 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -50,7 +50,7 @@ async def test_wrong_create_scheme( new_achievements = new_achievements.scalar() assert new_achievements == achievements - async def test_forbidden_create_achievemrnt_user( + async def test_forbidden_create_achievement_user( self, db_session: AsyncSession, auth_client: TestClient @@ -68,7 +68,7 @@ async def test_forbidden_create_achievemrnt_user( new_achievements = new_achievements.scalar() assert new_achievements == achievements - async def test_forbidden_create_achievemrnt_nonauth( + async def test_forbidden_create_achievement_nonauth( self, db_session: AsyncSession, new_client: TestClient @@ -85,3 +85,19 @@ async def test_forbidden_create_achievemrnt_nonauth( new_achievements = await db_session.execute(stmt) new_achievements = new_achievements.scalar() assert new_achievements == achievements + + +class TestGetAchievement: + async def test_get_all_achievements_superuser( + self, + moc_achievements, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Тест получения всех ачивмент суперюзером.""" + stmt = func.count(Achievement.id) + achievements = await db_session.execute(stmt) + achievements = achievements.scalar() + response = auth_superuser.get('/achievements') + assert response.status_code == status.HTTP_200_OK + assert len(response.json()) == achievements From 8da75b4d90a1307826fafab2f64fd92f6fdfc12c Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Mon, 29 Jan 2024 18:36:59 +0700 Subject: [PATCH 28/44] endpoints, tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ручка гет всех ачивментс поменял депенденсис на суперюзера Добавил тесты запрета получения всех ачивментс юзером и неавторизованным --- app/api/endpoints/achievement.py | 2 +- tests/test_achievement.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index 60c764f..327309d 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -14,7 +14,7 @@ @router.get( "/", response_model=list[AchievementRead], - dependencies=[Depends(current_user)] + dependencies=[Depends(current_superuser)] ) async def get_all_achievements( session: AsyncSession = Depends(get_async_session), diff --git a/tests/test_achievement.py b/tests/test_achievement.py index c19db27..f279e01 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -101,3 +101,22 @@ async def test_get_all_achievements_superuser( response = auth_superuser.get('/achievements') assert response.status_code == status.HTTP_200_OK assert len(response.json()) == achievements + + async def test_forbidden_get_all_achievements_user( + self, + moc_achievements, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест запрета получения всех ачивмент юзером.""" + response = auth_client.get('/achievements') + assert response.status_code == status.HTTP_403_FORBIDDEN + + async def test_forbidden_get_all_achievements_nonauth( + self, + moc_achievements, + new_client: TestClient + ): + """Тест запрета получения ачивментс неавторизованным.""" + response = new_client.get('/achievements') + assert response.status_code == status.HTTP_401_UNAUTHORIZED From 2a49ebe69921b34030b0313e2366b8ef42e02312 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Tue, 30 Jan 2024 21:12:17 +0700 Subject: [PATCH 29/44] endpoints, crud for achievement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил ручку получения юзером своих ачивментс Переписал get_users_obj для ачивмент --- app/api/endpoints/achievement.py | 14 ++++++ app/crud/achievement.py | 16 ++++++- app/models/profile.py | 2 +- tests/test_achievement.py | 74 +++++++++++++++++++++++++++++++- 4 files changed, 101 insertions(+), 5 deletions(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index 327309d..ecfc39b 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -7,6 +7,7 @@ from app.crud import achievement_crud from app.schemas.achievement import AchievementCreate, AchievementRead from app.services.endpoints_services import delete_obj +from app.models import User router = APIRouter() @@ -23,6 +24,19 @@ async def get_all_achievements( return await achievement_crud.get_multi(session) +@router.get( + '/me', + response_model=list[AchievementRead], + dependencies=[Depends(current_user)] +) +async def get_self_achievements( + user: User = Depends(current_user), + session: AsyncSession = Depends(get_async_session) +): + """Возвращает ачивментс юзера.""" + return await achievement_crud.get_users_obj(user.id, session) + + @router.post( "/", response_model=AchievementRead, diff --git a/app/crud/achievement.py b/app/crud/achievement.py index ec44edc..c1261ad 100644 --- a/app/crud/achievement.py +++ b/app/crud/achievement.py @@ -1,9 +1,21 @@ +from sqlalchemy.ext.asyncio import AsyncSession from app.crud.base import CRUDBase -from app.models import Achievement +from app.models import Achievement, Profile +from sqlalchemy import select +from sqlalchemy.orm import selectinload class CRUDAchievement(CRUDBase): - pass + async def get_users_obj(self, user_id: int, session: AsyncSession): + stmt = ( + select(Achievement) + .options( + selectinload(Achievement.profiles) + .selectinload(Profile.user) + ).where(Achievement.profiles.any(Profile.user_id == user_id)) + ) + db_obj = await session.execute(stmt) + return db_obj.scalars().all() achievement_crud = CRUDAchievement(Achievement) diff --git a/app/models/profile.py b/app/models/profile.py index f8688d5..333651c 100644 --- a/app/models/profile.py +++ b/app/models/profile.py @@ -38,7 +38,7 @@ class Profile(Base): ForeignKey('user.id'), unique=True ) user: Mapped[User] = relationship(back_populates='profile') - achievements: Mapped[Achievement] = relationship( + achievements: Mapped[list[Achievement]] = relationship( secondary=achievement_profile_association, back_populates="profiles" ) image: Mapped[str] = Column( diff --git a/tests/test_achievement.py b/tests/test_achievement.py index f279e01..e7f272a 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -1,9 +1,10 @@ from fastapi import status from fastapi.testclient import TestClient from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import func +from sqlalchemy import func, select +from sqlalchemy.orm import selectinload -from app.models import Achievement +from app.models import Achievement, User, Profile CREATE_SCHEME = { 'name': 'Achievment name', @@ -120,3 +121,72 @@ async def test_forbidden_get_all_achievements_nonauth( """Тест запрета получения ачивментс неавторизованным.""" response = new_client.get('/achievements') assert response.status_code == status.HTTP_401_UNAUTHORIZED + + async def test_get_self_achievements_user( + self, + moc_users, + moc_achievements, + register_client, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест получения юзером своих ачивментс.""" + user = await db_session.execute( + select(User) + .where(User.username == register_client.username) + .options( + selectinload(User.profile) + .selectinload(Profile.achievements) + ) + ) + user = user.scalars().first() + profile = Profile( + first_name='testuser_first_name', + last_name='testuser_last_name', + age=47, + user_id=user.id + ) + db_session.add(profile) + await db_session.commit() + await db_session.refresh(profile) + # await db_session.refresh(user) + achievement = await db_session.execute( + select(Achievement) + .where(Achievement.id == 1) + .options( + selectinload(Achievement.profiles) + ) + ) + achievement = achievement.scalars().first() + achiv = await db_session.execute( + select(Achievement) + .where(Achievement.id == 2) + .options( + selectinload(Achievement.profiles) + ) + ) + achiv = achiv.scalars().first() + achiv.profiles.append(profile) + achievement.profiles.append(profile) + await db_session.commit() + await db_session.refresh(achievement) + await db_session.refresh(user) + print(achievement) + print(user.id) + user = await db_session.execute( + select(User) + .options( + selectinload(User.profile) + .selectinload(Profile.achievements) + ) + .where(User.id == user.id) + + ) + user = user.scalars().first() + response = auth_client.get('achievements/me') + assert response.status_code == status.HTTP_200_OK + print(response.json()) + print(achievement) + print(profile) + print(user.profile) + print(user.profile.achievements) From b166e529240f60803e0c16c6805a93c0b3db36f9 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Tue, 30 Jan 2024 21:17:15 +0700 Subject: [PATCH 30/44] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=8E=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=20=D1=81=D0=B2=D0=BE=D0=B8=D1=85=20=D0=B0?= =?UTF-8?q?=D1=87=D0=B8=D0=B2=D0=BC=D0=B5=D0=BD=D1=82=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crud/achievement.py | 1 - tests/test_achievement.py | 13 +++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/crud/achievement.py b/app/crud/achievement.py index c1261ad..adc6c69 100644 --- a/app/crud/achievement.py +++ b/app/crud/achievement.py @@ -11,7 +11,6 @@ async def get_users_obj(self, user_id: int, session: AsyncSession): select(Achievement) .options( selectinload(Achievement.profiles) - .selectinload(Profile.user) ).where(Achievement.profiles.any(Profile.user_id == user_id)) ) db_obj = await session.execute(stmt) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index e7f272a..fac1233 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -149,7 +149,6 @@ async def test_get_self_achievements_user( db_session.add(profile) await db_session.commit() await db_session.refresh(profile) - # await db_session.refresh(user) achievement = await db_session.execute( select(Achievement) .where(Achievement.id == 1) @@ -171,8 +170,6 @@ async def test_get_self_achievements_user( await db_session.commit() await db_session.refresh(achievement) await db_session.refresh(user) - print(achievement) - print(user.id) user = await db_session.execute( select(User) .options( @@ -185,8 +182,8 @@ async def test_get_self_achievements_user( user = user.scalars().first() response = auth_client.get('achievements/me') assert response.status_code == status.HTTP_200_OK - print(response.json()) - print(achievement) - print(profile) - print(user.profile) - print(user.profile.achievements) + result = response.json() + assert len(result) == 2 + assert result[0]['id'] in (1, 2) + assert result[1]['id'] in (1, 2) + assert result[0]['id'] != result[1]['id'] From c4c94a7e03f7d4598f31e49e9bd76ee7018e53c6 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Tue, 30 Jan 2024 23:10:59 +0700 Subject: [PATCH 31/44] endpoints, crud, tests for achievement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit добавил ручку /me/{achievement_id} переписал круд get добавил тест на ручку /me/{achievement_id} рефакторинг теста речки /me --- app/api/endpoints/achievement.py | 31 +++++++++++++- app/crud/achievement.py | 12 ++++++ tests/test_achievement.py | 72 +++++++++++++++++++++++--------- 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index ecfc39b..aafaf48 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, status, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate @@ -7,7 +7,7 @@ from app.crud import achievement_crud from app.schemas.achievement import AchievementCreate, AchievementRead from app.services.endpoints_services import delete_obj -from app.models import User +from app.models import User, Achievement router = APIRouter() @@ -37,6 +37,33 @@ async def get_self_achievements( return await achievement_crud.get_users_obj(user.id, session) +@router.get( + '/me/{achievement_id}', + response_model=AchievementRead, + dependencies=[Depends(current_user)] +) +async def get_self_achievement_by_id( + achievement_id: int, + user: User = Depends(current_user), + session: AsyncSession = Depends(get_async_session) +): + """Возвращает ачивмент юзера по id.""" + achievement: Achievement = await achievement_crud.get( + achievement_id, session + ) + if achievement is None: + raise HTTPException( + status_code=404, + detail='Achievement не существует.' + ) + if user.id not in [_.id for _ in achievement.profiles]: + raise HTTPException( + status_code=403, + detail='У выс нет этого achievement.' + ) + return achievement + + @router.post( "/", response_model=AchievementRead, diff --git a/app/crud/achievement.py b/app/crud/achievement.py index adc6c69..a1b316a 100644 --- a/app/crud/achievement.py +++ b/app/crud/achievement.py @@ -6,6 +6,18 @@ class CRUDAchievement(CRUDBase): + async def get(self, obj_id: int, session: AsyncSession): + stmt = ( + select(Achievement) + .where(Achievement.id == obj_id) + .options( + selectinload(Achievement.profiles) + ) + ) + achievement = await session.execute(stmt) + achievement = achievement.scalars().first() + return achievement + async def get_users_obj(self, user_id: int, session: AsyncSession): stmt = ( select(Achievement) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index fac1233..5dbee5f 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -124,9 +124,9 @@ async def test_forbidden_get_all_achievements_nonauth( async def test_get_self_achievements_user( self, + register_client, moc_users, moc_achievements, - register_client, db_session: AsyncSession, auth_client: TestClient ): @@ -140,15 +140,11 @@ async def test_get_self_achievements_user( ) ) user = user.scalars().first() - profile = Profile( - first_name='testuser_first_name', - last_name='testuser_last_name', - age=47, - user_id=user.id + profile = await db_session.execute( + select(Profile) + .where(Profile.user_id == user.id) ) - db_session.add(profile) - await db_session.commit() - await db_session.refresh(profile) + profile = profile.scalars().first() achievement = await db_session.execute( select(Achievement) .where(Achievement.id == 1) @@ -168,22 +164,60 @@ async def test_get_self_achievements_user( achiv.profiles.append(profile) achievement.profiles.append(profile) await db_session.commit() - await db_session.refresh(achievement) - await db_session.refresh(user) + response = auth_client.get('achievements/me') + assert response.status_code == status.HTTP_200_OK + result = response.json() + assert len(result) == 2 + assert result[0]['id'] in (1, 2) + assert result[1]['id'] in (1, 2) + assert result[0]['id'] != result[1]['id'] + + async def test_get_self_achievement_by_id_user( + self, + register_client, + moc_users, + moc_achievements, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест получения юзером своего ачивмент по id.""" user = await db_session.execute( select(User) + .where(User.username == register_client.username) .options( selectinload(User.profile) .selectinload(Profile.achievements) ) - .where(User.id == user.id) - ) user = user.scalars().first() - response = auth_client.get('achievements/me') + profile = await db_session.execute( + select(Profile) + .where(Profile.user_id == user.id) + ) + profile = profile.scalars().first() + achievement = await db_session.execute( + select(Achievement) + .where(Achievement.id == 1) + .options( + selectinload(Achievement.profiles) + ) + ) + achievement = achievement.scalars().first() + achiv = await db_session.execute( + select(Achievement) + .where(Achievement.id == 2) + .options( + selectinload(Achievement.profiles) + ) + ) + achiv = achiv.scalars().first() + achiv.profiles.append(profile) + achievement.profiles.append(profile) + await db_session.commit() + response = auth_client.get('/achievements/me/1') assert response.status_code == status.HTTP_200_OK - result = response.json() - assert len(result) == 2 - assert result[0]['id'] in (1, 2) - assert result[1]['id'] in (1, 2) - assert result[0]['id'] != result[1]['id'] + assert response.json()['id'] == 1 + response = auth_client.get('/achievements/me/3') + assert response.status_code == status.HTTP_403_FORBIDDEN + response = auth_client.get('/achievements/me/22') + assert response.status_code == status.HTTP_404_NOT_FOUND From b7862d02d773aa3a5169d69c1c6328d28c6b08ff Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Tue, 30 Jan 2024 23:25:22 +0700 Subject: [PATCH 32/44] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D1=82=D0=B0=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B0=D1=87=D0=B8=D0=B2=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BE=D0=BC=20=D0=B8=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D1=80=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_achievement.py | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 5dbee5f..0d0200f 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -221,3 +221,49 @@ async def test_get_self_achievement_by_id_user( assert response.status_code == status.HTTP_403_FORBIDDEN response = auth_client.get('/achievements/me/22') assert response.status_code == status.HTTP_404_NOT_FOUND + + +class TestUpdateAchievement: + ... + + +class TestDeleteAchievement: + async def test_forbidden_delete_acievement_user( + self, + moc_achievements, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест запрета удаления ачивмент юзером.""" + achievements = await db_session.execute( + func.count(Achievement.id) + ) + achievements = achievements.scalar() + assert achievements > 0 + response = auth_client.delete('/achievements/1') + assert response.status_code == status.HTTP_403_FORBIDDEN + check_achiv = await db_session.execute( + func.count(Achievement.id) + ) + check_achiv = check_achiv.scalar() + assert check_achiv == achievements + + async def test_forbidden_delete_acievement_nonauth( + self, + moc_achievements, + db_session: AsyncSession, + new_client: TestClient + ): + """Тест запрета удаления ачивмент юзером.""" + achievements = await db_session.execute( + func.count(Achievement.id) + ) + achievements = achievements.scalar() + assert achievements > 0 + response = new_client.delete('/achievements/1') + assert response.status_code == status.HTTP_401_UNAUTHORIZED + check_achiv = await db_session.execute( + func.count(Achievement.id) + ) + check_achiv = check_achiv.scalar() + assert check_achiv == achievements From e9e7f70aee891c4d70dd15cd49597e3599db65ea Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Tue, 30 Jan 2024 23:48:49 +0700 Subject: [PATCH 33/44] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D1=87=D0=B8=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20=D1=81=D1=83=D0=BF=D0=B5=D1=80=D1=8E=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/achievement.py | 3 ++- tests/test_achievement.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index aafaf48..660377a 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -79,7 +79,8 @@ async def create_achievement( return await achievement_crud.create(obj_in=achievement, session=session) -@router.delete("/{obj_id}", dependencies=[Depends(current_superuser)]) +@router.delete("/{obj_id}", dependencies=[Depends(current_superuser)], + status_code=status.HTTP_204_NO_CONTENT) async def delete_achievement( obj_id: int, session: AsyncSession = Depends(get_async_session), diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 0d0200f..0e4463a 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -254,7 +254,7 @@ async def test_forbidden_delete_acievement_nonauth( db_session: AsyncSession, new_client: TestClient ): - """Тест запрета удаления ачивмент юзером.""" + """Тест запрета удаления ачивмент неавторизованным.""" achievements = await db_session.execute( func.count(Achievement.id) ) @@ -267,3 +267,33 @@ async def test_forbidden_delete_acievement_nonauth( ) check_achiv = check_achiv.scalar() assert check_achiv == achievements + + async def test_delete_achievement_superuser( + self, + moc_achievements, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Тест удаления ачивмент суперюзером.""" + achievement = await db_session.execute( + select(Achievement) + .where(Achievement.id == 1) + ) + achievement = achievement.scalar() + achiv_count = await db_session.execute( + func.count(Achievement.id) + ) + achiv_count = achiv_count.scalar() + response = auth_superuser.delete('/achievements/1') + assert response.status_code == status.HTTP_204_NO_CONTENT + removed_achiv = await db_session.execute( + select(Achievement) + .where(Achievement.id == 1) + ) + removed_achiv = removed_achiv.scalar() + assert removed_achiv is None + check_achiv_count = await db_session.execute( + func.count(Achievement.id) + ) + check_achiv_count = check_achiv_count.scalar() + assert check_achiv_count == achiv_count - 1 From 771eee7e971f93c3239ae70d2786ce8a85bcc538 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Tue, 30 Jan 2024 23:58:05 +0700 Subject: [PATCH 34/44] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D1=82=D0=B0=20=D0=B0=D0=BF=D0=B4=D0=B5=D0=B9=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B0=D1=87=D0=B8=D0=B2=D0=BC=D0=B5=D0=BD=D1=82=20?= =?UTF-8?q?=D1=8E=D0=B7=D0=B5=D1=80=D0=BE=D0=BC=20=D0=B8=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/achievement.py | 13 +++++++++++++ tests/test_achievement.py | 24 +++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index 660377a..f264bcf 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -79,6 +79,19 @@ async def create_achievement( return await achievement_crud.create(obj_in=achievement, session=session) +@router.patch( + '/{achievement_id}', + response_model=AchievementRead, + dependencies=[Depends(current_superuser)] +) +async def update_achievement( + achievement_id: int, + session: AsyncSession = Depends(get_async_session) +): + """Апдейт ачивмент.""" + return {'resp': 'ok'} + + @router.delete("/{obj_id}", dependencies=[Depends(current_superuser)], status_code=status.HTTP_204_NO_CONTENT) async def delete_achievement( diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 0e4463a..4918210 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -224,7 +224,29 @@ async def test_get_self_achievement_by_id_user( class TestUpdateAchievement: - ... + async def test_forbidden_update_achievement_user( + self, + moc_achievements, + db_session: AsyncSession, + auth_client: TestClient + ): + """Тест запрета апдейта ачивмент юзером.""" + response = auth_client.patch( + '/achievements/1' + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + + async def test_forbidden_update_achievement_nonauth( + self, + moc_achievements, + db_session: AsyncSession, + new_client: TestClient + ): + """Тест запрета апдейта ачивмент неавторизованным.""" + response = new_client.patch( + '/achievements/1' + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED class TestDeleteAchievement: From 5c241269223de612e9e76c7b15f674934de8fbd6 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Wed, 31 Jan 2024 00:14:21 +0700 Subject: [PATCH 35/44] endpoints, schemas, tests for achievement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил ручку апдейт Добавил схему для апдейта Добавил тест апдейта суперюзером --- app/api/endpoints/achievement.py | 9 +++++++-- app/schemas/achievement.py | 7 ++++++- tests/test_achievement.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index f264bcf..4a8608c 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -5,7 +5,8 @@ from app.core.db import get_async_session from app.core.user import current_superuser, current_user from app.crud import achievement_crud -from app.schemas.achievement import AchievementCreate, AchievementRead +from app.schemas.achievement import (AchievementCreate, AchievementRead, + AchievementUpdate) from app.services.endpoints_services import delete_obj from app.models import User, Achievement @@ -86,10 +87,14 @@ async def create_achievement( ) async def update_achievement( achievement_id: int, + data: AchievementUpdate, session: AsyncSession = Depends(get_async_session) ): """Апдейт ачивмент.""" - return {'resp': 'ok'} + _achievement = await achievement_crud.get(achievement_id, session) + return await achievement_crud.update( + _achievement, data, session + ) @router.delete("/{obj_id}", dependencies=[Depends(current_superuser)], diff --git a/app/schemas/achievement.py b/app/schemas/achievement.py index bc47044..f4b67fb 100644 --- a/app/schemas/achievement.py +++ b/app/schemas/achievement.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field class AchievementCreate(BaseModel): @@ -15,3 +15,8 @@ class AchievementRead(BaseModel): class Config: from_attributes = True + + +class AchievementUpdate(BaseModel): + name: Optional[str] = Field(None,) + description: Optional[str] = Field(None,) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 4918210..afe380c 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -13,6 +13,9 @@ WRONG_CREATE_SCHEME = { 'description': 'Achievment description' } +UPDATE_SCHEME = { + 'name': 'new achievement name' +} class TestCreateAchievement: @@ -248,6 +251,32 @@ async def test_forbidden_update_achievement_nonauth( ) assert response.status_code == status.HTTP_401_UNAUTHORIZED + async def test_update_achievement_superuser( + self, + moc_achievements, + db_session: AsyncSession, + auth_superuser: TestClient + ): + """Тест апдейта ачивмент суперюзером.""" + achievement = await db_session.execute( + select(Achievement) + .where(Achievement.id == 1) + ) + achievement = achievement.scalar() + response = auth_superuser.patch( + '/achievements/1', + json=UPDATE_SCHEME + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()['name'] == UPDATE_SCHEME['name'] + check_achievement = await db_session.execute( + select(Achievement) + .where(Achievement.id == 1) + ) + check_achievement = check_achievement.scalar() + assert check_achievement.name == UPDATE_SCHEME['name'] + assert check_achievement.description == achievement.description + class TestDeleteAchievement: async def test_forbidden_delete_acievement_user( From 1321f846c18cef387d622b376d4b1762526a3012 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Wed, 31 Jan 2024 00:17:54 +0700 Subject: [PATCH 36/44] isort --- app/api/endpoints/achievement.py | 4 ++-- app/crud/achievement.py | 5 +++-- tests/test_achievement.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index 4a8608c..46fc2f1 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -1,14 +1,14 @@ -from fastapi import APIRouter, Depends, status, HTTPException +from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate from app.core.db import get_async_session from app.core.user import current_superuser, current_user from app.crud import achievement_crud +from app.models import Achievement, User from app.schemas.achievement import (AchievementCreate, AchievementRead, AchievementUpdate) from app.services.endpoints_services import delete_obj -from app.models import User, Achievement router = APIRouter() diff --git a/app/crud/achievement.py b/app/crud/achievement.py index a1b316a..46c6240 100644 --- a/app/crud/achievement.py +++ b/app/crud/achievement.py @@ -1,8 +1,9 @@ +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + from app.crud.base import CRUDBase from app.models import Achievement, Profile -from sqlalchemy import select -from sqlalchemy.orm import selectinload class CRUDAchievement(CRUDBase): diff --git a/tests/test_achievement.py b/tests/test_achievement.py index afe380c..167854f 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -1,10 +1,10 @@ from fastapi import status from fastapi.testclient import TestClient -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.models import Achievement, User, Profile +from app.models import Achievement, Profile, User CREATE_SCHEME = { 'name': 'Achievment name', From fc0098d41bea3f1a465c51fdc9f8152e5422ede9 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Wed, 31 Jan 2024 04:39:44 +0700 Subject: [PATCH 37/44] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit получение количества объектов вынесено в отделбную утилиту --- tests/conftest.py | 1 - tests/fixtures/profile.py | 2 +- tests/test_profile.py | 43 ++++++++++++++++++--------------------- tests/utils.py | 12 +++++++++++ 4 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/conftest.py b/tests/conftest.py index 4c171dc..4c8c647 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,6 @@ from app.core.db import Base from app.main import app - BASE_DIR = Path(__file__).resolve(strict=True).parent.parent TEST_DB = BASE_DIR / 'test.db' DATABASE_URL_TEST = f'sqlite+aiosqlite:///{str(TEST_DB)}' diff --git a/tests/fixtures/profile.py b/tests/fixtures/profile.py index c11058b..c6ce10f 100644 --- a/tests/fixtures/profile.py +++ b/tests/fixtures/profile.py @@ -4,7 +4,7 @@ from passlib.hash import bcrypt from sqlalchemy import select -from app.models import User, Profile +from app.models import Profile, User from tests.conftest import AsyncSessionLocalTest diff --git a/tests/test_profile.py b/tests/test_profile.py index 77bdd88..749c412 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -1,21 +1,21 @@ import base64 import io from pathlib import Path -from PIL import Image from typing import AsyncGenerator -from fastapi import status, Response +from fastapi import Response, status from fastapi.testclient import TestClient +from PIL import Image from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.crud.profile import profile_crud -from app.crud.user import user_crud -from app.models import User from app.core.config import settings - +from app.models import Profile, User from tests.fixtures.user import USER_EMAIL, USER_PASSWORD, USER_USERNAME +from .utils import get_obj_count + REGISTRATION_SCHEMA = { 'email': USER_EMAIL, 'password': USER_PASSWORD, @@ -47,41 +47,39 @@ async def _get_user( class TestProfile: async def test_create_profile_with_create_user( - self, new_client, db_session + self, new_client, + db_session: AsyncSession ): """Тест создания профиля при регистрации пользователя.""" - profiles = await profile_crud.get_multi(db_session) - assert len(profiles) == 0 - users = await user_crud.get_multi(db_session) - assert len(users) == 0 + profiles = await get_obj_count(Profile, db_session) + users = await get_obj_count(User, db_session) response: Response = new_client.post( '/auth/register', json=REGISTRATION_SCHEMA ) assert response.status_code == status.HTTP_201_CREATED - profiles = await profile_crud.get_multi(db_session) - assert len(profiles) == 1 - users = await user_crud.get_multi(db_session) - assert len(users) == 1 + check_profiles = await get_obj_count(Profile, db_session) + assert check_profiles == profiles + 1 + check_users = await get_obj_count(User, db_session) + assert check_users == users + 1 async def test_get_all_profiles_superuser( self, moc_users, - db_session: AsyncGenerator, - auth_superuser: AsyncGenerator | TestClient + db_session: AsyncSession, + auth_superuser: TestClient ): """Тест получения всех профилей суперюзером.""" - profiles = await profile_crud.get_multi(db_session) + profiles = await get_obj_count(Profile, db_session) response: Response = auth_superuser.get( '/profiles/' ) assert response.status_code == status.HTTP_200_OK - assert len(response.json()) == len(profiles) + assert len(response.json()) == profiles - async def test_forbidden_get_all_profiles_for_user( + async def test_forbidden_get_all_profiles_user( self, moc_users, - db_session: AsyncGenerator, - auth_client: AsyncGenerator | TestClient, + auth_client: TestClient, ): """Тест запрета получения профилей простым пользователем.""" response = auth_client.get( @@ -148,7 +146,6 @@ async def test_get_self_profile( '/profiles/me/' ) result = response.json() - assert len(result) == 6 assert result['first_name'] == user.profile.first_name async def test_forbidden_get_other_user_profile_for_user( diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..06e17e8 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,12 @@ +from sqlalchemy import func +from sqlalchemy.ext.asyncio import AsyncSession + + +async def get_obj_count( + model, + session: AsyncSession +) -> int: + """Возвращает количество объектов в базе.""" + stmt = func.count(model.id) + count = await session.execute(stmt) + return count.scalar() From 5bb1292644824047ea0228a8fddd6a852a3cfe80 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Wed, 31 Jan 2024 05:12:43 +0700 Subject: [PATCH 38/44] refactoring --- tests/test_group.py | 81 +++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/tests/test_group.py b/tests/test_group.py index 4690a7a..0bcbe24 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -6,6 +6,8 @@ from app.models import Group, User +from .utils import get_obj_count + GROUP_SCHEME = { 'name': 'Test Group', 'description': 'Test Group Description' @@ -16,6 +18,28 @@ } +async def _get_user( + db_session: AsyncSession +): + """Возвращает юзера.""" + stmt = (select(User) + .where(User.username == 'testuser') + .options(selectinload(User.groups))) + user = await db_session.execute(stmt) + return user.scalar() + + +async def _get_group_by_id( + index: int, + db_session: AsyncSession +): + """Возвращает группу по id.""" + stmt = (select(Group).where(Group.id == index) + .options(selectinload(Group.users))) + group = await db_session.execute(stmt) + return group.scalar() + + class TestCreateGroup: async def test_create_group( self, @@ -23,16 +47,13 @@ async def test_create_group( auth_superuser: TestClient ): """Создание группы суперюзером""" - stmt = func.count(Group.id) - groups = await db_session.execute(stmt) - groups = groups.scalar() + groups = await get_obj_count(Group, db_session) response = auth_superuser.post( '/groups', json=GROUP_SCHEME ) assert response.status_code == status.HTTP_201_CREATED - new_groups = await db_session.execute(stmt) - new_groups = new_groups.scalar() + new_groups = await get_obj_count(Group, db_session) assert new_groups == groups + 1 async def test_forbidden_create_group_by_user( @@ -66,9 +87,7 @@ async def test_get_all_groups_superuser( auth_superuser: TestClient ): """Получение всех групп суперюзером.""" - stmt = func.count(Group.id) - groups = await db_session.execute(stmt) - groups = groups.scalar() + groups = await get_obj_count(Group, db_session) response = auth_superuser.get( '/groups' ) @@ -130,18 +149,10 @@ async def test_get_self_groups_user( auth_client: TestClient ): """Тест получения юзером своих групп.""" - stmt_1 = select(User).options(selectinload(User.groups)) - user = await db_session.execute(stmt_1) - user = user.scalars().first() - stmt = (select(Group).filter(Group.id == 1) - .options(selectinload(Group.users))) - group = await db_session.execute(stmt) - group = group.scalars().first() + user = await _get_user(db_session) + group = await _get_group_by_id(1, db_session) group.users.append(user) - stmt = (select(Group).filter(Group.id == 2) - .options(selectinload(Group.users))) - group_2 = await db_session.execute(stmt) - group_2 = group_2.scalars().first() + group_2 = await _get_group_by_id(2, db_session) group_2.users.append(user) await db_session.commit() response = auth_client.get( @@ -163,18 +174,10 @@ async def test_get_group_by_id_user( auth_client: TestClient ): """Тест получения юзером группы по id.""" - stmt_1 = select(User).options(selectinload(User.groups)) - user = await db_session.execute(stmt_1) - user = user.scalars().first() - stmt = (select(Group).filter(Group.id == 1) - .options(selectinload(Group.users))) - group = await db_session.execute(stmt) - group = group.scalars().first() + user = await _get_user(db_session) + group = await _get_group_by_id(1, db_session) group.users.append(user) - stmt = (select(Group).filter(Group.id == 2) - .options(selectinload(Group.users))) - group_2 = await db_session.execute(stmt) - group_2 = group_2.scalars().first() + group_2 = await _get_group_by_id(2, db_session) group_2.users.append(user) await db_session.commit() response = auth_client.get('groups/me/1') @@ -194,19 +197,14 @@ async def test_delete_group_superuser( auth_superuser: TestClient ): """Тест удаления группы суперюзером.""" - stmt = func.count(Group.id) - groups = await db_session.execute(stmt) - groups = groups.scalar() + groups = await get_obj_count(Group, db_session) response = auth_superuser.delete( '/groups/1' ) assert response.status_code == status.HTTP_204_NO_CONTENT - groups_after_remove = await db_session.execute(stmt) - groups_after_remove = groups_after_remove.scalar() + groups_after_remove = await get_obj_count(Group, db_session) assert groups_after_remove == groups - 1 - stmt = select(Group).filter(Group.id == 1) - removed_group = await db_session.execute(stmt) - removed_group = removed_group.scalars().first() + removed_group = await _get_group_by_id(1, db_session) assert removed_group is None async def test_forbidden_delete_group_user( @@ -239,9 +237,7 @@ async def test_update_group_superuser( auth_superuser: TestClient ): """Тест апдейта группы суперюзером.""" - stmt = select(Group).filter(Group.id == 1) - group = await db_session.execute(stmt) - group = group.scalars().first() + group = await _get_group_by_id(1, db_session) assert group.id == 1 assert group.name != UPDATE_SCHEME['name'] assert group.description != UPDATE_SCHEME['description'] @@ -250,8 +246,7 @@ async def test_update_group_superuser( json=UPDATE_SCHEME ) assert response.status_code == status.HTTP_200_OK - upated_group = await db_session.execute(stmt) - upated_group = upated_group.scalars().first() + upated_group = await _get_group_by_id(1, db_session) assert group.id == upated_group.id assert upated_group.name == UPDATE_SCHEME['name'] assert upated_group.description == UPDATE_SCHEME['description'] From e8f0910d46b26e0e93b2959c88fc22fa3177c5d7 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Wed, 31 Jan 2024 05:13:37 +0700 Subject: [PATCH 39/44] pep8 --- tests/test_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_group.py b/tests/test_group.py index 0bcbe24..dae4b2a 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,6 +1,6 @@ from fastapi import status from fastapi.testclient import TestClient -from sqlalchemy import func, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload From fd61fa5711f60b91991467360ac41500716e3678 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Wed, 31 Jan 2024 05:56:49 +0700 Subject: [PATCH 40/44] refactoring --- tests/test_achievement.py | 203 ++++++++++++++------------------------ 1 file changed, 75 insertions(+), 128 deletions(-) diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 167854f..9ffc824 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -1,11 +1,13 @@ from fastapi import status from fastapi.testclient import TestClient -from sqlalchemy import func, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.models import Achievement, Profile, User +from .utils import get_obj_count + CREATE_SCHEME = { 'name': 'Achievment name', 'description': 'Achievment description' @@ -18,6 +20,50 @@ } +async def _get_achievement_by_id( + index: int, + db_session: AsyncSession +): + """Возвращает ачивмент по id.""" + stmt = ( + select(Achievement) + .where(Achievement.id == index) + .options( + selectinload(Achievement.profiles) + ) + ) + achievement = await db_session.execute(stmt) + return achievement.scalar() + + +async def _get_user_by_id( + index: int, + db_session: AsyncSession +): + """Возвращает юзера по id.""" + user = await db_session.execute( + select(User) + .where(User.id == index) + .options( + selectinload(User.profile) + .selectinload(Profile.achievements) + ) + ) + return user.scalar() + + +async def _get_profile_by_user_id( + index: int, + db_session: AsyncSession +): + """Возвращает профиль юзера.""" + profile = await db_session.execute( + select(Profile) + .where(Profile.user_id == index) + ) + return profile.scalar() + + class TestCreateAchievement: async def test_create_achievement_superuser( self, @@ -25,16 +71,13 @@ async def test_create_achievement_superuser( auth_superuser: TestClient ): """Тест создания ачивмент.""" - stmt = func.count(Achievement.id) - achievements = await db_session.execute(stmt) - achievements = achievements.scalar() + achievements = await get_obj_count(Achievement, db_session) response = auth_superuser.post( '/achievements', json=CREATE_SCHEME ) assert response.status_code == status.HTTP_201_CREATED - new_achievements = await db_session.execute(stmt) - new_achievements = new_achievements.scalar() + new_achievements = await get_obj_count(Achievement, db_session) assert new_achievements == achievements + 1 async def test_wrong_create_scheme( @@ -42,16 +85,13 @@ async def test_wrong_create_scheme( db_session: AsyncSession, auth_superuser: TestClient ): - stmt = func.count(Achievement.id) - achievements = await db_session.execute(stmt) - achievements = achievements.scalar() + achievements = await get_obj_count(Achievement, db_session) response = auth_superuser.post( '/achievements', json=WRONG_CREATE_SCHEME ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY - new_achievements = await db_session.execute(stmt) - new_achievements = new_achievements.scalar() + new_achievements = await get_obj_count(Achievement, db_session) assert new_achievements == achievements async def test_forbidden_create_achievement_user( @@ -60,16 +100,13 @@ async def test_forbidden_create_achievement_user( auth_client: TestClient ): """Тест запрета создания ачивмент юзером.""" - stmt = func.count(Achievement.id) - achievements = await db_session.execute(stmt) - achievements = achievements.scalar() + achievements = await get_obj_count(Achievement, db_session) response = auth_client.post( '/achievements', json=CREATE_SCHEME ) assert response.status_code == status.HTTP_403_FORBIDDEN - new_achievements = await db_session.execute(stmt) - new_achievements = new_achievements.scalar() + new_achievements = await get_obj_count(Achievement, db_session) assert new_achievements == achievements async def test_forbidden_create_achievement_nonauth( @@ -78,16 +115,13 @@ async def test_forbidden_create_achievement_nonauth( new_client: TestClient ): """Тест запрета создания ачивмент юзером.""" - stmt = func.count(Achievement.id) - achievements = await db_session.execute(stmt) - achievements = achievements.scalar() + achievements = await get_obj_count(Achievement, db_session) response = new_client.post( '/achievements', json=CREATE_SCHEME ) assert response.status_code == status.HTTP_401_UNAUTHORIZED - new_achievements = await db_session.execute(stmt) - new_achievements = new_achievements.scalar() + new_achievements = await get_obj_count(Achievement, db_session) assert new_achievements == achievements @@ -99,9 +133,7 @@ async def test_get_all_achievements_superuser( auth_superuser: TestClient ): """Тест получения всех ачивмент суперюзером.""" - stmt = func.count(Achievement.id) - achievements = await db_session.execute(stmt) - achievements = achievements.scalar() + achievements = await get_obj_count(Achievement, db_session) response = auth_superuser.get('/achievements') assert response.status_code == status.HTTP_200_OK assert len(response.json()) == achievements @@ -134,36 +166,10 @@ async def test_get_self_achievements_user( auth_client: TestClient ): """Тест получения юзером своих ачивментс.""" - user = await db_session.execute( - select(User) - .where(User.username == register_client.username) - .options( - selectinload(User.profile) - .selectinload(Profile.achievements) - ) - ) - user = user.scalars().first() - profile = await db_session.execute( - select(Profile) - .where(Profile.user_id == user.id) - ) - profile = profile.scalars().first() - achievement = await db_session.execute( - select(Achievement) - .where(Achievement.id == 1) - .options( - selectinload(Achievement.profiles) - ) - ) - achievement = achievement.scalars().first() - achiv = await db_session.execute( - select(Achievement) - .where(Achievement.id == 2) - .options( - selectinload(Achievement.profiles) - ) - ) - achiv = achiv.scalars().first() + user = await _get_user_by_id(register_client.id, db_session) + profile = await _get_profile_by_user_id(user.id, db_session) + achievement = await _get_achievement_by_id(1, db_session) + achiv = await _get_achievement_by_id(2, db_session) achiv.profiles.append(profile) achievement.profiles.append(profile) await db_session.commit() @@ -184,36 +190,10 @@ async def test_get_self_achievement_by_id_user( auth_client: TestClient ): """Тест получения юзером своего ачивмент по id.""" - user = await db_session.execute( - select(User) - .where(User.username == register_client.username) - .options( - selectinload(User.profile) - .selectinload(Profile.achievements) - ) - ) - user = user.scalars().first() - profile = await db_session.execute( - select(Profile) - .where(Profile.user_id == user.id) - ) - profile = profile.scalars().first() - achievement = await db_session.execute( - select(Achievement) - .where(Achievement.id == 1) - .options( - selectinload(Achievement.profiles) - ) - ) - achievement = achievement.scalars().first() - achiv = await db_session.execute( - select(Achievement) - .where(Achievement.id == 2) - .options( - selectinload(Achievement.profiles) - ) - ) - achiv = achiv.scalars().first() + user = await _get_user_by_id(register_client.id, db_session) + profile = await _get_profile_by_user_id(user.id, db_session) + achievement = await _get_achievement_by_id(1, db_session) + achiv = await _get_achievement_by_id(2, db_session) achiv.profiles.append(profile) achievement.profiles.append(profile) await db_session.commit() @@ -258,22 +238,14 @@ async def test_update_achievement_superuser( auth_superuser: TestClient ): """Тест апдейта ачивмент суперюзером.""" - achievement = await db_session.execute( - select(Achievement) - .where(Achievement.id == 1) - ) - achievement = achievement.scalar() + achievement = await _get_achievement_by_id(1, db_session) response = auth_superuser.patch( '/achievements/1', json=UPDATE_SCHEME ) assert response.status_code == status.HTTP_200_OK assert response.json()['name'] == UPDATE_SCHEME['name'] - check_achievement = await db_session.execute( - select(Achievement) - .where(Achievement.id == 1) - ) - check_achievement = check_achievement.scalar() + check_achievement = await _get_achievement_by_id(1, db_session) assert check_achievement.name == UPDATE_SCHEME['name'] assert check_achievement.description == achievement.description @@ -286,17 +258,11 @@ async def test_forbidden_delete_acievement_user( auth_client: TestClient ): """Тест запрета удаления ачивмент юзером.""" - achievements = await db_session.execute( - func.count(Achievement.id) - ) - achievements = achievements.scalar() + achievements = await get_obj_count(Achievement, db_session) assert achievements > 0 response = auth_client.delete('/achievements/1') assert response.status_code == status.HTTP_403_FORBIDDEN - check_achiv = await db_session.execute( - func.count(Achievement.id) - ) - check_achiv = check_achiv.scalar() + check_achiv = await get_obj_count(Achievement, db_session) assert check_achiv == achievements async def test_forbidden_delete_acievement_nonauth( @@ -306,17 +272,11 @@ async def test_forbidden_delete_acievement_nonauth( new_client: TestClient ): """Тест запрета удаления ачивмент неавторизованным.""" - achievements = await db_session.execute( - func.count(Achievement.id) - ) - achievements = achievements.scalar() + achievements = await get_obj_count(Achievement, db_session) assert achievements > 0 response = new_client.delete('/achievements/1') assert response.status_code == status.HTTP_401_UNAUTHORIZED - check_achiv = await db_session.execute( - func.count(Achievement.id) - ) - check_achiv = check_achiv.scalar() + check_achiv = await get_obj_count(Achievement, db_session) assert check_achiv == achievements async def test_delete_achievement_superuser( @@ -326,25 +286,12 @@ async def test_delete_achievement_superuser( auth_superuser: TestClient ): """Тест удаления ачивмент суперюзером.""" - achievement = await db_session.execute( - select(Achievement) - .where(Achievement.id == 1) - ) - achievement = achievement.scalar() - achiv_count = await db_session.execute( - func.count(Achievement.id) - ) - achiv_count = achiv_count.scalar() + achievement = await _get_achievement_by_id(1, db_session) + assert achievement is not None + achiv_count = await get_obj_count(Achievement, db_session) response = auth_superuser.delete('/achievements/1') assert response.status_code == status.HTTP_204_NO_CONTENT - removed_achiv = await db_session.execute( - select(Achievement) - .where(Achievement.id == 1) - ) - removed_achiv = removed_achiv.scalar() + removed_achiv = await _get_achievement_by_id(1, db_session) assert removed_achiv is None - check_achiv_count = await db_session.execute( - func.count(Achievement.id) - ) - check_achiv_count = check_achiv_count.scalar() + check_achiv_count = await get_obj_count(Achievement, db_session) assert check_achiv_count == achiv_count - 1 From 9774e9c5e70eadfdd4d0d834df38456db039b9d3 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 1 Feb 2024 03:45:54 +0700 Subject: [PATCH 41/44] refactoring status codes --- app/api/endpoints/group.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index 8a08350..22b31c2 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate @@ -51,12 +51,12 @@ async def get_self_group_by_id( group: Group | None = await group_crud.get(group_id, session) if group is None: raise HTTPException( - status_code=404, + status_code=status.HTTP_404_NOT_FOUND, detail='Такой группы не существует.' ) if user not in group.users: raise HTTPException( - status_code=403, + status_code=status.HTTP_403_FORBIDDEN, detail='Вы не состоите в этой группе.' ) return group @@ -79,7 +79,7 @@ async def get_group( "/", response_model=GroupRead, dependencies=[Depends(current_superuser)], - status_code=201 + status_code=status.HTTP_201_CREATED ) async def create_group( group: GroupCreate, session: AsyncSession = Depends(get_async_session) @@ -107,7 +107,7 @@ async def update_group( @router.delete( "/{obj_id}", dependencies=[Depends(current_superuser)], - status_code=204 + status_code=status.HTTP_204_NO_CONTENT ) async def delete_group( obj_id: int, From c9a23a7fdeedb1c56a75464b30cfe643898d41a2 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 1 Feb 2024 04:02:34 +0700 Subject: [PATCH 42/44] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D0=B3=D0=B5=D1=82=20=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/group.py | 13 +++++++++++-- tests/test_group.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index 22b31c2..cc84b46 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Response from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate @@ -9,6 +9,9 @@ from app.schemas.group import GroupCreate, GroupRead, GroupUpdate from app.services.endpoints_services import delete_obj +from app.services.utils import (Pagination, get_pagination_params, paginated, + add_response_headers) + router = APIRouter() @@ -18,10 +21,16 @@ dependencies=[Depends(current_superuser)] ) async def get_all_groups( + response: Response, session: AsyncSession = Depends(get_async_session), + pagination: Pagination = Depends(get_pagination_params) ) -> list[GroupRead]: """Возвращает все группы.""" - return await group_crud.get_multi(session) + groups = await group_crud.get_multi(session) + response = add_response_headers( + response, groups, pagination + ) + return paginated(groups, pagination) @router.get( diff --git a/tests/test_group.py b/tests/test_group.py index dae4b2a..1869bb7 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -272,3 +272,26 @@ async def test_forbidden_update_group_nonauth( json=UPDATE_SCHEME ) assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +class TestPaginationGroup: + async def test_pagination( + self, + moc_groups, + auth_superuser: TestClient + ): + """Тест пагинации профилей""" + response = auth_superuser.get( + '/groups/?limit=2' + ) + result = response.json() + assert len(result) == 2 + assert result[0]['id'] == 1 + assert result[1]['id'] == 2 + response = auth_superuser.get( + '/groups/?offset=2&limit=2' + ) + result = response.json() + assert len(result) == 2 + assert result[0]['id'] == 3 + assert result[1]['id'] == 4 From f0eed7019cf4328aa459ac570dd1edded125fdd8 Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 1 Feb 2024 04:41:15 +0700 Subject: [PATCH 43/44] isort --- app/api/endpoints/group.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/api/endpoints/group.py b/app/api/endpoints/group.py index cc84b46..04825ff 100644 --- a/app/api/endpoints/group.py +++ b/app/api/endpoints/group.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Response +from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate @@ -8,9 +8,8 @@ from app.models import Group, User from app.schemas.group import GroupCreate, GroupRead, GroupUpdate from app.services.endpoints_services import delete_obj - -from app.services.utils import (Pagination, get_pagination_params, paginated, - add_response_headers) +from app.services.utils import (Pagination, add_response_headers, + get_pagination_params, paginated) router = APIRouter() From 87176657598176b3d3d4af90848359c560244dec Mon Sep 17 00:00:00 2001 From: Dmitry Abramov Date: Thu, 1 Feb 2024 05:37:53 +0700 Subject: [PATCH 44/44] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/endpoints/achievement.py | 12 ++++++++++-- tests/test_achievement.py | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/api/endpoints/achievement.py b/app/api/endpoints/achievement.py index 46fc2f1..6639e04 100644 --- a/app/api/endpoints/achievement.py +++ b/app/api/endpoints/achievement.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.validators import check_name_duplicate @@ -9,6 +9,8 @@ from app.schemas.achievement import (AchievementCreate, AchievementRead, AchievementUpdate) from app.services.endpoints_services import delete_obj +from app.services.utils import (Pagination, add_response_headers, + get_pagination_params, paginated) router = APIRouter() @@ -19,10 +21,16 @@ dependencies=[Depends(current_superuser)] ) async def get_all_achievements( + response: Response, + pagination: Pagination = Depends(get_pagination_params), session: AsyncSession = Depends(get_async_session), ) -> list[AchievementRead]: """Возвращает все achievement.""" - return await achievement_crud.get_multi(session) + achievements = await achievement_crud.get_multi(session) + response = add_response_headers( + response, achievements, pagination + ) + return paginated(achievements, pagination) @router.get( diff --git a/tests/test_achievement.py b/tests/test_achievement.py index 9ffc824..9059cea 100644 --- a/tests/test_achievement.py +++ b/tests/test_achievement.py @@ -295,3 +295,26 @@ async def test_delete_achievement_superuser( assert removed_achiv is None check_achiv_count = await get_obj_count(Achievement, db_session) assert check_achiv_count == achiv_count - 1 + + +class TestPaginationGroup: + async def test_pagination( + self, + moc_achievements, + auth_superuser: TestClient + ): + """Тест пагинации профилей""" + response = auth_superuser.get( + '/achievements/?limit=2' + ) + result = response.json() + assert len(result) == 2 + assert result[0]['id'] == 1 + assert result[1]['id'] == 2 + response = auth_superuser.get( + '/achievements/?offset=2&limit=2' + ) + result = response.json() + assert len(result) == 2 + assert result[0]['id'] == 3 + assert result[1]['id'] == 4