Skip to content
Merged
2 changes: 1 addition & 1 deletion rental_backend/routes/rental_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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.
Expand Down
92 changes: 83 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import importlib
import sys
from functools import lru_cache
Expand All @@ -9,12 +10,14 @@
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.routes.rental_session import RENTAL_SESSION_EXPIRY
from rental_backend.schemas.models import RentStatus
from rental_backend.settings import Settings, get_settings


Expand Down Expand Up @@ -95,6 +98,10 @@ def authlib_user():
"groups": [0],
"id": 0,
"email": "string",
"userdata": [
{"param": "Полное имя", "value": "Тестов Тест"},
{"param": "Номер телефона", "value": "+79991234567"},
],
}


Expand Down Expand Up @@ -229,6 +236,22 @@ def item_fixture(dbsession, item_type_fixture):
return item


@pytest.fixture
def available_item(dbsession, item_types):
"""Создаёт доступный предмет для первого типа."""
item = Item(type_id=item_types[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):
"""Фикстура Item.
Expand All @@ -254,12 +277,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()
Expand Down Expand Up @@ -290,6 +307,40 @@ def items_with_same_type_id(dbsession):
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]:
"""Создает 2 Item с одним itemType в БД и возвращает их."""
Expand All @@ -303,7 +354,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

Expand Down Expand Up @@ -346,7 +397,7 @@ def another_rentses(dbsession, items_with_same_type, another_authlib_user) -> Re
item_id=renting_item.id,
status=RentStatus.RESERVED,
)
Item.update(id=renting_item.id, session=dbsession, is_available=False)
renting_item.is_available = False
dbsession.add(rent)
dbsession.commit()
return rent
Expand All @@ -367,6 +418,29 @@ def active_rentses(dbsession, item_fixture, authlib_user) -> RentalSession:
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]:
"""Возвращает поля модели БД в виде словаря."""
Expand Down
140 changes: 92 additions & 48 deletions tests/test_routes/test_rental_session.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import datetime
from contextlib import contextmanager
from typing import Generator

import pytest
from sqlalchemy import desc
from starlette import status

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
Expand Down Expand Up @@ -42,41 +40,58 @@ def check_object_update(model_instance: BaseDbModel, session, **final_fields):


# Tests for POST /rental-sessions/{item_type_id}
@pytest.mark.usefixtures('expire_mock')
@pytest.mark.parametrize(
'start_item_avail, end_item_avail, itemtype_list_ind, right_status_code, num_of_creations',
"case_name, expected_status, should_create, expected_available",
[
(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),
("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=['avail_item', 'not_avail_item', 'unexisting_itemtype'],
ids=["available_item", "unavailable_item", "no_items", "nonexistent_type"],
)
def test_create_with_diff_item(
def test_create_rental_session(
dbsession,
client,
item_fixture,
base_rentses_url,
start_item_avail,
end_item_avail,
itemtype_list_ind,
right_status_code,
num_of_creations,
available_item,
item_fixture,
item_type_fixture,
nonexistent_type_id,
case_name,
expected_status,
should_create,
expected_available,
):
"""Проверка старта аренды разных 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
with (
check_object_creation(RentalSession, dbsession, num_of_creations=num_of_creations),
check_object_update(item_fixture, session=dbsession, is_available=end_item_avail),
):
if case_name == "available_item":
type_id = available_item.type_id
item = available_item
elif case_name == "unavailable_item":
type_id = item_fixture.type_id
item = item_fixture
elif case_name == "no_items":
type_id = item_type_fixture[1].id
item = None
else:
type_id = 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}')

assert response.status_code == expected_status


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 == right_status_code
assert response.status_code == status.HTTP_409_CONFLICT


@pytest.mark.usefixtures('expire_mock')
Expand All @@ -99,16 +114,20 @@ def test_create_with_invalid_id(dbsession, client, base_rentses_url, invalid_ite


@pytest.mark.usefixtures('expiration_time_mock')
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}')
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 (
RentalSession.get(id=response.json()['id'], session=dbsession).status == RentStatus.EXPIRED
), 'Убедитесь, что по истечение RENTAL_SESSION_EXPIRY, аренда переходит в RentStatus.CANCELED!'
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


# Tests for PATCH /rental-sessions/{session_id}/start
Expand Down Expand Up @@ -192,7 +211,15 @@ 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()
Expand All @@ -201,18 +228,35 @@ 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
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


def test_return_with_set_end_ts(dbsession, client, base_rentses_url, active_rentses):
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


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


Expand Down Expand Up @@ -500,7 +544,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),
('-1?hoho=hihi', status.HTTP_405_METHOD_NOT_ALLOWED),
('-1?hoho=hihi', status.HTTP_404_NOT_FOUND),
],
ids=['text', 'hyphen', 'trailing_slash', 'negative_num', 'empty', 'excess_query'],
)
Expand Down
Loading