From d7b4e36abbe4b9f706667bee7db3a6e9c9ec8adb Mon Sep 17 00:00:00 2001 From: Georgon Date: Fri, 13 Mar 2026 09:46:21 +0300 Subject: [PATCH 01/10] tests for rental session and some new fixtures --- rental_backend/routes/rental_session.py | 26 ++++ tests/conftest.py | 90 ++++++++++++- tests/test_routes/test_rental_session.py | 160 +++++++++++++++++++++-- 3 files changed, 263 insertions(+), 13 deletions(-) diff --git a/rental_backend/routes/rental_session.py b/rental_backend/routes/rental_session.py index e574024..e7e8ccc 100644 --- a/rental_backend/routes/rental_session.py +++ b/rental_backend/routes/rental_session.py @@ -85,7 +85,11 @@ async def check_sessions_overdue(): ) async def create_rental_session( item_type_id: int, +<<<<<<< Updated upstream user=Depends(UnionAuth(scopes=["rental.session.create"], enable_userdata=True)), +======= + user=Depends(UnionAuth(scopes=["rental.session.create"], enable_userdata=True)), +>>>>>>> Stashed changes ): """ Создает новую сессию аренды для указанного типа предмета. @@ -176,7 +180,13 @@ def validate_deadline_ts(deadline_ts: datetime.datetime | None = Query(descripti "/{session_id}/start", response_model=RentalSessionGet, dependencies=[Depends(check_sessions_expiration)] ) async def start_rental_session( +<<<<<<< Updated upstream session_id, deadline_ts=Depends(validate_deadline_ts), user=Depends(UnionAuth(scopes=["rental.session.admin"])) +======= + session_id: int, + deadline_ts=Depends(validate_deadline_ts), + user=Depends(UnionAuth(scopes=["rental.session.admin"])) +>>>>>>> Stashed changes ): """ Starts a rental session, changing its status to ACTIVE. @@ -230,7 +240,11 @@ async def accept_end_rental_session( session_id: int, with_strike: bool = Query(False, description="A flag indicating whether to issue a strike."), strike_reason: str = Query("", description="The reason for the strike."), +<<<<<<< Updated upstream user=Depends(UnionAuth(scopes=["rental.session.admin"])), +======= + user=Depends(UnionAuth(scopes=["rental.session.admin"])) +>>>>>>> Stashed changes ): """ Ends a rental session, changing its status to RETURNED. Issues a strike if specified. @@ -279,7 +293,11 @@ async def accept_end_rental_session( session=db.session, **strike_info.model_dump(), create_ts=datetime.datetime.now(tz=datetime.timezone.utc) ) +<<<<<<< Updated upstream ended_session.strike_id = new_strike.id +======= + ended_session.strike = new_strike +>>>>>>> Stashed changes db.session.commit() ActionLogger.log_event( @@ -398,7 +416,11 @@ async def get_rental_sessions( is_expired: bool = Query(False, description="Флаг, показывать просроченные"), item_type_id: int = Query(0, description="ID типа предмета"), user_id: int = Query(0, description="User_id для получения сессий"), +<<<<<<< Updated upstream user=Depends(UnionAuth(scopes=["rental.session.admin"])), +======= + user=Depends(UnionAuth(scopes=["rental.session.admin"])), +>>>>>>> Stashed changes ): """ Retrieves a list of rental sessions with optional status filtering. @@ -543,7 +565,11 @@ async def cancel_rental_session(session_id: int, user=Depends(UnionAuth())): @rental_session.patch("/{session_id}", response_model=RentalSessionGet) async def update_rental_session( +<<<<<<< Updated upstream session_id: int, update_data: RentalSessionPatch, user=Depends(UnionAuth(scopes=["rental.session.admin"])) +======= + session_id: int, update_data: RentalSessionPatch, user=Depends(UnionAuth(scopes=["rental.session.admin"])) +>>>>>>> Stashed changes ): """ Updates the information of a rental session. diff --git a/tests/conftest.py b/tests/conftest.py index 5314ef6..ca159e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,18 +4,21 @@ from pathlib import Path from typing import Any, Dict, List +import datetime import pytest from _pytest.monkeypatch import MonkeyPatch from alembic import command from alembic.config import Config as AlembicConfig from fastapi.testclient import TestClient -from sqlalchemy import create_engine +from sqlalchemy import create_engine, func from sqlalchemy.orm import sessionmaker from testcontainers.postgres import PostgresContainer from rental_backend.models.db import * from rental_backend.routes import app from rental_backend.settings import Settings, get_settings +from rental_backend.schemas.models import RentStatus +from rental_backend.routes.rental_session import RENTAL_SESSION_EXPIRY class PostgresConfig: @@ -90,11 +93,23 @@ def authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], +<<<<<<< Updated upstream "user_scopes": [{"id": 0, "name": "string"}], +======= + "user_scopes": [{"id": 1, "name": "rental.session.admin"}], # добавлен нужный скоуп "rental.session.admin" (по сути сейчас эта строка ничего не делает, но как в UnionAuth) + "scopes": ["rental.session.admin"], # добавлено для корректной работы прав в тесте test_admin_can_update_any_rental_session +>>>>>>> Stashed changes "indirect_groups": [0], "groups": [0], "id": 0, "email": "string", +<<<<<<< Updated upstream +======= + "userdata": [ + {"param": "Полное имя", "value": "Тестов Тест"}, + {"param": "Номер телефона", "value": "+79991234567"} + ], +>>>>>>> Stashed changes } @@ -107,7 +122,12 @@ def another_authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], +<<<<<<< Updated upstream "user_scopes": [{"id": 0, "name": "string"}], +======= + "user_scopes": [], + "scopes": [], +>>>>>>> Stashed changes "indirect_groups": [0], "groups": [0], "id": 1, @@ -228,6 +248,19 @@ def item_fixture(dbsession, item_type_fixture): dbsession.commit() return item +@pytest.fixture +def available_item(dbsession, item_type_fixture): + """Создаёт доступный предмет для первого типа.""" + item = Item(type_id=item_type_fixture[0].id, is_available=True) + dbsession.add(item) + dbsession.commit() + return item + +@pytest.fixture +def nonexistent_type_id(dbsession): + """Возвращает заведомо несуществующий ID типа предмета. (для тестов при создании сессий)""" + max_id = dbsession.query(func.max(ItemType.id)).scalar() or 0 + return max_id + 1 @pytest.fixture() def items_with_types(dbsession): @@ -289,6 +322,37 @@ def items_with_same_type_id(dbsession): dbsession.delete(i) dbsession.commit() +@pytest.fixture +def two_available_items_same_type(dbsession, item_types): + """ + Создаёт для два доступных предмета к первому типу из item_types и возвращает тип. + """ + item_type = item_types[0] + items = [ + Item(type_id=item_type.id, is_available=True), + Item(type_id=item_type.id, is_available=True), + ] + dbsession.add_all(items) + dbsession.commit() + return item_type + +@pytest.fixture(params=[RentStatus.RESERVED, RentStatus.ACTIVE, RentStatus.OVERDUE]) +def blocking_session(request, dbsession, two_available_items_same_type, authlib_user): + """Создаёт сессию для первого предмета типа с заданным статусом.""" + item_type = two_available_items_same_type + items = item_type.items + now = datetime.datetime.now(datetime.timezone.utc) + session = RentalSession.create( + session=dbsession, + user_id=authlib_user["id"], + item_id=items[0].id, + status=request.param, + reservation_ts=now, + ) + items[0].is_available = False + dbsession.add(session, items[0]) + dbsession.commit() + return item_type @pytest.fixture def items_with_same_type(dbsession, item_types) -> List[Item]: @@ -346,7 +410,11 @@ def another_rentses(dbsession, items_with_same_type, another_authlib_user) -> Re item_id=renting_item.id, status=RentStatus.RESERVED, ) +<<<<<<< Updated upstream Item.update(id=renting_item.id, session=dbsession, is_available=False) +======= + renting_item.is_available = False +>>>>>>> Stashed changes dbsession.add(rent) dbsession.commit() return rent @@ -366,6 +434,26 @@ def active_rentses(dbsession, item_fixture, authlib_user) -> RentalSession: dbsession.commit() return rent +@pytest.fixture +def expired_reserved_session(dbsession, rentses): + """ + Принимает сессию rentses (RESERVED) и сдвигает её reservation_ts в прошлое, чтобы она стала просроченной согласно RENTAL_SESSION_EXPIRY. + Возвращает ID сессии. + """ + now = datetime.datetime.now(datetime.timezone.utc) + past_ts = now - RENTAL_SESSION_EXPIRY - datetime.timedelta(seconds=1) + rentses.reservation_ts = past_ts + dbsession.add(rentses) + dbsession.commit() + return rentses.id + +@pytest.fixture +def active_rentses_with_end_ts(dbsession, active_rentses): + """Возвращает активную сессию с предустановленным end_ts.""" + active_rentses.end_ts = datetime.datetime.now(tz=datetime.timezone.utc) + dbsession.add(active_rentses) + dbsession.commit() + return active_rentses # Utils def model_to_dict(model: BaseDbModel) -> Dict[str, Any]: diff --git a/tests/test_routes/test_rental_session.py b/tests/test_routes/test_rental_session.py index 791155f..d7b2247 100644 --- a/tests/test_routes/test_rental_session.py +++ b/tests/test_routes/test_rental_session.py @@ -42,6 +42,7 @@ def check_object_update(model_instance: BaseDbModel, session, **final_fields): # Tests for POST /rental-sessions/{item_type_id} +<<<<<<< Updated upstream @pytest.mark.usefixtures('expire_mock') @pytest.mark.parametrize( 'start_item_avail, end_item_avail, itemtype_list_ind, right_status_code, num_of_creations', @@ -71,14 +72,54 @@ def test_create_with_diff_item( type_id = ItemType.query(session=dbsession).all()[itemtype_list_ind].id except IndexError: type_id = ItemType.query(session=dbsession).order_by(desc('id'))[0].id + 1 +======= +def test_create_with_available_item(dbsession, client, base_rentses_url, available_item): + """Тест на успешное создание сессии при доступном предмете.""" +>>>>>>> Stashed changes with ( - check_object_creation(RentalSession, dbsession, num_of_creations=num_of_creations), - check_object_update(item_fixture, session=dbsession, is_available=end_item_avail), + check_object_creation(RentalSession, dbsession, num_of_creations=1), + check_object_update(available_item, dbsession, is_available=False) ): + response = client.post(f'{base_rentses_url}/{available_item.type_id}') + assert response.status_code == status.HTTP_200_OK + + +def test_create_with_unavailable_item(dbsession, client, base_rentses_url, item_fixture): + """Попытка создания сессии при недоступном предмете.""" + with ( + check_object_creation(RentalSession, dbsession, num_of_creations=0), + check_object_update(item_fixture, dbsession, is_available=False) + ): + response = client.post(f'{base_rentses_url}/{item_fixture.type_id}') + assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_create_with_type_no_items(dbsession, client, base_rentses_url, item_type_fixture): + """Тест на создание сессии аренды, когда тип существует, но не имеет предметов.""" + type_id = item_type_fixture[1].id + with check_object_creation(RentalSession, dbsession, num_of_creations=0): response = client.post(f'{base_rentses_url}/{type_id}') - assert response.status_code == right_status_code + assert response.status_code == status.HTTP_404_NOT_FOUND + + +<<<<<<< Updated upstream +======= +def test_create_with_nonexistent_type(dbsession, client, base_rentses_url, nonexistent_type_id): + """Тест на создание сессии при несуществующем типе предмета.""" + with check_object_creation(RentalSession, dbsession, num_of_creations=0): + response = client.post(f'{base_rentses_url}/{nonexistent_type_id}') + assert response.status_code == status.HTTP_404_NOT_FOUND +def test_create_with_existing_blocking_session(client, base_rentses_url, blocking_session): + """ + Проверяет, что нельзя создать новую сессию для типа, если у пользователя уже есть + сессия в статусе RESERVED/ACTIVE/OVERDUE для этого типа. + """ + response = client.post(f"{base_rentses_url}/{blocking_session.id}") + assert response.status_code == status.HTTP_409_CONFLICT + +>>>>>>> Stashed changes @pytest.mark.usefixtures('expire_mock') @pytest.mark.parametrize( 'invalid_itemtype_id, right_status_code', @@ -99,6 +140,7 @@ def test_create_with_invalid_id(dbsession, client, base_rentses_url, invalid_ite @pytest.mark.usefixtures('expiration_time_mock') +<<<<<<< Updated upstream def test_create_and_expire(dbsession, client, base_rentses_url, item_fixture): """Проверка правильного срабатывания check_session_expiration.""" item_fixture.is_available = True @@ -109,6 +151,23 @@ def test_create_and_expire(dbsession, client, base_rentses_url, item_fixture): assert ( RentalSession.get(id=response.json()['id'], session=dbsession).status == RentStatus.EXPIRED ), 'Убедитесь, что по истечение RENTAL_SESSION_EXPIRY, аренда переходит в RentStatus.CANCELED!' +======= +def test_create_and_expire(client, base_rentses_url, expired_reserved_session): + """ + Проверяет, что просроченная сессия (RESERVED) переходит в EXPIRED при следующем вызове check_sessions_expiration. + """ + session_id = expired_reserved_session + response = client.get(f"{base_rentses_url}/{session_id}") + assert response.status_code == status.HTTP_200_OK + assert response.json()["status"] == RentStatus.EXPIRED + + +# Тест на начало уже активной сессии +def test_start_already_active_session(dbsession, client, base_rentses_url, active_rentses): + """Проверка, что нельзя начать уже активную сессию.""" + response = client.patch(f'{base_rentses_url}/{active_rentses.id}/start') + assert response.status_code == status.HTTP_403_FORBIDDEN +>>>>>>> Stashed changes # Tests for PATCH /rental-sessions/{session_id}/start @@ -192,7 +251,7 @@ def test_return_inactive(dbsession, client, rentses, base_rentses_url): ], ) def test_return_with_strike( - dbsession, client, base_rentses_url, active_rentses, with_strike, strike_reason, right_status_code, strike_created + dbsession, client, base_rentses_url, active_rentses, authlib_user, with_strike, strike_reason, right_status_code, strike_created ): """Проверяет завершение аренды со страйком.""" query_dict = dict() @@ -201,18 +260,41 @@ def test_return_with_strike( if strike_reason is not None: query_dict['strike_reason'] = strike_reason num_of_creations = 1 if strike_created else 0 +<<<<<<< Updated upstream with check_object_creation(Strike, dbsession, num_of_creations): response = client.patch(f'{base_rentses_url}/{active_rentses.id}/return', params=query_dict) assert response.status_code == right_status_code - - -def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rentses): +======= + session_id = active_rentses.id + admin_id = authlib_user["id"] + with check_object_creation(Strike, dbsession, num_of_creations): + response = client.patch(f'{base_rentses_url}/{active_rentses.id}/return', params=query_dict) + assert response.status_code == right_status_code + dbsession.refresh(active_rentses) + if right_status_code == status.HTTP_200_OK: + assert active_rentses.status == RentStatus.RETURNED + assert active_rentses.item.is_available is True + if strike_created: + strike = active_rentses.strike + assert strike is not None, "Страйк должен быть создан" + assert strike.user_id == active_rentses.user_id + assert strike.admin_id == admin_id + expected_reason = strike_reason if strike_reason is not None else "" + assert strike.reason == expected_reason + assert strike.session_id == session_id + else: + assert active_rentses.strike is None, "Страйк не должен быть создан" + else: + assert active_rentses.status == RentStatus.ACTIVE + assert active_rentses.item.is_available is False + assert active_rentses.strike is None +>>>>>>> Stashed changes + + +def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rentses_with_end_ts): """Проверяет, что при обновлении RentalSession с end_ts не None сохраняется именно существующий, а не создается новый.""" - active_rentses.end_ts = datetime.datetime.now(tz=datetime.timezone.utc) - dbsession.add(active_rentses) - dbsession.commit() - with check_object_update(active_rentses, dbsession, end_ts=active_rentses.end_ts): - response = client.patch(f'{base_rentses_url}/{active_rentses.id}/return') + with check_object_update(active_rentses_with_end_ts, dbsession, end_ts=active_rentses_with_end_ts.end_ts): + response = client.patch(f'{base_rentses_url}/{active_rentses_with_end_ts.id}/return') assert response.status_code == status.HTTP_200_OK @@ -222,6 +304,10 @@ def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rent 'session_id, right_status_code', [ (0, status.HTTP_200_OK), +<<<<<<< Updated upstream +======= + (1, status.HTTP_404_NOT_FOUND), +>>>>>>> Stashed changes ('hihi', status.HTTP_422_UNPROCESSABLE_ENTITY), ('ha-ha', status.HTTP_422_UNPROCESSABLE_ENTITY), ('he-he/hoho', status.HTTP_404_NOT_FOUND), @@ -377,6 +463,52 @@ def test_update_payload(dbsession, rentses, client, base_rentses_url, payload, r assert is_really_updated == update_in_db +<<<<<<< Updated upstream +======= +def test_regular_user_cannot_update_rental_session(dbsession, client, rentses, another_authlib_user): + """ + Проверка, что обычный пользователь (не админ) не может обновить сессию. + Ожидается 403 Forbidden, данные в БД не должны измениться. + """ + + def mock_unionauth_call(self, request): + required_scopes = set(self.scopes or []) + user_scopes = set(another_authlib_user.get('scopes', [])) + if required_scopes and not required_scopes.issubset(user_scopes): + raise HTTPException(status_code=403, detail="Not enough permissions") + return another_authlib_user + + with patch('auth_lib.fastapi.UnionAuth.__call__', new=mock_unionauth_call): + old_end_ts = rentses.end_ts + payload = {"end_ts": "2026-12-31T23:59:59.000Z"} + with check_object_update(rentses, dbsession, end_ts=old_end_ts): + response = client.patch(f"/rental-sessions/{rentses.id}", json=payload) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_admin_can_update_any_rental_session(dbsession, client, another_rentses, authlib_user): + """ + Проверка, что администратор может обновить сессию другого пользователя. + Данные в БД должны измениться. + """ + + def mock_unionauth_call(self, request): + required_scopes = set(self.scopes or []) + user_scopes = set(authlib_user.get('scopes', [])) + if required_scopes and not required_scopes.issubset(user_scopes): + raise HTTPException(status_code=403, detail="Not enough permissions") + return authlib_user + + with patch('auth_lib.fastapi.UnionAuth.__call__', new=mock_unionauth_call): + payload = {"end_ts": "2026-12-31T23:59:59.000Z"} + # Преобразуем строку в naive datetime (убираем временную зону) + expected_end_ts = datetime.datetime.fromisoformat(payload["end_ts"].replace('Z', '+00:00')).replace(tzinfo=None) + with check_object_update(another_rentses, dbsession, end_ts=expected_end_ts): + response = client.patch(f"/rental-sessions/{another_rentses.id}", json=payload) + assert response.status_code == status.HTTP_200_OK + + +>>>>>>> Stashed changes @pytest.mark.usefixtures('dbsession', 'rentses') @pytest.mark.parametrize( 'session_id, right_status_code', @@ -500,7 +632,11 @@ def test_cancel_success(dbsession, client, base_rentses_url, rentses): ('he-he/hoho', status.HTTP_404_NOT_FOUND), (-1, status.HTTP_404_NOT_FOUND), ('', status.HTTP_404_NOT_FOUND), +<<<<<<< Updated upstream ('-1?hoho=hihi', status.HTTP_405_METHOD_NOT_ALLOWED), +======= + ('-1?hoho=hihi', status.HTTP_404_NOT_FOUND), +>>>>>>> Stashed changes ], ids=['text', 'hyphen', 'trailing_slash', 'negative_num', 'empty', 'excess_query'], ) From 1a32c9cf09d919a8f2b6d96e646a1e8bfa8eca5c Mon Sep 17 00:00:00 2001 From: Georgon Date: Fri, 13 Mar 2026 10:52:24 +0300 Subject: [PATCH 02/10] Fix for tests rental_session --- rental_backend/routes/rental_session.py | 24 --------- tests/conftest.py | 17 +----- tests/test_routes/test_rental_session.py | 69 ++---------------------- 3 files changed, 4 insertions(+), 106 deletions(-) diff --git a/rental_backend/routes/rental_session.py b/rental_backend/routes/rental_session.py index e7e8ccc..bc9de1f 100644 --- a/rental_backend/routes/rental_session.py +++ b/rental_backend/routes/rental_session.py @@ -85,11 +85,7 @@ async def check_sessions_overdue(): ) async def create_rental_session( item_type_id: int, -<<<<<<< Updated upstream - user=Depends(UnionAuth(scopes=["rental.session.create"], enable_userdata=True)), -======= user=Depends(UnionAuth(scopes=["rental.session.create"], enable_userdata=True)), ->>>>>>> Stashed changes ): """ Создает новую сессию аренды для указанного типа предмета. @@ -180,13 +176,9 @@ def validate_deadline_ts(deadline_ts: datetime.datetime | None = Query(descripti "/{session_id}/start", response_model=RentalSessionGet, dependencies=[Depends(check_sessions_expiration)] ) async def start_rental_session( -<<<<<<< Updated upstream - session_id, deadline_ts=Depends(validate_deadline_ts), user=Depends(UnionAuth(scopes=["rental.session.admin"])) -======= session_id: int, deadline_ts=Depends(validate_deadline_ts), user=Depends(UnionAuth(scopes=["rental.session.admin"])) ->>>>>>> Stashed changes ): """ Starts a rental session, changing its status to ACTIVE. @@ -240,11 +232,7 @@ async def accept_end_rental_session( session_id: int, with_strike: bool = Query(False, description="A flag indicating whether to issue a strike."), strike_reason: str = Query("", description="The reason for the strike."), -<<<<<<< Updated upstream - user=Depends(UnionAuth(scopes=["rental.session.admin"])), -======= user=Depends(UnionAuth(scopes=["rental.session.admin"])) ->>>>>>> Stashed changes ): """ Ends a rental session, changing its status to RETURNED. Issues a strike if specified. @@ -293,11 +281,7 @@ async def accept_end_rental_session( session=db.session, **strike_info.model_dump(), create_ts=datetime.datetime.now(tz=datetime.timezone.utc) ) -<<<<<<< Updated upstream - ended_session.strike_id = new_strike.id -======= ended_session.strike = new_strike ->>>>>>> Stashed changes db.session.commit() ActionLogger.log_event( @@ -416,11 +400,7 @@ async def get_rental_sessions( is_expired: bool = Query(False, description="Флаг, показывать просроченные"), item_type_id: int = Query(0, description="ID типа предмета"), user_id: int = Query(0, description="User_id для получения сессий"), -<<<<<<< Updated upstream - user=Depends(UnionAuth(scopes=["rental.session.admin"])), -======= user=Depends(UnionAuth(scopes=["rental.session.admin"])), ->>>>>>> Stashed changes ): """ Retrieves a list of rental sessions with optional status filtering. @@ -565,11 +545,7 @@ async def cancel_rental_session(session_id: int, user=Depends(UnionAuth())): @rental_session.patch("/{session_id}", response_model=RentalSessionGet) async def update_rental_session( -<<<<<<< Updated upstream - session_id: int, update_data: RentalSessionPatch, user=Depends(UnionAuth(scopes=["rental.session.admin"])) -======= session_id: int, update_data: RentalSessionPatch, user=Depends(UnionAuth(scopes=["rental.session.admin"])) ->>>>>>> Stashed changes ): """ Updates the information of a rental session. diff --git a/tests/conftest.py b/tests/conftest.py index ca159e2..f6aa5b8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,23 +93,16 @@ def authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], -<<<<<<< Updated upstream - "user_scopes": [{"id": 0, "name": "string"}], -======= "user_scopes": [{"id": 1, "name": "rental.session.admin"}], # добавлен нужный скоуп "rental.session.admin" (по сути сейчас эта строка ничего не делает, но как в UnionAuth) "scopes": ["rental.session.admin"], # добавлено для корректной работы прав в тесте test_admin_can_update_any_rental_session ->>>>>>> Stashed changes "indirect_groups": [0], "groups": [0], "id": 0, "email": "string", -<<<<<<< Updated upstream -======= "userdata": [ {"param": "Полное имя", "value": "Тестов Тест"}, {"param": "Номер телефона", "value": "+79991234567"} ], ->>>>>>> Stashed changes } @@ -122,12 +115,8 @@ def another_authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], -<<<<<<< Updated upstream - "user_scopes": [{"id": 0, "name": "string"}], -======= "user_scopes": [], "scopes": [], ->>>>>>> Stashed changes "indirect_groups": [0], "groups": [0], "id": 1, @@ -367,7 +356,7 @@ def items_with_same_type(dbsession, item_types) -> List[Item]: @pytest.fixture() def expire_mock(mocker): """Mock-объект для функции check_session_expiration.""" - fake_check = mocker.patch('rental_backend.routes.rental_session.check_session_expiration') + fake_check = mocker.patch('rental_backend.routes.rental_session.check_sessions_expiration') fake_check.return_value = True return fake_check @@ -410,11 +399,7 @@ def another_rentses(dbsession, items_with_same_type, another_authlib_user) -> Re item_id=renting_item.id, status=RentStatus.RESERVED, ) -<<<<<<< Updated upstream - Item.update(id=renting_item.id, session=dbsession, is_available=False) -======= renting_item.is_available = False ->>>>>>> Stashed changes dbsession.add(rent) dbsession.commit() return rent diff --git a/tests/test_routes/test_rental_session.py b/tests/test_routes/test_rental_session.py index d7b2247..709a36e 100644 --- a/tests/test_routes/test_rental_session.py +++ b/tests/test_routes/test_rental_session.py @@ -5,7 +5,8 @@ import pytest from sqlalchemy import desc from starlette import status - +from unittest.mock import patch +from fastapi import HTTPException from rental_backend.models.base import BaseDbModel from rental_backend.models.db import Item, ItemType, RentalSession, Strike from rental_backend.routes.rental_session import rental_session @@ -42,40 +43,8 @@ def check_object_update(model_instance: BaseDbModel, session, **final_fields): # Tests for POST /rental-sessions/{item_type_id} -<<<<<<< Updated upstream -@pytest.mark.usefixtures('expire_mock') -@pytest.mark.parametrize( - 'start_item_avail, end_item_avail, itemtype_list_ind, right_status_code, num_of_creations', - [ - (True, False, 0, status.HTTP_200_OK, 1), - (False, False, 0, status.HTTP_404_NOT_FOUND, 0), - (True, True, 1, status.HTTP_404_NOT_FOUND, 0), - ], - ids=['avail_item', 'not_avail_item', 'unexisting_itemtype'], -) -def test_create_with_diff_item( - dbsession, - client, - item_fixture, - base_rentses_url, - start_item_avail, - end_item_avail, - itemtype_list_ind, - right_status_code, - num_of_creations, -): - """Проверка старта аренды разных Item от разных ItemType.""" - item_fixture.is_available = start_item_avail - dbsession.add(item_fixture) - dbsession.commit() - try: - type_id = ItemType.query(session=dbsession).all()[itemtype_list_ind].id - except IndexError: - type_id = ItemType.query(session=dbsession).order_by(desc('id'))[0].id + 1 -======= def test_create_with_available_item(dbsession, client, base_rentses_url, available_item): """Тест на успешное создание сессии при доступном предмете.""" ->>>>>>> Stashed changes with ( check_object_creation(RentalSession, dbsession, num_of_creations=1), check_object_update(available_item, dbsession, is_available=False) @@ -102,8 +71,6 @@ def test_create_with_type_no_items(dbsession, client, base_rentses_url, item_typ assert response.status_code == status.HTTP_404_NOT_FOUND -<<<<<<< Updated upstream -======= def test_create_with_nonexistent_type(dbsession, client, base_rentses_url, nonexistent_type_id): """Тест на создание сессии при несуществующем типе предмета.""" with check_object_creation(RentalSession, dbsession, num_of_creations=0): @@ -119,7 +86,6 @@ def test_create_with_existing_blocking_session(client, base_rentses_url, blockin response = client.post(f"{base_rentses_url}/{blocking_session.id}") assert response.status_code == status.HTTP_409_CONFLICT ->>>>>>> Stashed changes @pytest.mark.usefixtures('expire_mock') @pytest.mark.parametrize( 'invalid_itemtype_id, right_status_code', @@ -140,18 +106,6 @@ def test_create_with_invalid_id(dbsession, client, base_rentses_url, invalid_ite @pytest.mark.usefixtures('expiration_time_mock') -<<<<<<< Updated upstream -def test_create_and_expire(dbsession, client, base_rentses_url, item_fixture): - """Проверка правильного срабатывания check_session_expiration.""" - item_fixture.is_available = True - dbsession.add(item_fixture) - dbsession.commit() - response = client.post(f'{base_rentses_url}/{item_fixture.type_id}') - assert response.status_code == status.HTTP_200_OK - assert ( - RentalSession.get(id=response.json()['id'], session=dbsession).status == RentStatus.EXPIRED - ), 'Убедитесь, что по истечение RENTAL_SESSION_EXPIRY, аренда переходит в RentStatus.CANCELED!' -======= def test_create_and_expire(client, base_rentses_url, expired_reserved_session): """ Проверяет, что просроченная сессия (RESERVED) переходит в EXPIRED при следующем вызове check_sessions_expiration. @@ -167,7 +121,6 @@ def test_start_already_active_session(dbsession, client, base_rentses_url, activ """Проверка, что нельзя начать уже активную сессию.""" response = client.patch(f'{base_rentses_url}/{active_rentses.id}/start') assert response.status_code == status.HTTP_403_FORBIDDEN ->>>>>>> Stashed changes # Tests for PATCH /rental-sessions/{session_id}/start @@ -260,11 +213,6 @@ def test_return_with_strike( if strike_reason is not None: query_dict['strike_reason'] = strike_reason num_of_creations = 1 if strike_created else 0 -<<<<<<< Updated upstream - with check_object_creation(Strike, dbsession, num_of_creations): - response = client.patch(f'{base_rentses_url}/{active_rentses.id}/return', params=query_dict) - assert response.status_code == right_status_code -======= session_id = active_rentses.id admin_id = authlib_user["id"] with check_object_creation(Strike, dbsession, num_of_creations): @@ -288,7 +236,6 @@ def test_return_with_strike( assert active_rentses.status == RentStatus.ACTIVE assert active_rentses.item.is_available is False assert active_rentses.strike is None ->>>>>>> Stashed changes def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rentses_with_end_ts): @@ -304,10 +251,7 @@ def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rent 'session_id, right_status_code', [ (0, status.HTTP_200_OK), -<<<<<<< Updated upstream -======= - (1, status.HTTP_404_NOT_FOUND), ->>>>>>> Stashed changes + #(1, status.HTTP_404_NOT_FOUND), ('hihi', status.HTTP_422_UNPROCESSABLE_ENTITY), ('ha-ha', status.HTTP_422_UNPROCESSABLE_ENTITY), ('he-he/hoho', status.HTTP_404_NOT_FOUND), @@ -463,8 +407,6 @@ def test_update_payload(dbsession, rentses, client, base_rentses_url, payload, r assert is_really_updated == update_in_db -<<<<<<< Updated upstream -======= def test_regular_user_cannot_update_rental_session(dbsession, client, rentses, another_authlib_user): """ Проверка, что обычный пользователь (не админ) не может обновить сессию. @@ -508,7 +450,6 @@ def mock_unionauth_call(self, request): assert response.status_code == status.HTTP_200_OK ->>>>>>> Stashed changes @pytest.mark.usefixtures('dbsession', 'rentses') @pytest.mark.parametrize( 'session_id, right_status_code', @@ -632,11 +573,7 @@ def test_cancel_success(dbsession, client, base_rentses_url, rentses): ('he-he/hoho', status.HTTP_404_NOT_FOUND), (-1, status.HTTP_404_NOT_FOUND), ('', status.HTTP_404_NOT_FOUND), -<<<<<<< Updated upstream - ('-1?hoho=hihi', status.HTTP_405_METHOD_NOT_ALLOWED), -======= ('-1?hoho=hihi', status.HTTP_404_NOT_FOUND), ->>>>>>> Stashed changes ], ids=['text', 'hyphen', 'trailing_slash', 'negative_num', 'empty', 'excess_query'], ) From 1c4a2ddea48060cc3126294c357d27ae46ce4c1f Mon Sep 17 00:00:00 2001 From: Georgon Date: Sat, 14 Mar 2026 10:22:55 +0300 Subject: [PATCH 03/10] Fix test_item --- tests/conftest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f6aa5b8..cbc9a08 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -276,12 +276,12 @@ def items_with_types(dbsession): dbsession.add(i) dbsession.commit() yield items - for i in item_types: - for item in i.items: - dbsession.delete(item) - dbsession.flush() - dbsession.delete(i) - dbsession.commit() + # for i in item_types: + # for item in i.items: + # dbsession.delete(item) + # dbsession.flush() + # dbsession.delete(i) + # dbsession.commit() @pytest.fixture() From e2dbf86f251929a728bf602b713ba844cfc06177 Mon Sep 17 00:00:00 2001 From: Georgon Date: Sat, 14 Mar 2026 10:25:48 +0300 Subject: [PATCH 04/10] fix test_item --- tests/conftest.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index cbc9a08..7797b40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -276,12 +276,6 @@ def items_with_types(dbsession): dbsession.add(i) dbsession.commit() yield items - # for i in item_types: - # for item in i.items: - # dbsession.delete(item) - # dbsession.flush() - # dbsession.delete(i) - # dbsession.commit() @pytest.fixture() From dcc90d293eb163f781936bf2d7d2ac17366d67e1 Mon Sep 17 00:00:00 2001 From: Georgon Date: Thu, 2 Apr 2026 18:42:16 +0300 Subject: [PATCH 05/10] Combine several tests and remove test for auth --- tests/test_routes/test_rental_session.py | 121 ++++++++--------------- 1 file changed, 41 insertions(+), 80 deletions(-) diff --git a/tests/test_routes/test_rental_session.py b/tests/test_routes/test_rental_session.py index 709a36e..3748a17 100644 --- a/tests/test_routes/test_rental_session.py +++ b/tests/test_routes/test_rental_session.py @@ -43,47 +43,52 @@ def check_object_update(model_instance: BaseDbModel, session, **final_fields): # Tests for POST /rental-sessions/{item_type_id} -def test_create_with_available_item(dbsession, client, base_rentses_url, available_item): - """Тест на успешное создание сессии при доступном предмете.""" - with ( - check_object_creation(RentalSession, dbsession, num_of_creations=1), - check_object_update(available_item, dbsession, is_available=False) - ): - response = client.post(f'{base_rentses_url}/{available_item.type_id}') - assert response.status_code == status.HTTP_200_OK - +@pytest.mark.parametrize( + "case_name, expected_status, should_create, expected_available", + [ + ("available_item", status.HTTP_200_OK, True, False), + ("unavailable_item", status.HTTP_404_NOT_FOUND, False, False), + ("no_items", status.HTTP_404_NOT_FOUND, False, None), + ("nonexistent_type", status.HTTP_404_NOT_FOUND, False, None), + ], + ids=["available_item", "unavailable_item", "no_items", "nonexistent_type"] +) +def test_create_rental_session( + request, dbsession, client, base_rentses_url, + case_name, expected_status, should_create, expected_available +): + if case_name == "available_item": + item = request.getfixturevalue("available_item") + type_id = item.type_id + elif case_name == "unavailable_item": + item = request.getfixturevalue("item_fixture") + type_id = item.type_id + elif case_name == "no_items": + item_type = request.getfixturevalue("item_type_fixture") + type_id = item_type[1].id + item = None + else: + type_id = request.getfixturevalue("nonexistent_type_id") + item = None + + with check_object_creation(RentalSession, dbsession, num_of_creations=1 if should_create else 0): + if item is not None and expected_available is not None: + with check_object_update(item, dbsession, is_available=expected_available): + response = client.post(f'{base_rentses_url}/{type_id}') + else: + response = client.post(f'{base_rentses_url}/{type_id}') -def test_create_with_unavailable_item(dbsession, client, base_rentses_url, item_fixture): - """Попытка создания сессии при недоступном предмете.""" - with ( - check_object_creation(RentalSession, dbsession, num_of_creations=0), - check_object_update(item_fixture, dbsession, is_available=False) - ): - response = client.post(f'{base_rentses_url}/{item_fixture.type_id}') - assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.status_code == expected_status -def test_create_with_type_no_items(dbsession, client, base_rentses_url, item_type_fixture): - """Тест на создание сессии аренды, когда тип существует, но не имеет предметов.""" - type_id = item_type_fixture[1].id +# Тест для блокирующего кейса (параметризуется фикстурой blocking_session) +def test_create_rental_session_blocking( + dbsession, client, base_rentses_url, blocking_session +): + """Попытка создания сессии для предмета с уже созданной сессией с разными статусами.""" + type_id = blocking_session.id with check_object_creation(RentalSession, dbsession, num_of_creations=0): response = client.post(f'{base_rentses_url}/{type_id}') - assert response.status_code == status.HTTP_404_NOT_FOUND - - -def test_create_with_nonexistent_type(dbsession, client, base_rentses_url, nonexistent_type_id): - """Тест на создание сессии при несуществующем типе предмета.""" - with check_object_creation(RentalSession, dbsession, num_of_creations=0): - response = client.post(f'{base_rentses_url}/{nonexistent_type_id}') - assert response.status_code == status.HTTP_404_NOT_FOUND - - -def test_create_with_existing_blocking_session(client, base_rentses_url, blocking_session): - """ - Проверяет, что нельзя создать новую сессию для типа, если у пользователя уже есть - сессия в статусе RESERVED/ACTIVE/OVERDUE для этого типа. - """ - response = client.post(f"{base_rentses_url}/{blocking_session.id}") assert response.status_code == status.HTTP_409_CONFLICT @pytest.mark.usefixtures('expire_mock') @@ -406,50 +411,6 @@ def test_update_payload(dbsession, rentses, client, base_rentses_url, payload, r is_really_updated = old_model_fields != new_model_fields assert is_really_updated == update_in_db - -def test_regular_user_cannot_update_rental_session(dbsession, client, rentses, another_authlib_user): - """ - Проверка, что обычный пользователь (не админ) не может обновить сессию. - Ожидается 403 Forbidden, данные в БД не должны измениться. - """ - - def mock_unionauth_call(self, request): - required_scopes = set(self.scopes or []) - user_scopes = set(another_authlib_user.get('scopes', [])) - if required_scopes and not required_scopes.issubset(user_scopes): - raise HTTPException(status_code=403, detail="Not enough permissions") - return another_authlib_user - - with patch('auth_lib.fastapi.UnionAuth.__call__', new=mock_unionauth_call): - old_end_ts = rentses.end_ts - payload = {"end_ts": "2026-12-31T23:59:59.000Z"} - with check_object_update(rentses, dbsession, end_ts=old_end_ts): - response = client.patch(f"/rental-sessions/{rentses.id}", json=payload) - assert response.status_code == status.HTTP_403_FORBIDDEN - - -def test_admin_can_update_any_rental_session(dbsession, client, another_rentses, authlib_user): - """ - Проверка, что администратор может обновить сессию другого пользователя. - Данные в БД должны измениться. - """ - - def mock_unionauth_call(self, request): - required_scopes = set(self.scopes or []) - user_scopes = set(authlib_user.get('scopes', [])) - if required_scopes and not required_scopes.issubset(user_scopes): - raise HTTPException(status_code=403, detail="Not enough permissions") - return authlib_user - - with patch('auth_lib.fastapi.UnionAuth.__call__', new=mock_unionauth_call): - payload = {"end_ts": "2026-12-31T23:59:59.000Z"} - # Преобразуем строку в naive datetime (убираем временную зону) - expected_end_ts = datetime.datetime.fromisoformat(payload["end_ts"].replace('Z', '+00:00')).replace(tzinfo=None) - with check_object_update(another_rentses, dbsession, end_ts=expected_end_ts): - response = client.patch(f"/rental-sessions/{another_rentses.id}", json=payload) - assert response.status_code == status.HTTP_200_OK - - @pytest.mark.usefixtures('dbsession', 'rentses') @pytest.mark.parametrize( 'session_id, right_status_code', From 50d8c47aa38bc577ab5fc6a3056633bb183cf4e5 Mon Sep 17 00:00:00 2001 From: Georgon Date: Thu, 2 Apr 2026 18:45:21 +0300 Subject: [PATCH 06/10] modification --- rental_backend/routes/rental_session.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rental_backend/routes/rental_session.py b/rental_backend/routes/rental_session.py index bc9de1f..2f63cf2 100644 --- a/rental_backend/routes/rental_session.py +++ b/rental_backend/routes/rental_session.py @@ -85,7 +85,7 @@ async def check_sessions_overdue(): ) async def create_rental_session( item_type_id: int, - user=Depends(UnionAuth(scopes=["rental.session.create"], enable_userdata=True)), + user=Depends(UnionAuth(scopes=["rental.session.create"], enable_userdata=True)), ): """ Создает новую сессию аренды для указанного типа предмета. @@ -176,9 +176,10 @@ def validate_deadline_ts(deadline_ts: datetime.datetime | None = Query(descripti "/{session_id}/start", response_model=RentalSessionGet, dependencies=[Depends(check_sessions_expiration)] ) async def start_rental_session( - session_id: int, - deadline_ts=Depends(validate_deadline_ts), - user=Depends(UnionAuth(scopes=["rental.session.admin"])) + session_id:int, deadline_ts=Depends(validate_deadline_ts), user=Depends(UnionAuth(scopes=["rental.session.admin"])) + # session_id: int, + # deadline_ts=Depends(validate_deadline_ts), + # user=Depends(UnionAuth(scopes=["rental.session.admin"])) ): """ Starts a rental session, changing its status to ACTIVE. @@ -232,7 +233,7 @@ async def accept_end_rental_session( session_id: int, with_strike: bool = Query(False, description="A flag indicating whether to issue a strike."), strike_reason: str = Query("", description="The reason for the strike."), - user=Depends(UnionAuth(scopes=["rental.session.admin"])) + user=Depends(UnionAuth(scopes=["rental.session.admin"])), ): """ Ends a rental session, changing its status to RETURNED. Issues a strike if specified. @@ -281,7 +282,8 @@ async def accept_end_rental_session( session=db.session, **strike_info.model_dump(), create_ts=datetime.datetime.now(tz=datetime.timezone.utc) ) - ended_session.strike = new_strike + # ended_session.strike = new_strike + ended_session.strike_id = new_strike.id db.session.commit() ActionLogger.log_event( @@ -400,7 +402,7 @@ async def get_rental_sessions( is_expired: bool = Query(False, description="Флаг, показывать просроченные"), item_type_id: int = Query(0, description="ID типа предмета"), user_id: int = Query(0, description="User_id для получения сессий"), - user=Depends(UnionAuth(scopes=["rental.session.admin"])), + user=Depends(UnionAuth(scopes=["rental.session.admin"])), ): """ Retrieves a list of rental sessions with optional status filtering. @@ -545,7 +547,7 @@ async def cancel_rental_session(session_id: int, user=Depends(UnionAuth())): @rental_session.patch("/{session_id}", response_model=RentalSessionGet) async def update_rental_session( - session_id: int, update_data: RentalSessionPatch, user=Depends(UnionAuth(scopes=["rental.session.admin"])) + session_id: int, update_data: RentalSessionPatch, user=Depends(UnionAuth(scopes=["rental.session.admin"])) ): """ Updates the information of a rental session. From 3f22f6d7af797bd58c8987231592d6a984daeae2 Mon Sep 17 00:00:00 2001 From: Georgon Date: Thu, 2 Apr 2026 21:15:41 +0300 Subject: [PATCH 07/10] Without comments --- rental_backend/routes/rental_session.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rental_backend/routes/rental_session.py b/rental_backend/routes/rental_session.py index 2f63cf2..cc7951a 100644 --- a/rental_backend/routes/rental_session.py +++ b/rental_backend/routes/rental_session.py @@ -177,9 +177,6 @@ def validate_deadline_ts(deadline_ts: datetime.datetime | None = Query(descripti ) async def start_rental_session( session_id:int, deadline_ts=Depends(validate_deadline_ts), user=Depends(UnionAuth(scopes=["rental.session.admin"])) - # session_id: int, - # deadline_ts=Depends(validate_deadline_ts), - # user=Depends(UnionAuth(scopes=["rental.session.admin"])) ): """ Starts a rental session, changing its status to ACTIVE. @@ -282,7 +279,6 @@ async def accept_end_rental_session( session=db.session, **strike_info.model_dump(), create_ts=datetime.datetime.now(tz=datetime.timezone.utc) ) - # ended_session.strike = new_strike ended_session.strike_id = new_strike.id db.session.commit() From 1bc1644067075ec6bdf7bfe8d1f8f132b70c6435 Mon Sep 17 00:00:00 2001 From: Georgon Date: Fri, 3 Apr 2026 14:07:07 +0300 Subject: [PATCH 08/10] Test_rental_session + black/isort --- rental_backend/routes/rental_session.py | 2 +- tests/conftest.py | 29 +++++++++++++++------ tests/test_routes/test_rental_session.py | 32 +++++++++++++----------- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/rental_backend/routes/rental_session.py b/rental_backend/routes/rental_session.py index cc7951a..ab94ca9 100644 --- a/rental_backend/routes/rental_session.py +++ b/rental_backend/routes/rental_session.py @@ -176,7 +176,7 @@ def validate_deadline_ts(deadline_ts: datetime.datetime | None = Query(descripti "/{session_id}/start", response_model=RentalSessionGet, dependencies=[Depends(check_sessions_expiration)] ) async def start_rental_session( - session_id:int, deadline_ts=Depends(validate_deadline_ts), user=Depends(UnionAuth(scopes=["rental.session.admin"])) + session_id: int, deadline_ts=Depends(validate_deadline_ts), user=Depends(UnionAuth(scopes=["rental.session.admin"])) ): """ Starts a rental session, changing its status to ACTIVE. diff --git a/tests/conftest.py b/tests/conftest.py index 7797b40..b75ac11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,10 @@ +import datetime import importlib import sys from functools import lru_cache from pathlib import Path from typing import Any, Dict, List -import datetime import pytest from _pytest.monkeypatch import MonkeyPatch from alembic import command @@ -16,9 +16,9 @@ from rental_backend.models.db import * from rental_backend.routes import app -from rental_backend.settings import Settings, get_settings -from rental_backend.schemas.models import RentStatus from rental_backend.routes.rental_session import RENTAL_SESSION_EXPIRY +from rental_backend.schemas.models import RentStatus +from rental_backend.settings import Settings, get_settings class PostgresConfig: @@ -93,15 +93,19 @@ def authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], - "user_scopes": [{"id": 1, "name": "rental.session.admin"}], # добавлен нужный скоуп "rental.session.admin" (по сути сейчас эта строка ничего не делает, но как в UnionAuth) - "scopes": ["rental.session.admin"], # добавлено для корректной работы прав в тесте test_admin_can_update_any_rental_session + "user_scopes": [ + {"id": 1, "name": "rental.session.admin"} + ], # добавлен нужный скоуп "rental.session.admin" (по сути сейчас эта строка ничего не делает, но как в UnionAuth) + "scopes": [ + "rental.session.admin" + ], # добавлено для корректной работы прав в тесте test_admin_can_update_any_rental_session "indirect_groups": [0], "groups": [0], "id": 0, "email": "string", "userdata": [ {"param": "Полное имя", "value": "Тестов Тест"}, - {"param": "Номер телефона", "value": "+79991234567"} + {"param": "Номер телефона", "value": "+79991234567"}, ], } @@ -116,7 +120,7 @@ def another_authlib_user(): "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], "user_scopes": [], - "scopes": [], + "scopes": [], "indirect_groups": [0], "groups": [0], "id": 1, @@ -237,6 +241,7 @@ def item_fixture(dbsession, item_type_fixture): dbsession.commit() return item + @pytest.fixture def available_item(dbsession, item_type_fixture): """Создаёт доступный предмет для первого типа.""" @@ -245,12 +250,14 @@ def available_item(dbsession, item_type_fixture): dbsession.commit() return item + @pytest.fixture def nonexistent_type_id(dbsession): """Возвращает заведомо несуществующий ID типа предмета. (для тестов при создании сессий)""" max_id = dbsession.query(func.max(ItemType.id)).scalar() or 0 return max_id + 1 + @pytest.fixture() def items_with_types(dbsession): """Фикстура Item. @@ -305,6 +312,7 @@ def items_with_same_type_id(dbsession): dbsession.delete(i) dbsession.commit() + @pytest.fixture def two_available_items_same_type(dbsession, item_types): """ @@ -319,6 +327,7 @@ def two_available_items_same_type(dbsession, item_types): dbsession.commit() return item_type + @pytest.fixture(params=[RentStatus.RESERVED, RentStatus.ACTIVE, RentStatus.OVERDUE]) def blocking_session(request, dbsession, two_available_items_same_type, authlib_user): """Создаёт сессию для первого предмета типа с заданным статусом.""" @@ -335,7 +344,8 @@ def blocking_session(request, dbsession, two_available_items_same_type, authlib_ items[0].is_available = False dbsession.add(session, items[0]) dbsession.commit() - return item_type + return item_type + @pytest.fixture def items_with_same_type(dbsession, item_types) -> List[Item]: @@ -413,6 +423,7 @@ def active_rentses(dbsession, item_fixture, authlib_user) -> RentalSession: dbsession.commit() return rent + @pytest.fixture def expired_reserved_session(dbsession, rentses): """ @@ -426,6 +437,7 @@ def expired_reserved_session(dbsession, rentses): dbsession.commit() return rentses.id + @pytest.fixture def active_rentses_with_end_ts(dbsession, active_rentses): """Возвращает активную сессию с предустановленным end_ts.""" @@ -434,6 +446,7 @@ def active_rentses_with_end_ts(dbsession, active_rentses): dbsession.commit() return active_rentses + # Utils def model_to_dict(model: BaseDbModel) -> Dict[str, Any]: """Возвращает поля модели БД в виде словаря.""" diff --git a/tests/test_routes/test_rental_session.py b/tests/test_routes/test_rental_session.py index 3748a17..0ec18d7 100644 --- a/tests/test_routes/test_rental_session.py +++ b/tests/test_routes/test_rental_session.py @@ -1,14 +1,11 @@ -import datetime from contextlib import contextmanager from typing import Generator import pytest -from sqlalchemy import desc from starlette import status -from unittest.mock import patch -from fastapi import HTTPException + from rental_backend.models.base import BaseDbModel -from rental_backend.models.db import Item, ItemType, RentalSession, Strike +from rental_backend.models.db import Item, RentalSession, Strike from rental_backend.routes.rental_session import rental_session from rental_backend.schemas.models import RentStatus from tests.conftest import model_to_dict @@ -51,11 +48,10 @@ def check_object_update(model_instance: BaseDbModel, session, **final_fields): ("no_items", status.HTTP_404_NOT_FOUND, False, None), ("nonexistent_type", status.HTTP_404_NOT_FOUND, False, None), ], - ids=["available_item", "unavailable_item", "no_items", "nonexistent_type"] + ids=["available_item", "unavailable_item", "no_items", "nonexistent_type"], ) def test_create_rental_session( - request, dbsession, client, base_rentses_url, - case_name, expected_status, should_create, expected_available + request, dbsession, client, base_rentses_url, case_name, expected_status, should_create, expected_available ): if case_name == "available_item": item = request.getfixturevalue("available_item") @@ -67,7 +63,7 @@ def test_create_rental_session( item_type = request.getfixturevalue("item_type_fixture") type_id = item_type[1].id item = None - else: + else: type_id = request.getfixturevalue("nonexistent_type_id") item = None @@ -82,15 +78,14 @@ def test_create_rental_session( # Тест для блокирующего кейса (параметризуется фикстурой blocking_session) -def test_create_rental_session_blocking( - dbsession, client, base_rentses_url, blocking_session -): +def test_create_rental_session_blocking(dbsession, client, base_rentses_url, blocking_session): """Попытка создания сессии для предмета с уже созданной сессией с разными статусами.""" type_id = blocking_session.id with check_object_creation(RentalSession, dbsession, num_of_creations=0): response = client.post(f'{base_rentses_url}/{type_id}') assert response.status_code == status.HTTP_409_CONFLICT + @pytest.mark.usefixtures('expire_mock') @pytest.mark.parametrize( 'invalid_itemtype_id, right_status_code', @@ -209,7 +204,15 @@ def test_return_inactive(dbsession, client, rentses, base_rentses_url): ], ) def test_return_with_strike( - dbsession, client, base_rentses_url, active_rentses, authlib_user, with_strike, strike_reason, right_status_code, strike_created + dbsession, + client, + base_rentses_url, + active_rentses, + authlib_user, + with_strike, + strike_reason, + right_status_code, + strike_created, ): """Проверяет завершение аренды со страйком.""" query_dict = dict() @@ -256,7 +259,7 @@ def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rent 'session_id, right_status_code', [ (0, status.HTTP_200_OK), - #(1, status.HTTP_404_NOT_FOUND), + # (1, status.HTTP_404_NOT_FOUND), ('hihi', status.HTTP_422_UNPROCESSABLE_ENTITY), ('ha-ha', status.HTTP_422_UNPROCESSABLE_ENTITY), ('he-he/hoho', status.HTTP_404_NOT_FOUND), @@ -411,6 +414,7 @@ def test_update_payload(dbsession, rentses, client, base_rentses_url, payload, r is_really_updated = old_model_fields != new_model_fields assert is_really_updated == update_in_db + @pytest.mark.usefixtures('dbsession', 'rentses') @pytest.mark.parametrize( 'session_id, right_status_code', From 42ca6e7522a4f181d291fac2c6c321eea5363199 Mon Sep 17 00:00:00 2001 From: Georgon <43513853+Georgon@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:54:04 +0300 Subject: [PATCH 09/10] Update conftest.py --- tests/conftest.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b75ac11..8a5974e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,12 +93,7 @@ def authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], - "user_scopes": [ - {"id": 1, "name": "rental.session.admin"} - ], # добавлен нужный скоуп "rental.session.admin" (по сути сейчас эта строка ничего не делает, но как в UnionAuth) - "scopes": [ - "rental.session.admin" - ], # добавлено для корректной работы прав в тесте test_admin_can_update_any_rental_session + "user_scopes": [{"id": 0, "name": "string"}], "indirect_groups": [0], "groups": [0], "id": 0, @@ -119,8 +114,7 @@ def another_authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], - "user_scopes": [], - "scopes": [], + "user_scopes": [{"id": 0, "name": "string"}], "indirect_groups": [0], "groups": [0], "id": 1, From fc1464b73bc2541b8611f513b86ee7c4d2f94d5d Mon Sep 17 00:00:00 2001 From: Georgon Date: Sat, 4 Apr 2026 21:06:00 +0300 Subject: [PATCH 10/10] Test_rental_session with no comments and no getfixturevalue --- tests/conftest.py | 14 ++++-------- tests/test_routes/test_rental_session.py | 28 ++++++++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b75ac11..dba7c9e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,12 +93,7 @@ def authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], - "user_scopes": [ - {"id": 1, "name": "rental.session.admin"} - ], # добавлен нужный скоуп "rental.session.admin" (по сути сейчас эта строка ничего не делает, но как в UnionAuth) - "scopes": [ - "rental.session.admin" - ], # добавлено для корректной работы прав в тесте test_admin_can_update_any_rental_session + "user_scopes": [{"id": 0, "name": "string"}], "indirect_groups": [0], "groups": [0], "id": 0, @@ -119,8 +114,7 @@ def another_authlib_user(): return { "auth_methods": ["string"], "session_scopes": [{"id": 0, "name": "string"}], - "user_scopes": [], - "scopes": [], + "user_scopes": [{"id": 0, "name": "string"}], "indirect_groups": [0], "groups": [0], "id": 1, @@ -243,9 +237,9 @@ def item_fixture(dbsession, item_type_fixture): @pytest.fixture -def available_item(dbsession, item_type_fixture): +def available_item(dbsession, item_types): """Создаёт доступный предмет для первого типа.""" - item = Item(type_id=item_type_fixture[0].id, is_available=True) + item = Item(type_id=item_types[0].id, is_available=True) dbsession.add(item) dbsession.commit() return item diff --git a/tests/test_routes/test_rental_session.py b/tests/test_routes/test_rental_session.py index 0ec18d7..9aa6aa9 100644 --- a/tests/test_routes/test_rental_session.py +++ b/tests/test_routes/test_rental_session.py @@ -51,20 +51,29 @@ def check_object_update(model_instance: BaseDbModel, session, **final_fields): ids=["available_item", "unavailable_item", "no_items", "nonexistent_type"], ) def test_create_rental_session( - request, dbsession, client, base_rentses_url, case_name, expected_status, should_create, expected_available + dbsession, + client, + base_rentses_url, + available_item, + item_fixture, + item_type_fixture, + nonexistent_type_id, + case_name, + expected_status, + should_create, + expected_available, ): if case_name == "available_item": - item = request.getfixturevalue("available_item") - type_id = item.type_id + type_id = available_item.type_id + item = available_item elif case_name == "unavailable_item": - item = request.getfixturevalue("item_fixture") - type_id = item.type_id + type_id = item_fixture.type_id + item = item_fixture elif case_name == "no_items": - item_type = request.getfixturevalue("item_type_fixture") - type_id = item_type[1].id + type_id = item_type_fixture[1].id item = None else: - type_id = request.getfixturevalue("nonexistent_type_id") + type_id = nonexistent_type_id item = None with check_object_creation(RentalSession, dbsession, num_of_creations=1 if should_create else 0): @@ -77,7 +86,6 @@ def test_create_rental_session( assert response.status_code == expected_status -# Тест для блокирующего кейса (параметризуется фикстурой blocking_session) def test_create_rental_session_blocking(dbsession, client, base_rentses_url, blocking_session): """Попытка создания сессии для предмета с уже созданной сессией с разными статусами.""" type_id = blocking_session.id @@ -116,7 +124,6 @@ def test_create_and_expire(client, base_rentses_url, expired_reserved_session): assert response.json()["status"] == RentStatus.EXPIRED -# Тест на начало уже активной сессии def test_start_already_active_session(dbsession, client, base_rentses_url, active_rentses): """Проверка, что нельзя начать уже активную сессию.""" response = client.patch(f'{base_rentses_url}/{active_rentses.id}/start') @@ -259,7 +266,6 @@ def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rent 'session_id, right_status_code', [ (0, status.HTTP_200_OK), - # (1, status.HTTP_404_NOT_FOUND), ('hihi', status.HTTP_422_UNPROCESSABLE_ENTITY), ('ha-ha', status.HTTP_422_UNPROCESSABLE_ENTITY), ('he-he/hoho', status.HTTP_404_NOT_FOUND),