1010import pytest_asyncio
1111from fastapi import FastAPI
1212from httpx import ASGITransport , AsyncClient
13+ from redis .exceptions import ConnectionError as RedisConnectionError
1314
1415from app .database import get_db
1516from app .models .user import User
2122
2223
2324class FakeRedis :
25+ """Тестовая реализация Redis-клиента для API-тестов."""
26+
2427 def __init__ (self ) -> None :
2528 self .store : dict [str , str ] = {}
2629 self .eval_result = 1
2730 self .ttl_result = 60
2831 self .forced_delete_result : int | None = None
2932 self .set_calls : list [tuple [str , str , int ]] = []
3033 self .delete_calls : list [str ] = []
34+ self .raise_on_get = False
35+ self .raise_on_set = False
36+ self .raise_on_delete = False
3137
3238 async def eval (
3339 self , script : str , number_of_keys : int , key : str , window_seconds : int
3440 ) -> int :
41+ """Возвращает заранее заданный результат выполнения Lua-скрипта."""
3542 _ = (script , number_of_keys , key , window_seconds )
3643 return self .eval_result
3744
3845 async def ttl (self , key : str ) -> int :
46+ """Возвращает заранее заданный TTL."""
3947 _ = key
4048 return self .ttl_result
4149
4250 async def set (self , key : str , value : str , ex : int ) -> bool :
51+ """Сохраняет значение по ключу с TTL."""
52+ if self .raise_on_set :
53+ raise RedisConnectionError ("Ошибка записи в тестовый Redis" )
4354 self .store [key ] = value
4455 self .set_calls .append ((key , value , ex ))
4556 return True
4657
47- async def delete (self , key : str ) -> int :
48- self .delete_calls .append (key )
58+ async def get (self , key : str ) -> str | None :
59+ """Возвращает значение по ключу."""
60+ if self .raise_on_get :
61+ raise RedisConnectionError ("Ошибка чтения из тестового Redis" )
62+ return self .store .get (key )
63+
64+ async def scan_iter (self , match : str ):
65+ """Итерирует ключи по паттерну, как Redis SCAN."""
66+ if match .endswith ("*" ):
67+ prefix = match [:- 1 ]
68+ for key in list (self .store .keys ()):
69+ if key .startswith (prefix ):
70+ yield key
71+ return
72+ if match in self .store :
73+ yield match
74+
75+ async def delete (self , * keys : str ) -> int :
76+ """Удаляет один или несколько ключей и возвращает число удалённых."""
77+ if self .raise_on_delete :
78+ raise RedisConnectionError ("Ошибка удаления из тестового Redis" )
79+ self .delete_calls .extend (keys )
4980 if self .forced_delete_result is not None :
5081 if self .forced_delete_result == 1 :
51- self .store .pop (key , None )
82+ for key in keys :
83+ self .store .pop (key , None )
5284 return self .forced_delete_result
5385
54- existed = key in self .store
55- self .store .pop (key , None )
56- return 1 if existed else 0
86+ removed = 0
87+ for key in keys :
88+ existed = key in self .store
89+ self .store .pop (key , None )
90+ removed += 1 if existed else 0
91+ return removed
5792
5893
5994@pytest .fixture
6095def fake_session () -> object :
96+ """Возвращает объект-заглушку сессии для API-тестов."""
6197 return object ()
6298
6399
64100@pytest .fixture
65101def fake_redis () -> FakeRedis :
102+ """Создаёт тестовый Redis-клиент."""
66103 return FakeRedis ()
67104
68105
69106@pytest .fixture
70107def current_user () -> User :
108+ """Возвращает тестового текущего пользователя."""
71109 return cast (
72110 User ,
73111 SimpleNamespace (
@@ -82,6 +120,7 @@ def current_user() -> User:
82120
83121@pytest .fixture
84122def api_app (fake_session : object , fake_redis : FakeRedis , current_user : User ) -> FastAPI :
123+ """Собирает тестовое FastAPI-приложение с override зависимостей."""
85124 app = FastAPI ()
86125 app .include_router (auth_router , prefix = "/api/v1" )
87126 app .include_router (tasks_router , prefix = "/api/v1" )
@@ -104,6 +143,7 @@ async def override_current_user() -> User:
104143
105144@pytest_asyncio .fixture
106145async def client (api_app : FastAPI ) -> AsyncIterator [AsyncClient ]:
146+ """Предоставляет асинхронный HTTP-клиент для API-тестов."""
107147 transport = ASGITransport (app = api_app )
108148 async with AsyncClient (transport = transport , base_url = "http://testserver" ) as ac :
109149 yield ac
0 commit comments