From 9a5d3b9ac044345bec4e35eaca732ed2ae5d8c4a Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 18 May 2026 21:20:06 +0300 Subject: [PATCH 01/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80?= =?UTF-8?q?=D1=8B:=20logging=5Fsql=5Freq=5Fbefore=5Fexecute=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20SQL-=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=81=D0=BE=D0=B2=20=D0=B8=20override=5Fdbsession?= =?UTF-8?q?=5Fin=5Froute,=20=D0=BC=D0=BE=D0=BA=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D1=81=D1=81=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=80=D1=83=D1=87=D0=BA=D0=B8=20=D0=BD=D0=B0=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=83=D1=8E=20=D1=81=D0=B5=D1=81?= =?UTF-8?q?=D1=81=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 029db70..8831f7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,8 @@ from alembic.config import Config as AlembicConfig from fastapi.testclient import TestClient from sqlalchemy import create_engine +from sqlalchemy import event +from fastapi_sqlalchemy import db from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer @@ -91,6 +93,28 @@ def dbsession(db_container): yield session +@pytest.fixture() +def logging_sql_req_before_execute(dbsession): + """ + Фикстура для логирования всех сформированных в рамках одной транзакции + SQL-запросов, до их отправки в бд, то есть до Session.flush() или + до Session.commit(). + """ + engine = dbsession.get_bind() + @event.listens_for(engine, "before_execute", named=True) + def sql_requests_listener(**kw_entities_of_executing): + print("\n========= SQL command =========\n") + print(f"SQL was sended: {kw_entities_of_executing.get("clauseelement")}\n") + print("===============================\n") + + + +@pytest.fixture() +def override_dbsession_in_route(dbsession, mocker): + "Мок для подмены db.session в ручке на тестовую сессию dbsession" + mocker.patch.object(db.__class__, "session", property(lambda self: dbsession)) + + @pytest.fixture def client(mocker): user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') @@ -214,11 +238,9 @@ def lecturers(dbsession): Lecturer(id=4, first_name='test_fname3', last_name='test_lname3', middle_name='test_mname3', timetable_id=9903) ) lecturers[-1].is_deleted = True - for lecturer in lecturers: - dbsession.add(lecturer) + dbsession.add_all(lecturers) dbsession.commit() yield lecturers - for lecturer in lecturers: for row in lecturer.comments: dbsession.delete(row) From b50733f64b9d8aca2e5b8f62de73bc3b6e03707b Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 18 May 2026 21:25:43 +0300 Subject: [PATCH 02/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20relationship=20=D0=B4=D0=BB=D1=8F=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=20Lectorer=20=D0=B8=20LectorerUs?= =?UTF-8?q?erComment=20=D1=81=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9=20cascade,=20=D1=87=D1=82=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B0=D0=BB=D1=85=D0=B8=D0=BC=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BD=D1=8F=D1=82=D1=8C=20=D0=B2=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BA=D0=B5?= =?UTF-8?q?=20=D0=B8=D1=85=20=D1=83=D0=B4=D0=B0=D0=BB=D1=8F=D1=82=D1=8C,?= =?UTF-8?q?=20=D0=B2=D0=BD=D0=B5=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=BE=D1=82=20=D1=81=D0=B2?= =?UTF-8?q?=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D1=80=D1=83?= =?UTF-8?q?=D0=B3=D0=B8=D1=85=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/models/db.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 1cdc664..120e70f 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -50,6 +50,7 @@ class Lecturer(BaseDbModel): avatar_link: Mapped[str] = mapped_column(String, nullable=True, comment="Ссылка на аву препода") timetable_id: Mapped[int] comments: Mapped[list[Comment]] = relationship("Comment", back_populates="lecturer") + lecturer_user_comments: Mapped[list[LecturerUserComment]] = relationship("LecturerUserComment", back_populates="lecturer_comments", cascade="all, delete-orphan") mark_weighted: Mapped[float] = mapped_column( Float, nullable=False, @@ -288,6 +289,7 @@ class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(Integer, nullable=False) lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) + lecturer_comments: Mapped[Lecturer] = relationship("Lecturer", back_populates="lecturer_user_comments") create_ts: Mapped[datetime.datetime] = mapped_column( DateTime, default=datetime.datetime.now(datetime.timezone.utc), nullable=False ) From 7e00a0d8a58d89a48c417df84bbca3bdf5f878a8 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 18 May 2026 21:28:12 +0300 Subject: [PATCH 03/38] =?UTF-8?q?=D0=92=20.gitignore=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B8=D1=81=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=B0=20uv.lock=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=B4=D0=B6=D0=B5=D1=80=D0=B0=20uv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b6e4761..b85d216 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# uv pocket manager +uv.lock From 832d434a5aca72c0bd0ae49b8f5b9f18c94e5441 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 00:46:07 +0300 Subject: [PATCH 04/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BC=D0=BE=D0=BA=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D1=81=20use?= =?UTF-8?q?rdata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 54 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8831f7e..07603ef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,8 +10,10 @@ from alembic.config import Config as AlembicConfig from fastapi.testclient import TestClient from sqlalchemy import create_engine + from sqlalchemy import event from fastapi_sqlalchemy import db + from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer @@ -19,6 +21,7 @@ from rating_api.routes import app from rating_api.settings import Settings, get_settings +from auth_lib.fastapi import UnionAuth class PostgresConfig: """Дата-класс со значениями для контейнера с тестовой БД и alembic-миграции.""" @@ -116,16 +119,47 @@ def override_dbsession_in_route(dbsession, mocker): @pytest.fixture -def client(mocker): - user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') - user_mock.return_value = { - "session_scopes": [{"id": 0, "name": "string", "comment": "string"}], - "user_scopes": [{"id": 0, "name": "string", "comment": "string"}], - "indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}], - "groups": [{"id": 0, "name": "string", "parent_id": 0}], - "id": 0, - "email": "string", - } +def authlib_user(): + """Данные о пользователе, возвращаемые сервисом auth. + """ + return {"auth_methods":["email","github_auth"], + "session_scopes":[ + {"id":145,"name":"auth.session.create"}, + {"id":146,"name":"auth.session.update"}, + {"id":165,"name":"auth.user.selfdelete"} + ], + "user_scopes":[ + {"id":145,"name":"auth.session.create"}, + {"id":146,"name":"auth.session.update"}, + {"id":165,"name":"auth.user.selfdelete"} + ], + "indirect_groups":[99], + "groups":[99], + "id":0, + "email":"aslimbo2001@gmail.com", + "userdata":[ + {"category":"Личная информация","param":"Полное имя","value":"Namazov Maksim"}, + {"category":"Личная информация","param":"Фото","value":"https://avatars.githubusercontent.com/u/192724282?v=4"}, + {"category":"Контакты","param":"Имя пользователя GitHub","value":"CaseAsLimbo"} + ] + } + + + +@pytest.fixture() +def authlib_mock(mocker): + auth_mock = mocker.patch("auth_lib.fastapi.UnionAuth.__call__") + return auth_mock + + +@pytest.fixture() +def user_mock(authlib_mock, authlib_user): + auth_mock = authlib_mock.return_value = authlib_user + return auth_mock + + +@pytest.fixture +def client(mocker, user_mock): client = TestClient(app) return client From bbd0fb3ca0dbd18aa9ecb71fc635d240ffb7319f Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 17:07:11 +0300 Subject: [PATCH 05/38] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B0=D1=82=D1=80=D0=B8=D0=B1=D1=83=D1=82=D0=B0=20lecturer=5Fc?= =?UTF-8?q?omments=20=D0=BD=D0=B0=20lecturer=20=D0=B2=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=D0=B5=20LecturerUserComments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/models/db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 120e70f..31af7ec 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -50,7 +50,7 @@ class Lecturer(BaseDbModel): avatar_link: Mapped[str] = mapped_column(String, nullable=True, comment="Ссылка на аву препода") timetable_id: Mapped[int] comments: Mapped[list[Comment]] = relationship("Comment", back_populates="lecturer") - lecturer_user_comments: Mapped[list[LecturerUserComment]] = relationship("LecturerUserComment", back_populates="lecturer_comments", cascade="all, delete-orphan") + lecturer_user_comments: Mapped[list[LecturerUserComment]] = relationship("LecturerUserComment", back_populates="lecturer", cascade="all, delete-orphan") mark_weighted: Mapped[float] = mapped_column( Float, nullable=False, @@ -289,7 +289,7 @@ class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(Integer, nullable=False) lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) - lecturer_comments: Mapped[Lecturer] = relationship("Lecturer", back_populates="lecturer_user_comments") + lecturer: Mapped[Lecturer] = relationship("Lecturer", back_populates="lecturer_user_comments") create_ts: Mapped[datetime.datetime] = mapped_column( DateTime, default=datetime.datetime.now(datetime.timezone.utc), nullable=False ) From ff6c6760b3fec2d5422060dd6e87f3b4c4bed194 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 21:08:54 +0300 Subject: [PATCH 06/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B2=20userdata=20=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 11 +++++++---- tests/test_routes/test_comment.py | 9 +++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 07603ef..f4b4e8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,13 +114,16 @@ def sql_requests_listener(**kw_entities_of_executing): @pytest.fixture() def override_dbsession_in_route(dbsession, mocker): - "Мок для подмены db.session в ручке на тестовую сессию dbsession" + """ + Мок для подмены db.session в ручке на тестовую сессию dbsession + """ mocker.patch.object(db.__class__, "session", property(lambda self: dbsession)) @pytest.fixture def authlib_user(): - """Данные о пользователе, возвращаемые сервисом auth. + """ + Данные о пользователе, возвращаемые сервисом auth. """ return {"auth_methods":["email","github_auth"], "session_scopes":[ @@ -154,8 +157,8 @@ def authlib_mock(mocker): @pytest.fixture() def user_mock(authlib_mock, authlib_user): - auth_mock = authlib_mock.return_value = authlib_user - return auth_mock + authlib_mock.return_value = authlib_user + return authlib_mock @pytest.fixture diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 0ea2692..6776096 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -7,6 +7,7 @@ from rating_api.models import Comment, CommentReaction, LecturerUserComment, Reaction, ReviewStatus from rating_api.settings import get_settings +from auth_lib.fastapi import UnionAuth logger = logging.getLogger(__name__) url: str = '/comment' @@ -177,6 +178,14 @@ def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response_status): params = {"lecturer_id": lecturers[lecturer_n].id} post_response = client.post(url, json=body, params=params) + + # Проверка корректности переданных в userdata "param" + user = UnionAuth.__call__(post_response) + acceptable_params = ["Полное имя", "Фото", "Имя пользователя GitHub", "Номер Телефона"] + real_params = [i["param"] for i in user.get("userdata")] + for i in real_params: + assert i in acceptable_params, f"Не допустимый параметр: \"{i}\"! Список допустимых параметров: {acceptable_params}" + assert post_response.status_code == response_status if response_status == status.HTTP_200_OK: comment = Comment.query(session=dbsession).filter(Comment.uuid == post_response.json()["uuid"]).one_or_none() From c27747138c955b99bcab26955a7b8bf95fc1f3ce Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 21:30:12 +0300 Subject: [PATCH 07/38] =?UTF-8?q?=D0=91=D1=8B=D0=BB=D0=B8=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B4=D0=BE=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=81=D0=BB=D1=83=D1=88=D0=B0=D1=82=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20BEGIN,=20COMMIT?= =?UTF-8?q?=20=D0=B8=20ROLLBACK=20=D0=B2=20=D1=84=D0=B8=D0=BA=D1=81=D1=82?= =?UTF-8?q?=D1=83=D1=80=D1=83=20logging=5Fsql=5Freq=5Fbefore=5Fexecute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index f4b4e8a..523b6ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,14 +101,33 @@ def logging_sql_req_before_execute(dbsession): """ Фикстура для логирования всех сформированных в рамках одной транзакции SQL-запросов, до их отправки в бд, то есть до Session.flush() или - до Session.commit(). + до Session.commit(), а так же событий сессии BEGIN, COMMIT и ROLLBACK """ engine = dbsession.get_bind() + @event.listens_for(engine, "before_execute", named=True) def sql_requests_listener(**kw_entities_of_executing): print("\n========= SQL command =========\n") print(f"SQL was sended: {kw_entities_of_executing.get("clauseelement")}\n") print("===============================\n") + + @event.listens_for(dbsession, "after_begin", named=True) + def begin_listener(**kw): + print("\n========= BEGIN =========\n") + print(f"BEGIN was executed\n") + print("===============================\n") + + @event.listens_for(dbsession, "after_rollback", named=True) + def begin_listener(**kw): + print("\n========= ROLLBACK =========\n") + print(f"ROLLBACK was executed\n") + print("===============================\n") + + @event.listens_for(dbsession, "after_commit", named=True) + def begin_listener(**kw): + print("\n========= COMMIT =========\n") + print(f"COMMIT was executed\n") + print("===============================\n") From 623bda4ec5f7bd849dd1a9a1a31f8c7a6461adfc Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 21:45:55 +0300 Subject: [PATCH 08/38] =?UTF-8?q?=D0=91=D1=8B=D0=BB=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D1=8B=20black=20=D0=B8=20i?= =?UTF-8?q?sort=20T=5FT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/models/db.py | 4 +- tests/conftest.py | 65 +++++++++++++++---------------- tests/test_routes/test_comment.py | 8 ++-- 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 31af7ec..5c75575 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -50,7 +50,9 @@ class Lecturer(BaseDbModel): avatar_link: Mapped[str] = mapped_column(String, nullable=True, comment="Ссылка на аву препода") timetable_id: Mapped[int] comments: Mapped[list[Comment]] = relationship("Comment", back_populates="lecturer") - lecturer_user_comments: Mapped[list[LecturerUserComment]] = relationship("LecturerUserComment", back_populates="lecturer", cascade="all, delete-orphan") + lecturer_user_comments: Mapped[list[LecturerUserComment]] = relationship( + "LecturerUserComment", back_populates="lecturer", cascade="all, delete-orphan" + ) mark_weighted: Mapped[float] = mapped_column( Float, nullable=False, diff --git a/tests/conftest.py b/tests/conftest.py index 523b6ea..f94524a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,11 +9,8 @@ from alembic import command from alembic.config import Config as AlembicConfig from fastapi.testclient import TestClient -from sqlalchemy import create_engine - -from sqlalchemy import event from fastapi_sqlalchemy import db - +from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer @@ -21,7 +18,6 @@ from rating_api.routes import app from rating_api.settings import Settings, get_settings -from auth_lib.fastapi import UnionAuth class PostgresConfig: """Дата-класс со значениями для контейнера с тестовой БД и alembic-миграции.""" @@ -100,7 +96,7 @@ def dbsession(db_container): def logging_sql_req_before_execute(dbsession): """ Фикстура для логирования всех сформированных в рамках одной транзакции - SQL-запросов, до их отправки в бд, то есть до Session.flush() или + SQL-запросов, до их отправки в бд, то есть до Session.flush() или до Session.commit(), а так же событий сессии BEGIN, COMMIT и ROLLBACK """ engine = dbsession.get_bind() @@ -108,27 +104,26 @@ def logging_sql_req_before_execute(dbsession): @event.listens_for(engine, "before_execute", named=True) def sql_requests_listener(**kw_entities_of_executing): print("\n========= SQL command =========\n") - print(f"SQL was sended: {kw_entities_of_executing.get("clauseelement")}\n") + print(f"SQL was sended: {kw_entities_of_executing.get('clauseelement')}\n") print("===============================\n") @event.listens_for(dbsession, "after_begin", named=True) def begin_listener(**kw): print("\n========= BEGIN =========\n") print(f"BEGIN was executed\n") - print("===============================\n") + print("===============================\n") @event.listens_for(dbsession, "after_rollback", named=True) def begin_listener(**kw): print("\n========= ROLLBACK =========\n") print(f"ROLLBACK was executed\n") - print("===============================\n") + print("===============================\n") @event.listens_for(dbsession, "after_commit", named=True) def begin_listener(**kw): print("\n========= COMMIT =========\n") print(f"COMMIT was executed\n") - print("===============================\n") - + print("===============================\n") @pytest.fixture() @@ -144,28 +139,32 @@ def authlib_user(): """ Данные о пользователе, возвращаемые сервисом auth. """ - return {"auth_methods":["email","github_auth"], - "session_scopes":[ - {"id":145,"name":"auth.session.create"}, - {"id":146,"name":"auth.session.update"}, - {"id":165,"name":"auth.user.selfdelete"} - ], - "user_scopes":[ - {"id":145,"name":"auth.session.create"}, - {"id":146,"name":"auth.session.update"}, - {"id":165,"name":"auth.user.selfdelete"} - ], - "indirect_groups":[99], - "groups":[99], - "id":0, - "email":"aslimbo2001@gmail.com", - "userdata":[ - {"category":"Личная информация","param":"Полное имя","value":"Namazov Maksim"}, - {"category":"Личная информация","param":"Фото","value":"https://avatars.githubusercontent.com/u/192724282?v=4"}, - {"category":"Контакты","param":"Имя пользователя GitHub","value":"CaseAsLimbo"} - ] - } - + return { + "auth_methods": ["email", "github_auth"], + "session_scopes": [ + {"id": 145, "name": "auth.session.create"}, + {"id": 146, "name": "auth.session.update"}, + {"id": 165, "name": "auth.user.selfdelete"}, + ], + "user_scopes": [ + {"id": 145, "name": "auth.session.create"}, + {"id": 146, "name": "auth.session.update"}, + {"id": 165, "name": "auth.user.selfdelete"}, + ], + "indirect_groups": [99], + "groups": [99], + "id": 0, + "email": "aslimbo2001@gmail.com", + "userdata": [ + {"category": "Личная информация", "param": "Полное имя", "value": "Namazov Maksim"}, + { + "category": "Личная информация", + "param": "Фото", + "value": "https://avatars.githubusercontent.com/u/192724282?v=4", + }, + {"category": "Контакты", "param": "Имя пользователя GitHub", "value": "CaseAsLimbo"}, + ], + } @pytest.fixture() diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 6776096..38ae9e8 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -2,12 +2,12 @@ import logging import pytest +from auth_lib.fastapi import UnionAuth from starlette import status from rating_api.models import Comment, CommentReaction, LecturerUserComment, Reaction, ReviewStatus from rating_api.settings import get_settings -from auth_lib.fastapi import UnionAuth logger = logging.getLogger(__name__) url: str = '/comment' @@ -178,13 +178,15 @@ def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response_status): params = {"lecturer_id": lecturers[lecturer_n].id} post_response = client.post(url, json=body, params=params) - + # Проверка корректности переданных в userdata "param" user = UnionAuth.__call__(post_response) acceptable_params = ["Полное имя", "Фото", "Имя пользователя GitHub", "Номер Телефона"] real_params = [i["param"] for i in user.get("userdata")] for i in real_params: - assert i in acceptable_params, f"Не допустимый параметр: \"{i}\"! Список допустимых параметров: {acceptable_params}" + assert ( + i in acceptable_params + ), f"Не допустимый параметр: \"{i}\"! Список допустимых параметров: {acceptable_params}" assert post_response.status_code == response_status if response_status == status.HTTP_200_OK: From cc84d3705921a586ad634699b9b6abe2b63d2f64 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 23:12:13 +0300 Subject: [PATCH 09/38] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=87=D0=BD=D1=83=D1=8E=20=D0=B8=D0=BD=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f94524a..41f4ae0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -156,13 +156,7 @@ def authlib_user(): "id": 0, "email": "aslimbo2001@gmail.com", "userdata": [ - {"category": "Личная информация", "param": "Полное имя", "value": "Namazov Maksim"}, - { - "category": "Личная информация", - "param": "Фото", - "value": "https://avatars.githubusercontent.com/u/192724282?v=4", - }, - {"category": "Контакты", "param": "Имя пользователя GitHub", "value": "CaseAsLimbo"}, + {"category": "Личная информация", "param": "Полное имя", "value": "Тестовый Тест"}, ], } From f4be566d4811a783187eb05402c750796547d046 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Wed, 20 May 2026 00:27:51 +0300 Subject: [PATCH 10/38] =?UTF-8?q?=D0=B1=D1=8B=D0=BB=D0=B8=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D0=B8=D0=BA=D1=81=D1=82?= =?UTF-8?q?=D1=80=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20dbsession?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 45 +------------------------------ tests/test_routes/test_comment.py | 4 +-- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 41f4ae0..561c6e8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,8 +9,7 @@ from alembic import command from alembic.config import Config as AlembicConfig from fastapi.testclient import TestClient -from fastapi_sqlalchemy import db -from sqlalchemy import create_engine, event +from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer @@ -92,48 +91,6 @@ def dbsession(db_container): yield session -@pytest.fixture() -def logging_sql_req_before_execute(dbsession): - """ - Фикстура для логирования всех сформированных в рамках одной транзакции - SQL-запросов, до их отправки в бд, то есть до Session.flush() или - до Session.commit(), а так же событий сессии BEGIN, COMMIT и ROLLBACK - """ - engine = dbsession.get_bind() - - @event.listens_for(engine, "before_execute", named=True) - def sql_requests_listener(**kw_entities_of_executing): - print("\n========= SQL command =========\n") - print(f"SQL was sended: {kw_entities_of_executing.get('clauseelement')}\n") - print("===============================\n") - - @event.listens_for(dbsession, "after_begin", named=True) - def begin_listener(**kw): - print("\n========= BEGIN =========\n") - print(f"BEGIN was executed\n") - print("===============================\n") - - @event.listens_for(dbsession, "after_rollback", named=True) - def begin_listener(**kw): - print("\n========= ROLLBACK =========\n") - print(f"ROLLBACK was executed\n") - print("===============================\n") - - @event.listens_for(dbsession, "after_commit", named=True) - def begin_listener(**kw): - print("\n========= COMMIT =========\n") - print(f"COMMIT was executed\n") - print("===============================\n") - - -@pytest.fixture() -def override_dbsession_in_route(dbsession, mocker): - """ - Мок для подмены db.session в ручке на тестовую сессию dbsession - """ - mocker.patch.object(db.__class__, "session", property(lambda self: dbsession)) - - @pytest.fixture def authlib_user(): """ diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 38ae9e8..166a09d 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -183,9 +183,9 @@ def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response user = UnionAuth.__call__(post_response) acceptable_params = ["Полное имя", "Фото", "Имя пользователя GitHub", "Номер Телефона"] real_params = [i["param"] for i in user.get("userdata")] - for i in real_params: + for param in real_params: assert ( - i in acceptable_params + param in acceptable_params ), f"Не допустимый параметр: \"{i}\"! Список допустимых параметров: {acceptable_params}" assert post_response.status_code == response_status From 6077bbd5fd240981baccdd3412ed583b59101d49 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 16:13:58 +0300 Subject: [PATCH 11/38] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=81=D0=BA=D0=B0=D0=B4=D0=BD=D0=BE=D0=B5=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/models/db.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 5c75575..1cdc664 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -50,9 +50,6 @@ class Lecturer(BaseDbModel): avatar_link: Mapped[str] = mapped_column(String, nullable=True, comment="Ссылка на аву препода") timetable_id: Mapped[int] comments: Mapped[list[Comment]] = relationship("Comment", back_populates="lecturer") - lecturer_user_comments: Mapped[list[LecturerUserComment]] = relationship( - "LecturerUserComment", back_populates="lecturer", cascade="all, delete-orphan" - ) mark_weighted: Mapped[float] = mapped_column( Float, nullable=False, @@ -291,7 +288,6 @@ class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(Integer, nullable=False) lecturer_id: Mapped[int] = mapped_column(Integer, ForeignKey("lecturer.id")) - lecturer: Mapped[Lecturer] = relationship("Lecturer", back_populates="lecturer_user_comments") create_ts: Mapped[datetime.datetime] = mapped_column( DateTime, default=datetime.datetime.now(datetime.timezone.utc), nullable=False ) From 90b7ff583cfccfb4bbb8d667a0c2136ae1f97ea7 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 16:16:59 +0300 Subject: [PATCH 12/38] =?UTF-8?q?=D0=92=D0=BE=D0=B7=D1=80=D0=B0=D1=89?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B0=20=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80=D1=8B?= =?UTF-8?q?=20lecturers;=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BA=D0=BE=D1=83?= =?UTF-8?q?=D0=BF=D1=8B=20=D0=B8=D0=B7=20=D0=BC=D0=BE=D0=BA=D0=B0=20=D1=8E?= =?UTF-8?q?=D0=B7=D0=B5=D1=80=D0=B0;=20=D0=BE=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BC=D0=BE=D0=BA=D0=B0=20=D1=8E=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=B0,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=BD=D0=B5?= =?UTF-8?q?=20=D1=85=D0=B0=D1=80=D0=B4=D0=BA=D0=BE=D0=B4=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20userdata=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 561c6e8..1e1edfe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -97,21 +97,12 @@ def authlib_user(): Данные о пользователе, возвращаемые сервисом auth. """ return { - "auth_methods": ["email", "github_auth"], - "session_scopes": [ - {"id": 145, "name": "auth.session.create"}, - {"id": 146, "name": "auth.session.update"}, - {"id": 165, "name": "auth.user.selfdelete"}, - ], - "user_scopes": [ - {"id": 145, "name": "auth.session.create"}, - {"id": 146, "name": "auth.session.update"}, - {"id": 165, "name": "auth.user.selfdelete"}, - ], - "indirect_groups": [99], - "groups": [99], + "session_scopes": [{"id": 0, "name": "string", "comment": "string"}], + "user_scopes": [{"id": 0, "name": "string", "comment": "string"}], + "indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}], + "groups": [{"id": 0, "name": "string", "parent_id": 0}], "id": 0, - "email": "aslimbo2001@gmail.com", + "email": "string", "userdata": [ {"category": "Личная информация", "param": "Полное имя", "value": "Тестовый Тест"}, ], @@ -135,7 +126,6 @@ def client(mocker, user_mock): client = TestClient(app) return client - @pytest.fixture def lecturer(dbsession): _lecturer = Lecturer(first_name="test_fname", last_name="test_lname", middle_name="test_mname", timetable_id=9900) @@ -244,7 +234,8 @@ def lecturers(dbsession): Lecturer(id=4, first_name='test_fname3', last_name='test_lname3', middle_name='test_mname3', timetable_id=9903) ) lecturers[-1].is_deleted = True - dbsession.add_all(lecturers) + for lecturer in lecturers: + dbsession.add(lecturer) dbsession.commit() yield lecturers for lecturer in lecturers: From 2a81afdfa0dcacb7c6e02a917617604b9169ddfd Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 16:19:12 +0300 Subject: [PATCH 13/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=D1=8F=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20user=5Fid=20=D0=B8=20fullname;=20=D1=83=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20userdata=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BC=D0=BE=D0=BA=D0=B0?= =?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_routes/test_comment.py | 110 +++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 11 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 166a09d..cac3720 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -2,7 +2,6 @@ import logging import pytest -from auth_lib.fastapi import UnionAuth from starlette import status from rating_api.models import Comment, CommentReaction, LecturerUserComment, Reaction, ReviewStatus @@ -175,24 +174,25 @@ ), ], ) -def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response_status): +def test_create_comment(client, dbsession, lecturers, authlib_user, body, lecturer_n, response_status): params = {"lecturer_id": lecturers[lecturer_n].id} post_response = client.post(url, json=body, params=params) - # Проверка корректности переданных в userdata "param" - user = UnionAuth.__call__(post_response) - acceptable_params = ["Полное имя", "Фото", "Имя пользователя GitHub", "Номер Телефона"] - real_params = [i["param"] for i in user.get("userdata")] - for param in real_params: - assert ( - param in acceptable_params - ), f"Не допустимый параметр: \"{i}\"! Список допустимых параметров: {acceptable_params}" - assert post_response.status_code == response_status + if response_status == status.HTTP_200_OK: comment = Comment.query(session=dbsession).filter(Comment.uuid == post_response.json()["uuid"]).one_or_none() assert comment is not None + # проверка корректной записи user_id и fullname при анонимных и не анонимных комментариях + if "is_anonymous" in body: + if body.is_anonymous: + assert comment.user_id is None + assert comment.fullname is None + else: + assert comment.user_id == authlib_user.get("id") + assert comment.fullname == authlib_user.get("userdata")[0]["value"] + if "create_ts" in body: assert comment.create_ts == datetime.datetime.fromisoformat(body["create_ts"]).replace(tzinfo=None) if "update_ts" in body: @@ -206,6 +206,94 @@ def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response assert user_comment is not None +@pytest.mark.parametrize( + "body, total, response_status", + [ + ( + { + "comments": [ + { + "subject": "string", + "text": "string", + "mark_kindness": 0, + "mark_freebie": 0, + "mark_clarity": 0, + "lecturer_id": 1, + "create_ts": "2026-05-25T11:41:26.777Z", + "update_ts": "2026-05-25T11:41:26.777Z", + }, + { + "subject": "string", + "text": "string", + "mark_kindness": 0, + "mark_freebie": 0, + "mark_clarity": 0, + "lecturer_id": 2, + "create_ts": "2026-05-25T11:41:26.777Z", + "update_ts": "2026-05-25T11:41:26.777Z", + }, + ], + }, + 2, + status.HTTP_200_OK, + ), + ( + {"comments": []}, + 0, + status.HTTP_200_OK, + ), + ( + { + "comments": [ + { + "subject": "string", + "text": "string", + "mark_kindness": 0, + "mark_freebie": 0, + "mark_clarity": 0, + "lecturer_id": 4, + "create_ts": "2026-05-25T11:41:26.777Z", + "update_ts": "2026-05-25T11:41:26.777Z", + }, + ], + }, + 1, + status.HTTP_200_OK, + ), + ( + { + "comments": [ + { + "subdject": "string", + "text": "string", + "mark_kindness": 0, + "mark_freebie": 0, + "mark_clarity": 0, + "lecturer_id": "abc", + }, + ], + }, + None, + status.HTTP_422_UNPROCESSABLE_CONTENT, + ), + ], +) +def test_import_comments(client, dbsession, lecturers, body, total, response_status): + response = client.post(f"{url}/import", json=body) + + assert response.status_code == response_status + + new_comments = response.json() + print(new_comments) + + assert total == new_comments.get("total") + + if new_comments.get("total") and total > 0: + for comment in new_comments.get("comments"): + comment_from_db = Comment.query(session=dbsession).filter(Comment.uuid == comment.get("uuid")).one_or_none() + assert comment_from_db is not None + + @pytest.mark.parametrize( "reaction_data, expected_reaction, comment_user_id", [ From 26f46359b3802f2a903ca3ce40a24a19c503d509 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 17:02:53 +0300 Subject: [PATCH 14/38] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20black=20=D0=B8=20isort=20T=5FT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 1e1edfe..ac7677c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -126,6 +126,7 @@ def client(mocker, user_mock): client = TestClient(app) return client + @pytest.fixture def lecturer(dbsession): _lecturer = Lecturer(first_name="test_fname", last_name="test_lname", middle_name="test_mname", timetable_id=9900) From 6a336df97975a86feb22556d2bfc202a0c9bcb23 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 17:09:37 +0300 Subject: [PATCH 15/38] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=20uv?= =?UTF-8?q?.lock=20=D0=B8=D0=B7=20gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index b85d216..b6e4761 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,3 @@ dmypy.json # Pyre type checker .pyre/ - -# uv pocket manager -uv.lock From 0e08f0309954673d5ad7c91791ecfc23648fa4ae Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:42:54 +0300 Subject: [PATCH 16/38] fix status --- tests/test_routes/test_comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index cac3720..075a235 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -274,7 +274,7 @@ def test_create_comment(client, dbsession, lecturers, authlib_user, body, lectur ], }, None, - status.HTTP_422_UNPROCESSABLE_CONTENT, + status.HTTP_422_UNPROCESSABLE_ENTITY, ), ], ) From cb6da6b1aa02dfa03b5f1fb1a00b401ed3a48202 Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:26:04 +0300 Subject: [PATCH 17/38] fixes --- rating_api/routes/comment.py | 22 +--------------------- tests/test_routes/test_comment.py | 6 +++--- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 8f0ea1d..f7ecf66 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -130,27 +130,7 @@ async def create_comment( review_status=ReviewStatus.PENDING, ) - # Выдача аччивки юзеру за первый комментарий - async with aiohttp.ClientSession() as session: - give_achievement = True - async with session.get( - settings.API_URL + f"achievement/user/{user.get('id'):}", - headers={"Accept": "application/json"}, - ) as response: - if response.status == 200: - user_achievements = await response.json() - for achievement in user_achievements.get("achievement", []): - if achievement.get("id") == settings.FIRST_COMMENT_ACHIEVEMENT_ID: - give_achievement = False - break - else: - give_achievement = False - if give_achievement: - session.post( - settings.API_URL - + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{user.get('id'):}", - headers={"Accept": "application/json", "Authorization": settings.ACHIEVEMENT_GIVE_TOKEN}, - ) + return CommentGet.model_validate(new_comment) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 075a235..0d1717e 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -186,12 +186,12 @@ def test_create_comment(client, dbsession, lecturers, authlib_user, body, lectur # проверка корректной записи user_id и fullname при анонимных и не анонимных комментариях if "is_anonymous" in body: - if body.is_anonymous: + if body.get("is_anonymous"): assert comment.user_id is None - assert comment.fullname is None + assert comment.user_fullname is None else: assert comment.user_id == authlib_user.get("id") - assert comment.fullname == authlib_user.get("userdata")[0]["value"] + assert comment.user_fullname == authlib_user.get("userdata")[0]["value"] if "create_ts" in body: assert comment.create_ts == datetime.datetime.fromisoformat(body["create_ts"]).replace(tzinfo=None) From d66ebbbb3ac3b4ab60ab9d6e7cb930cceaac5eaa Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:27:00 +0300 Subject: [PATCH 18/38] fix --- rating_api/routes/comment.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index f7ecf66..8f0ea1d 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -130,7 +130,27 @@ async def create_comment( review_status=ReviewStatus.PENDING, ) - + # Выдача аччивки юзеру за первый комментарий + async with aiohttp.ClientSession() as session: + give_achievement = True + async with session.get( + settings.API_URL + f"achievement/user/{user.get('id'):}", + headers={"Accept": "application/json"}, + ) as response: + if response.status == 200: + user_achievements = await response.json() + for achievement in user_achievements.get("achievement", []): + if achievement.get("id") == settings.FIRST_COMMENT_ACHIEVEMENT_ID: + give_achievement = False + break + else: + give_achievement = False + if give_achievement: + session.post( + settings.API_URL + + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{user.get('id'):}", + headers={"Accept": "application/json", "Authorization": settings.ACHIEVEMENT_GIVE_TOKEN}, + ) return CommentGet.model_validate(new_comment) From 9c23d1ea4ab67474b54ae61fd4bf62f65d415fa3 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 19:12:38 +0300 Subject: [PATCH 19/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20dbsession.flush()=20=D0=BF=D0=BE=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BD=D0=B0=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80=D0=B5=20lecturers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index ac7677c..2024b28 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -247,6 +247,7 @@ def lecturers(dbsession): ) for row in lecturer_user_comments: dbsession.delete(row) + dbsession.flush() dbsession.delete(lecturer) dbsession.commit() From b036e6db21371b9c349c8e54433276550f593182 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 28 Jun 2026 21:39:44 +0300 Subject: [PATCH 20/38] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20aioresponses=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B0=20aiohttp=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.dev.txt b/requirements.dev.txt index 336b8df..2051ef5 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -6,3 +6,4 @@ pytest pytest-cov pytest-mock testcontainers[postgres] +aioresponses From df0d4544eb125c8505d2e0678ba93538564fd69e Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 28 Jun 2026 21:41:06 +0300 Subject: [PATCH 21/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0=20aiohttp=5Fmp=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D1=83=D1=8E=D1=89=D0=B0=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=20=D0=B2=D1=81=D0=B5=D1=85=20aiohttp=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2024b28..378a275 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ import pytest from _pytest.monkeypatch import MonkeyPatch +from aioresponses import aioresponses from alembic import command from alembic.config import Config as AlembicConfig from fastapi.testclient import TestClient @@ -43,6 +44,13 @@ def session_mp(): mp.undo() +@pytest.fixture(autouse=True) +def aiohttp_mp(): + """Фикстура для перехвата любых aiohttp запросов aiohttp.ClientSession()""" + with aioresponses() as aiohttp_mock: + yield aiohttp_mock + + @pytest.fixture(scope='session') def get_settings_mock(session_mp): """Переопределение get_settings в rating_api/settings.py и перезагрузка base.app.""" From 4dab50e2b97463bf6b59152250a541a70ca08990 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 28 Jun 2026 21:42:20 +0300 Subject: [PATCH 22/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B8=20=D0=B0=D1=87=D0=B8=D0=B2=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_comment.py | 107 +++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 3 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 0d1717e..71ff3ae 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -15,9 +15,22 @@ @pytest.mark.parametrize( - 'body,lecturer_n,response_status', + 'body,lecturer_n,response_status,aiohttp_response_status,achievement_id', [ - ( + ( # тест логики выдачи ачивки за первый комментарий + { + "subject": "test_subject", + "text": "test text", + "mark_kindness": 1, + "mark_freebie": 0, + "mark_clarity": 0, + }, + 0, + status.HTTP_200_OK, + status.HTTP_200_OK, + 0, + ), + ( # тест логики блокирующей выдачу ачивки за первый комментарий, если она уже есть у юзера { "subject": "test_subject", "text": "test text", @@ -27,6 +40,21 @@ }, 0, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, + ), + ( # тест логики выдачи ачивки в случае неудачного get-запроса к серверу + { + "subject": "test_subject", + "text": "test text", + "mark_kindness": 1, + "mark_freebie": 0, + "mark_clarity": 0, + }, + 0, + status.HTTP_200_OK, + status.HTTP_500_INTERNAL_SERVER_ERROR, + 0, ), ( { @@ -38,6 +66,8 @@ }, 0, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( { @@ -49,6 +79,8 @@ }, 1, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # bad mark { @@ -60,6 +92,8 @@ }, 2, status.HTTP_400_BAD_REQUEST, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # deleted lecturer { @@ -71,6 +105,8 @@ }, 3, status.HTTP_404_NOT_FOUND, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # Anonymous comment { @@ -83,6 +119,8 @@ }, 0, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # NotAnonymous comment { @@ -95,6 +133,8 @@ }, 0, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # Not provided anonymity { @@ -106,6 +146,8 @@ }, 0, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # Bad anonymity { @@ -118,6 +160,8 @@ }, 0, status.HTTP_422_UNPROCESSABLE_ENTITY, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # regex test { @@ -133,6 +177,8 @@ }, 0, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # forbidden symbols { @@ -147,6 +193,8 @@ }, 0, status.HTTP_400_BAD_REQUEST, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # long comment { @@ -159,6 +207,8 @@ }, 0, status.HTTP_400_BAD_REQUEST, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ( # long comment but not that long { @@ -171,10 +221,42 @@ }, 0, status.HTTP_200_OK, + status.HTTP_200_OK, + settings.FIRST_COMMENT_ACHIEVEMENT_ID, ), ], ) -def test_create_comment(client, dbsession, lecturers, authlib_user, body, lecturer_n, response_status): +def test_create_comment( + client, + dbsession, + lecturers, + authlib_user, + aiohttp_mp, + body, + lecturer_n, + response_status, + aiohttp_response_status, + achievement_id, +): + check_get_response = aiohttp_mp.get( + settings.API_URL + f"achievement/user/{authlib_user.get('id'):}", + status=aiohttp_response_status, + payload={ + "user_id": 0, + "achievement": [ + { + "id": settings.FIRST_COMMENT_ACHIEVEMENT_ID, + } + ], + }, + ) + + check_post_response = aiohttp_mp.post( + settings.API_URL + + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{authlib_user.get('id'):}", + status=200, + ) + params = {"lecturer_id": lecturers[lecturer_n].id} post_response = client.post(url, json=body, params=params) @@ -205,6 +287,25 @@ def test_create_comment(client, dbsession, lecturers, authlib_user, body, lectur ) assert user_comment is not None + # Проверка логики выдачи ачивок + if check_get_response.called: + if aiohttp_response_status == status.HTTP_200_OK: + # Проверяем правильность заголовков get-запроса + headers = check_get_response.requests[0].kwargs["headers"] + assert headers["Accept"] == "application/json" + + if achievement_id != settings.FIRST_COMMENT_ACHIEVEMENT_ID: + assert check_post_response.called + # проверяем правильность заголовков post-запроса + headers = check_post_response.requests[0].kwargs["headers"] + assert headers["Accept"] == "application/json" + assert headers["Authorization"] == settings.ACHIEVEMENT_GIVE_TOKEN + + else: + assert not check_post_response.called + else: + assert not check_post_response.called + @pytest.mark.parametrize( "body, total, response_status", From 887d299ff8b6bd84396dae5665903ced9e6b1168 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Thu, 2 Jul 2026 18:51:33 +0300 Subject: [PATCH 23/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2=20post-=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20await=20=D0=BF=D1=80=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=B0=D1=87=D0=B8=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=8E=D0=B7=D0=B5=D1=80=D1=83,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=20=D0=B5=D0=B3=D0=BE=20=D0=B1=D1=8B=D0=BB?= =?UTF-8?q?=D0=BE=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=BE=D0=BA=D0=B0=D1=82=D1=8C=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=B8=D1=82=D1=8C=20=D1=85?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/routes/comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 8f0ea1d..0648f19 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -146,7 +146,7 @@ async def create_comment( else: give_achievement = False if give_achievement: - session.post( + await session.post( settings.API_URL + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{user.get('id'):}", headers={"Accept": "application/json", "Authorization": settings.ACHIEVEMENT_GIVE_TOKEN}, From 0490856f59997881e62876e4e5f1a5d0f5bd11fd Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Thu, 2 Jul 2026 18:53:14 +0300 Subject: [PATCH 24/38] =?UTF-8?q?=D0=A1=D0=BD=D1=8F=D1=82=20=D1=84=D0=BB?= =?UTF-8?q?=D0=B0=D0=B3=20autouse=3DTrue=20=D1=81=20=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=D1=82=D1=83=D1=80=D1=8B=20=D0=BF=D0=BE=D0=B4=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20aiohttp=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BE=D0=B2,=20=D0=B7=D0=B0=20=D0=BD=D0=B5=20=D0=BD=D1=83?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 378a275..0f002a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,7 @@ def session_mp(): mp.undo() -@pytest.fixture(autouse=True) +@pytest.fixture def aiohttp_mp(): """Фикстура для перехвата любых aiohttp запросов aiohttp.ClientSession()""" with aioresponses() as aiohttp_mock: From 615728532ae22243b208ee87ac81d52c11da3e97 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Thu, 2 Jul 2026 18:55:16 +0300 Subject: [PATCH 25/38] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0(=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0)=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B0=D1=87?= =?UTF-8?q?=D0=B8=D0=B2=D0=BA=D0=B8;=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B4=D0=BE=D0=BF.=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8?= =?UTF-8?q?=D0=B5=D0=B2;=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=BC=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_comment.py | 78 ++++++++++++++++++------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 71ff3ae..0e268f8 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -61,6 +61,7 @@ "subject": "test_subject", "text": "test text", "mark_kindness": 1, + "mark_freebie": 0, "mark_clarity": 0, }, @@ -238,23 +239,24 @@ def test_create_comment( aiohttp_response_status, achievement_id, ): - check_get_response = aiohttp_mp.get( - settings.API_URL + f"achievement/user/{authlib_user.get('id'):}", + achive_get_url = settings.API_URL + f"achievement/user/{authlib_user.get('id'):}" + achive_post_url = settings.API_URL + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{authlib_user.get('id'):}" + + aiohttp_mp.get( + achive_get_url, status=aiohttp_response_status, payload={ - "user_id": 0, + "user_id": authlib_user.get("id"), "achievement": [ { - "id": settings.FIRST_COMMENT_ACHIEVEMENT_ID, + "id": achievement_id, } ], }, ) - - check_post_response = aiohttp_mp.post( - settings.API_URL - + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{authlib_user.get('id'):}", - status=200, + aiohttp_mp.post( + achive_post_url, + status=status.HTTP_200_OK, ) params = {"lecturer_id": lecturers[lecturer_n].id} @@ -265,15 +267,15 @@ def test_create_comment( if response_status == status.HTTP_200_OK: comment = Comment.query(session=dbsession).filter(Comment.uuid == post_response.json()["uuid"]).one_or_none() assert comment is not None + assert comment.review_status is ReviewStatus.PENDING # проверка корректной записи user_id и fullname при анонимных и не анонимных комментариях - if "is_anonymous" in body: - if body.get("is_anonymous"): - assert comment.user_id is None - assert comment.user_fullname is None - else: - assert comment.user_id == authlib_user.get("id") - assert comment.user_fullname == authlib_user.get("userdata")[0]["value"] + if body.get("is_anonymous") is not False: + assert comment.user_id is None + assert comment.user_fullname is None + else: + assert comment.user_id == authlib_user.get("id") + assert comment.user_fullname == authlib_user.get("userdata")[0]["value"] if "create_ts" in body: assert comment.create_ts == datetime.datetime.fromisoformat(body["create_ts"]).replace(tzinfo=None) @@ -286,25 +288,35 @@ def test_create_comment( .one_or_none() ) assert user_comment is not None - + # Проверка логики выдачи ачивок - if check_get_response.called: - if aiohttp_response_status == status.HTTP_200_OK: - # Проверяем правильность заголовков get-запроса - headers = check_get_response.requests[0].kwargs["headers"] - assert headers["Accept"] == "application/json" - - if achievement_id != settings.FIRST_COMMENT_ACHIEVEMENT_ID: - assert check_post_response.called - # проверяем правильность заголовков post-запроса - headers = check_post_response.requests[0].kwargs["headers"] - assert headers["Accept"] == "application/json" - assert headers["Authorization"] == settings.ACHIEVEMENT_GIVE_TOKEN - - else: - assert not check_post_response.called + check_post_response = False + get_headers = {} + post_headers = {} + + for k, v in aiohttp_mp.requests.items(): + if k[0] == "GET" and str(k[1]) == achive_get_url: + get_headers = v[0].kwargs.get("headers", {}) + + if k[0] == "POST" and str(k[1]) == achive_post_url: + check_post_response = True + post_headers = v[0].kwargs.get("headers", {}) + + + if aiohttp_response_status == status.HTTP_200_OK: + # Проверяем правильность заголовков get-запроса + assert get_headers.get("Accept") == "application/json" + + if achievement_id != settings.FIRST_COMMENT_ACHIEVEMENT_ID: + assert check_post_response + # проверяем правильность заголовков post-запроса + assert post_headers.get("Accept") == "application/json" + assert post_headers.get("Authorization") == settings.ACHIEVEMENT_GIVE_TOKEN + else: - assert not check_post_response.called + assert not check_post_response + else: + assert not check_post_response @pytest.mark.parametrize( From ec52cc093c9ed63a326b124cb9ee278e6b1cffca Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 4 Jul 2026 03:05:33 +0300 Subject: [PATCH 26/38] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20assert=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B4=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=B2=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B5=20test=5Fget=5Fcomment=5Fwith=5Freaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_comment.py | 39 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 0e268f8..0c2f7b2 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -408,22 +408,22 @@ def test_import_comments(client, dbsession, lecturers, body, total, response_sta @pytest.mark.parametrize( - "reaction_data, expected_reaction, comment_user_id", + "reaction_data, expected_reaction, comment_user_id, response_status", [ - (None, None, 0), - ((0, Reaction.LIKE), "is_liked", 0), # my like on my comment - ((0, Reaction.DISLIKE), "is_disliked", 0), - ((999, Reaction.LIKE), None, 0), # someone else's like on my comment - ((999, Reaction.DISLIKE), None, 0), - ((0, Reaction.LIKE), "is_liked", 999), # my like on someone else's comment - ((0, Reaction.DISLIKE), "is_disliked", 999), - ((333, Reaction.LIKE), None, 999), # someone else's like on another person's comment - ((333, Reaction.DISLIKE), None, 999), - (None, None, None), # anonymous + (None, None, 0, status.HTTP_200_OK), + ((0, Reaction.LIKE), "is_liked", 0, status.HTTP_200_OK), # my like on my comment + ((0, Reaction.DISLIKE), "is_disliked", 0, status.HTTP_200_OK), + ((999, Reaction.LIKE), None, 0, status.HTTP_200_OK), # someone else's like on my comment + ((999, Reaction.DISLIKE), None, 0, status.HTTP_200_OK), + ((0, Reaction.LIKE), "is_liked", 999, status.HTTP_200_OK), # my like on someone else's comment + ((0, Reaction.DISLIKE), "is_disliked", 999, status.HTTP_200_OK), + ((333, Reaction.LIKE), None, 999, status.HTTP_200_OK), # someone else's like on another person's comment + ((333, Reaction.DISLIKE), None, 999, status.HTTP_200_OK), + (None, None, None, status.HTTP_200_OK), # anonymous ], ) def test_get_comment_with_reaction( - client, comment, reaction_data, expected_reaction, comment_user_id, comment_reaction + client, comment, reaction_data, expected_reaction, comment_user_id, comment_reaction, response_status, ): comment.user_id = comment_user_id @@ -432,16 +432,15 @@ def test_get_comment_with_reaction( comment_reaction(user_id, reaction_type) response_comment = client.get(f'{url}/{comment.uuid}') + + assert response_comment.status_code == response_status - if response_comment: - data = response_comment.json() - if expected_reaction: - assert data[expected_reaction] - else: - assert data["is_liked"] == False - assert data["is_disliked"] == False + data = response_comment.json() + if expected_reaction: + assert data[expected_reaction] else: - assert response_comment.status_code == status.HTTP_404_NOT_FOUND + assert data["is_liked"] == False + assert data["is_disliked"] == False @pytest.fixture From 30b3df6b34094b3c3a6fb2fc9103bad606751089 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 4 Jul 2026 03:07:54 +0300 Subject: [PATCH 27/38] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0/=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=81=20=D0=BA=D0=BE=D0=B4=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20test=5Flecturer=5Frating=5Fupdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_lecturer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_routes/test_lecturer.py b/tests/test_routes/test_lecturer.py index 64b284d..6d598cc 100644 --- a/tests/test_routes/test_lecturer.py +++ b/tests/test_routes/test_lecturer.py @@ -377,8 +377,8 @@ def test_delete_lecturer(client, dbsession, lecturers_with_comments): def test_lecturer_rating_update(client, dbsession, body, response_status): response = client.patch('/lecturer/import_rating', json=[body]) - if response_status == status.HTTP_200_OK: - response_dict = response.json() - assert isinstance(response_dict, dict) + assert response.status_code == response_status + response_dict = response.json() + assert isinstance(response_dict, dict) - assert response_dict["failed"] == 0 + assert response_dict["failed"] == 0 From 2bc0328baf4c1d95840ed8d054fe29edf1b676fd Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 4 Jul 2026 03:11:01 +0300 Subject: [PATCH 28/38] =?UTF-8?q?=D0=B2=20mocker.patch=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BF=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=20autospec=3DTrue,=20=D1=87=D1=82=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=20MagicMock=20=D0=BA=D0=BE=D0=BF=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=20=D1=81=D0=B8=D0=B3=D0=BD=D0=B0=D1=82=D1=83?= =?UTF-8?q?=D1=80=D1=83=20=D0=BC=D0=BE=D0=BA=D1=83=D0=BD=D1=83=D1=82=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0f002a6..4fae4d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -119,7 +119,7 @@ def authlib_user(): @pytest.fixture() def authlib_mock(mocker): - auth_mock = mocker.patch("auth_lib.fastapi.UnionAuth.__call__") + auth_mock = mocker.patch("auth_lib.fastapi.UnionAuth.__call__", autospec=True) return auth_mock From 47dfb2a8e0e1fc74e32bc45e69df630570b49a71 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 4 Jul 2026 03:15:15 +0300 Subject: [PATCH 29/38] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20black=20=D0=B8=20isort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_comment.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 0c2f7b2..b2746c4 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -61,7 +61,6 @@ "subject": "test_subject", "text": "test text", "mark_kindness": 1, - "mark_freebie": 0, "mark_clarity": 0, }, @@ -240,7 +239,10 @@ def test_create_comment( achievement_id, ): achive_get_url = settings.API_URL + f"achievement/user/{authlib_user.get('id'):}" - achive_post_url = settings.API_URL + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{authlib_user.get('id'):}" + achive_post_url = ( + settings.API_URL + + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{authlib_user.get('id'):}" + ) aiohttp_mp.get( achive_get_url, @@ -288,7 +290,7 @@ def test_create_comment( .one_or_none() ) assert user_comment is not None - + # Проверка логики выдачи ачивок check_post_response = False get_headers = {} @@ -302,7 +304,6 @@ def test_create_comment( check_post_response = True post_headers = v[0].kwargs.get("headers", {}) - if aiohttp_response_status == status.HTTP_200_OK: # Проверяем правильность заголовков get-запроса assert get_headers.get("Accept") == "application/json" @@ -423,7 +424,13 @@ def test_import_comments(client, dbsession, lecturers, body, total, response_sta ], ) def test_get_comment_with_reaction( - client, comment, reaction_data, expected_reaction, comment_user_id, comment_reaction, response_status, + client, + comment, + reaction_data, + expected_reaction, + comment_user_id, + comment_reaction, + response_status, ): comment.user_id = comment_user_id @@ -432,8 +439,8 @@ def test_get_comment_with_reaction( comment_reaction(user_id, reaction_type) response_comment = client.get(f'{url}/{comment.uuid}') - - assert response_comment.status_code == response_status + + assert response_comment.status_code == response_status data = response_comment.json() if expected_reaction: From bd22bb8b942a7257a0936415323b65df5db14b4e Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:37:32 +0300 Subject: [PATCH 30/38] checks --- .github/workflows/checks.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index eeea8b5..3d7177b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -25,12 +25,16 @@ jobs: python -m ensurepip python -m pip install --upgrade --no-cache-dir pip python -m pip install --upgrade --no-cache-dir -r requirements.txt -r requirements.dev.txt + - name: Show installed packages + run: pip freeze - name: Migrate DB run: | DB_DSN=postgresql://postgres@localhost:5432/postgres alembic upgrade head - name: Build coverage file + id: pytest + continue-on-error: true run: | - DB_DSN=postgresql://postgres@localhost:5432/postgres pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=rating_api tests/ | tee pytest-coverage.txt + DB_DSN=postgresql://postgres@localhost:5432/postgres pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=rating_api tests/ -vv --tb=long | tee pytest-coverage.txt - name: Print report if: always() run: | @@ -68,5 +72,4 @@ jobs: uses: thollander/actions-comment-pull-request@v2 with: message: | - :poop: Code linting failed, use `black` and `isort` to fix it. - + :poop: Code linting failed, use `black` and `isort` to fix it. \ No newline at end of file From c072a817d24f542294aeced261de1fabe31f68a9 Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:22:39 +0300 Subject: [PATCH 31/38] revert --- .github/workflows/checks.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3d7177b..63d7c7f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -25,16 +25,12 @@ jobs: python -m ensurepip python -m pip install --upgrade --no-cache-dir pip python -m pip install --upgrade --no-cache-dir -r requirements.txt -r requirements.dev.txt - - name: Show installed packages - run: pip freeze - name: Migrate DB run: | DB_DSN=postgresql://postgres@localhost:5432/postgres alembic upgrade head - name: Build coverage file - id: pytest - continue-on-error: true run: | - DB_DSN=postgresql://postgres@localhost:5432/postgres pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=rating_api tests/ -vv --tb=long | tee pytest-coverage.txt + DB_DSN=postgresql://postgres@localhost:5432/postgres pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=rating_api tests/ | tee pytest-coverage.txt - name: Print report if: always() run: | @@ -72,4 +68,4 @@ jobs: uses: thollander/actions-comment-pull-request@v2 with: message: | - :poop: Code linting failed, use `black` and `isort` to fix it. \ No newline at end of file + :poop: Code linting failed, use `black` and `isort` to fix it. From a2a76af425474f3a102c4559f5a279ebb42a8182 Mon Sep 17 00:00:00 2001 From: Petr Cherkasov <88943157+petrCher@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:25:07 +0300 Subject: [PATCH 32/38] Update checks.yml --- .github/workflows/checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 63d7c7f..72b9787 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -69,3 +69,4 @@ jobs: with: message: | :poop: Code linting failed, use `black` and `isort` to fix it. + From 828738f0ae9ae57925eda858beec927a3f7e4bf7 Mon Sep 17 00:00:00 2001 From: Petr Cherkasov <88943157+petrCher@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:25:41 +0300 Subject: [PATCH 33/38] Update checks.yml --- .github/workflows/checks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 72b9787..63d7c7f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -69,4 +69,3 @@ jobs: with: message: | :poop: Code linting failed, use `black` and `isort` to fix it. - From 4a04c1febb98237b2ae54c09cd64f019a26a479c Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:29:33 +0300 Subject: [PATCH 34/38] fix --- .github/workflows/checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 63d7c7f..eeea8b5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -69,3 +69,4 @@ jobs: with: message: | :poop: Code linting failed, use `black` and `isort` to fix it. + From f1280dbac999f55893bb180876620f6c8402d920 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 5 Jul 2026 00:38:36 +0300 Subject: [PATCH 35/38] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=84=D0=B8=D0=BA=D1=81=D1=82=D1=83=D1=80=D0=B0=20aio?= =?UTF-8?q?http=5Fmp.=20=D0=A0=D0=B5=D1=88=D0=B5=D0=BD=D0=BE=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=D0=B7=D0=B0=D1=82=D1=8C=D1=81=D1=8F=20=D0=BE?= =?UTF-8?q?=D1=82=20aioressponses=20=D0=B2=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D1=83=20mocker,=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BC=D1=83?= =?UTF-8?q?=20=D1=87=D1=82=D0=BE=D1=8D=D1=82=D0=B0=20=D0=B1=D0=B8=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA=D0=B0=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B8=D0=BC=D0=B0=20?= =?UTF-8?q?=D1=81=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=D0=BC=D0=B8=20aio?= =?UTF-8?q?http=20(=3D=3D3.14.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/conftest.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4fae4d4..678d91f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,6 @@ import pytest from _pytest.monkeypatch import MonkeyPatch -from aioresponses import aioresponses from alembic import command from alembic.config import Config as AlembicConfig from fastapi.testclient import TestClient @@ -44,13 +43,6 @@ def session_mp(): mp.undo() -@pytest.fixture -def aiohttp_mp(): - """Фикстура для перехвата любых aiohttp запросов aiohttp.ClientSession()""" - with aioresponses() as aiohttp_mock: - yield aiohttp_mock - - @pytest.fixture(scope='session') def get_settings_mock(session_mp): """Переопределение get_settings в rating_api/settings.py и перезагрузка base.app.""" From 86e57e32dfedff5784fbf4509654050f7339748e Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 5 Jul 2026 00:40:25 +0300 Subject: [PATCH 36/38] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=20=D0=BC=D0=BE=D0=BA=20aiohttp=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2=20=D1=81=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=D0=BC=20pytest.mocker=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20aioresponses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_comment.py | 86 +++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index b2746c4..199dc9c 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -231,7 +231,7 @@ def test_create_comment( dbsession, lecturers, authlib_user, - aiohttp_mp, + mocker, body, lecturer_n, response_status, @@ -244,8 +244,21 @@ def test_create_comment( + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{authlib_user.get('id'):}" ) - aiohttp_mp.get( - achive_get_url, + # функция для создания мока ответа на асинхронный запрос + def create_response_mock(status=status.HTTP_200_OK, payload=None): + mock_post_response = mocker.AsyncMock() + mock_post_response.status = status + mock_post_response.json = mocker.AsyncMock(return_value=payload or {}) + return mock_post_response + + # функция для создания мока асинхронного контекстного менеджера(для подмены async with session.get(...) as response: ...) + def create_ae_context_manager(mock_response): + ctx = mocker.AsyncMock() + ctx.__aenter__.return_value = mock_response + return ctx + + # coздаем моки ответов aiohttp get- и post- запросов + mock_get_response = create_response_mock( status=aiohttp_response_status, payload={ "user_id": authlib_user.get("id"), @@ -256,10 +269,26 @@ def test_create_comment( ], }, ) - aiohttp_mp.post( - achive_post_url, - status=status.HTTP_200_OK, - ) + mock_post_response = create_response_mock(payload={}) + + # словари ожидаемых ответов c запросов на конкретный url для side_effect + get_responses = {achive_get_url: create_ae_context_manager(mock_get_response)} + post_responses = {achive_post_url: mock_post_response} + + # функции для side_effect моков get- и post- aiohttp запросов, если запрос был к не тому url, мок всегда вернет 404(не используется для проверки) + def get_side_effect(url, *args, **kwargs): + return get_responses.get(url, create_ae_context_manager(create_response_mock(status=status.HTTP_404_NOT_FOUND))) + + def post_side_effect(url, *args, **kwargs): + return post_responses.get(url, (create_response_mock(status=status.HTTP_404_NOT_FOUND))) + + # создаем мок сессии aiohttp.ClientSession + mock_aiohttp_session = mocker.AsyncMock() + # для мока session.get(...) используем MagicMock вместо AsyncMock, потому что это синхронный метод + mock_aiohttp_session.get = mocker.MagicMock(side_effect=get_side_effect) + mock_aiohttp_session.post.side_effect = post_side_effect + mock_aiohttp_session.__aenter__.return_value = mock_aiohttp_session + mocker.patch("aiohttp.ClientSession", return_value=mock_aiohttp_session) params = {"lecturer_id": lecturers[lecturer_n].id} post_response = client.post(url, json=body, params=params) @@ -291,33 +320,36 @@ def test_create_comment( ) assert user_comment is not None - # Проверка логики выдачи ачивок - check_post_response = False - get_headers = {} - post_headers = {} - - for k, v in aiohttp_mp.requests.items(): - if k[0] == "GET" and str(k[1]) == achive_get_url: - get_headers = v[0].kwargs.get("headers", {}) - - if k[0] == "POST" and str(k[1]) == achive_post_url: - check_post_response = True - post_headers = v[0].kwargs.get("headers", {}) + # Проверка логики ачивки + check_get_response = mock_aiohttp_session.get + check_post_response = mock_aiohttp_session.post if aiohttp_response_status == status.HTTP_200_OK: - # Проверяем правильность заголовков get-запроса - assert get_headers.get("Accept") == "application/json" + # Проверяем правильность заголовков и url get-запроса + get_headers = {"Accept": "application/json"} + try: + check_get_response.assert_any_call(achive_get_url, headers=get_headers) + except AssertionError as e: + raise AssertionError( + f"Ожидался GET-запрос на {achive_get_url} c загловками {get_headers}," + f"но вызов, либо не состоялся, либо были переданы неверные заголовки." + ) from e if achievement_id != settings.FIRST_COMMENT_ACHIEVEMENT_ID: - assert check_post_response - # проверяем правильность заголовков post-запроса - assert post_headers.get("Accept") == "application/json" - assert post_headers.get("Authorization") == settings.ACHIEVEMENT_GIVE_TOKEN + # проверяем правильность заголовков и url post-запроса + post_headers = {"Accept": "application/json", "Authorization": settings.ACHIEVEMENT_GIVE_TOKEN} + try: + check_post_response.assert_any_await(achive_post_url, headers=post_headers) + except AssertionError as e: + raise AssertionError( + f"Ожидался POST-запрос на {achive_post_url} c загловками {post_headers}," + f"но вызов, либо не состоялся, либо были переданы неверные заголовки." + ) else: - assert not check_post_response + check_post_response.assert_not_awaited() else: - assert not check_post_response + check_post_response.assert_not_awaited() @pytest.mark.parametrize( From 0a238c9506da4e34ebe2db7328ca4633cfe2ecf2 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 5 Jul 2026 02:12:30 +0300 Subject: [PATCH 37/38] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=20ai?= =?UTF-8?q?oresponses=20=D0=B8=D0=B7=20requirements.dev.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index 2051ef5..336b8df 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -6,4 +6,3 @@ pytest pytest-cov pytest-mock testcontainers[postgres] -aioresponses From 1388d3d4badc9274b197fa33c545c2c9645db5ef Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 5 Jul 2026 21:32:18 +0300 Subject: [PATCH 38/38] =?UTF-8?q?=D0=9B=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=20aiohttp=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B7=D0=B0=20=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D1=8B=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=20test=5Fcreate=5Fcomment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_comment.py | 110 ++++++++++++++++++------------ 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 199dc9c..95ef37e 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -1,5 +1,6 @@ import datetime import logging +from unittest.mock import AsyncMock, MagicMock import pytest from starlette import status @@ -14,6 +15,65 @@ settings = get_settings() +def create_response_mock(status=status.HTTP_200_OK, payload=None): + """Вспомогательная функция, создающая мок-объекты-ответы типа aiohttp.ClientResponse.""" + mock_post_response = AsyncMock() + mock_post_response.status = status + mock_post_response.json = AsyncMock(return_value=payload or {}) + return mock_post_response + + +def create_ae_context_manager(mock_response): + """ + Вспомогательная функция, создающая мок-объекты имитируютщие асинхронный контекстный + менеджер возвращающий мок-объекты-ответы типа aiohttp.ClientResponse + (для подмены async with session.get(...) as response: ...). + """ + ctx = AsyncMock() + ctx.__aenter__.return_value = mock_response + return ctx + + +def aiohttp_mock(authlib_user_id, aiohttp_response_status, achievement_id, get_url, post_url): + """Функция создающая мок-объект сессию типа aiohttp.ClientSession. Реализует моки get- и post- методов данного объекта.""" + + # url для проверки логики выдачи ачивок + achive_get_url = get_url + achive_post_url = post_url + + # coздаем моки ответов aiohttp get- и post- запросов + mock_get_response = create_response_mock( + status=aiohttp_response_status, + payload={ + "user_id": authlib_user_id, + "achievement": [ + { + "id": achievement_id, + } + ], + }, + ) + mock_post_response = create_response_mock(payload={}) + get_responses = {achive_get_url: create_ae_context_manager(mock_get_response)} + post_responses = {achive_post_url: mock_post_response} + + # функции для side_effect моков get- и post- aiohttp запросов, если запрос был к не тому url, мок всегда вернет 404(не используется для проверки) + def get_side_effect(url, *args, **kwargs): + return get_responses.get(url, create_ae_context_manager(create_response_mock(status=status.HTTP_404_NOT_FOUND))) + + def post_side_effect(url, *args, **kwargs): + return post_responses.get(url, (create_response_mock(status=status.HTTP_404_NOT_FOUND))) + + # создаем мок сессии aiohttp.ClientSession + mock_aiohttp_session = AsyncMock() + # для мока session.get(...) используем MagicMock вместо AsyncMock, потому что это синхронный метод + mock_aiohttp_session.get = MagicMock(side_effect=get_side_effect) + mock_aiohttp_session.post.side_effect = post_side_effect + mock_aiohttp_session.__aenter__.return_value = mock_aiohttp_session + + return mock_aiohttp_session + + @pytest.mark.parametrize( 'body,lecturer_n,response_status,aiohttp_response_status,achievement_id', [ @@ -238,56 +298,22 @@ def test_create_comment( aiohttp_response_status, achievement_id, ): + # url для проверки логики выдачи ачивок achive_get_url = settings.API_URL + f"achievement/user/{authlib_user.get('id'):}" achive_post_url = ( settings.API_URL + f"achievement/achievement/{settings.FIRST_COMMENT_ACHIEVEMENT_ID}/reciever/{authlib_user.get('id'):}" ) - # функция для создания мока ответа на асинхронный запрос - def create_response_mock(status=status.HTTP_200_OK, payload=None): - mock_post_response = mocker.AsyncMock() - mock_post_response.status = status - mock_post_response.json = mocker.AsyncMock(return_value=payload or {}) - return mock_post_response - - # функция для создания мока асинхронного контекстного менеджера(для подмены async with session.get(...) as response: ...) - def create_ae_context_manager(mock_response): - ctx = mocker.AsyncMock() - ctx.__aenter__.return_value = mock_response - return ctx - - # coздаем моки ответов aiohttp get- и post- запросов - mock_get_response = create_response_mock( - status=aiohttp_response_status, - payload={ - "user_id": authlib_user.get("id"), - "achievement": [ - { - "id": achievement_id, - } - ], - }, + # мок aiohttp get- и post- запросов связанных с выдачей ачивки + mock_aiohttp_session = aiohttp_mock( + authlib_user_id=authlib_user.get("id"), + aiohttp_response_status=aiohttp_response_status, + achievement_id=achievement_id, + get_url=achive_get_url, + post_url=achive_post_url, ) - mock_post_response = create_response_mock(payload={}) - - # словари ожидаемых ответов c запросов на конкретный url для side_effect - get_responses = {achive_get_url: create_ae_context_manager(mock_get_response)} - post_responses = {achive_post_url: mock_post_response} - - # функции для side_effect моков get- и post- aiohttp запросов, если запрос был к не тому url, мок всегда вернет 404(не используется для проверки) - def get_side_effect(url, *args, **kwargs): - return get_responses.get(url, create_ae_context_manager(create_response_mock(status=status.HTTP_404_NOT_FOUND))) - - def post_side_effect(url, *args, **kwargs): - return post_responses.get(url, (create_response_mock(status=status.HTTP_404_NOT_FOUND))) - # создаем мок сессии aiohttp.ClientSession - mock_aiohttp_session = mocker.AsyncMock() - # для мока session.get(...) используем MagicMock вместо AsyncMock, потому что это синхронный метод - mock_aiohttp_session.get = mocker.MagicMock(side_effect=get_side_effect) - mock_aiohttp_session.post.side_effect = post_side_effect - mock_aiohttp_session.__aenter__.return_value = mock_aiohttp_session mocker.patch("aiohttp.ClientSession", return_value=mock_aiohttp_session) params = {"lecturer_id": lecturers[lecturer_n].id}