From f85af90b22ccb8db0c162eb12daec889a66a2e57 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 18 May 2026 21:20:06 +0300 Subject: [PATCH 01/24] =?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 9ee40011083dca5146aa11fdda548cc7492d1ffa Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 18 May 2026 21:25:43 +0300 Subject: [PATCH 02/24] =?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 47b118c6db6f2489a5f7ea1308b1ef007fd136a3 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Mon, 18 May 2026 21:28:12 +0300 Subject: [PATCH 03/24] =?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 54ccba8397c14e74ea0753572f2d7dc4fd811f82 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 00:46:07 +0300 Subject: [PATCH 04/24] =?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 944ae37b2f46012f7ff7a7ae5b88b5088ae43884 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 17:07:11 +0300 Subject: [PATCH 05/24] =?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 b00ad8bef665900028f6f033ded706b2b660945b Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 21:08:54 +0300 Subject: [PATCH 06/24] =?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 e5d6cf19ed34689e91eb8f64a0e368126be20444 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 21:30:12 +0300 Subject: [PATCH 07/24] =?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 810eeb4bcfb6a9a7386bd38a18bd3c403b5249f3 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 21:45:55 +0300 Subject: [PATCH 08/24] =?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 a118c238150fd7df198782248bdaa1ca314f4373 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Tue, 19 May 2026 23:12:13 +0300 Subject: [PATCH 09/24] =?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 03429654fc6b8436a055f5fb2be1cc1c3960f816 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Wed, 20 May 2026 00:27:51 +0300 Subject: [PATCH 10/24] =?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 81d86c52e3fdc03057b193aa321633fe02f98e86 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 16:13:58 +0300 Subject: [PATCH 11/24] =?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 ce326ebeb3190faad05c6daa804ab2a33a47970b Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 16:16:59 +0300 Subject: [PATCH 12/24] =?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 ad15093769ba3d9792d5b454356497dcc73fef40 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 16:19:12 +0300 Subject: [PATCH 13/24] =?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 39275f05fd76ef3a2b1e3b5cc67bb103d48ef580 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 17:02:53 +0300 Subject: [PATCH 14/24] =?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 fda3bc867ba636bd93ab633f1acb75c0595a1894 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 17:09:37 +0300 Subject: [PATCH 15/24] =?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 e23ca7c389b044fdfbb9230c9633d31c6be4e181 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/24] 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 d1adcf8a9d0cb601b6e88c6f20c8cbeeace4cc4f 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/24] fixes --- rating_api/routes/comment.py | 24 ++---------------------- tests/test_routes/test_comment.py | 6 +++--- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 3c5b535..0cca734 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -79,7 +79,7 @@ async def create_comment( # Дата, до которой учитываем комментарии для проверки лимита на комментарии конкретному лектору. cutoff_date_lecturer = datetime.datetime( now.year + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) // 12, - (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) % 12, + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) % 12+1, 1, ) lecturer_user_comments_count = ( @@ -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 887df60711f85bcd4ca4a5d0d1efdcb04717ec6a 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/24] 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 0cca734..e5288e9 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 2e8f23003bd61e16c4372a6d2bfc59d51182d2fc Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:28:35 +0300 Subject: [PATCH 19/24] fix --- 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 e5288e9..123745f 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -79,7 +79,7 @@ async def create_comment( # Дата, до которой учитываем комментарии для проверки лимита на комментарии конкретному лектору. cutoff_date_lecturer = datetime.datetime( now.year + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) // 12, - (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) % 12+1, + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) % 12 + 1, 1, ) lecturer_user_comments_count = ( From 6f19b2f7e35fafac36d5eb853ab71675b621e6ed Mon Sep 17 00:00:00 2001 From: petrCher <88943157+petrCher@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:33:18 +0300 Subject: [PATCH 20/24] fix --- 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 123745f..3c5b535 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -79,7 +79,7 @@ async def create_comment( # Дата, до которой учитываем комментарии для проверки лимита на комментарии конкретному лектору. cutoff_date_lecturer = datetime.datetime( now.year + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) // 12, - (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) % 12 + 1, + (now.month - settings.COMMENT_LECTURER_FREQUENCE_IN_MONTH) % 12, 1, ) lecturer_user_comments_count = ( From 226065592ec22bd38639507c6a36f2e3a5aa7aba Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sat, 27 Jun 2026 19:12:38 +0300 Subject: [PATCH 21/24] =?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 42e40dfb47779215638f92be471ae8614b21dbf4 Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 28 Jun 2026 21:39:44 +0300 Subject: [PATCH 22/24] =?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 9eb580603596827bd68b9ca6eff7582de781bd8b Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 28 Jun 2026 21:41:06 +0300 Subject: [PATCH 23/24] =?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 1a635ebce0b069109c5f97902b065912ccb3e25f Mon Sep 17 00:00:00 2001 From: NamazovMaksim Date: Sun, 28 Jun 2026 21:42:20 +0300 Subject: [PATCH 24/24] =?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",