diff --git a/src/cache/rate_limit.py b/src/cache/rate_limit.py index 4e59d5b..6ed4fa2 100644 --- a/src/cache/rate_limit.py +++ b/src/cache/rate_limit.py @@ -19,8 +19,7 @@ class RateLimit: if attempts>5: raise self.errors.rate_limit_error(detail="too many attempts", retry_after=60) - async def check_rate_limit(self, request: Request) -> None: - client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown') + async def check_rate_limit(self, client_ip:str) -> None: await self.rate_limit(client_ip) diff --git a/src/models/database_models/model.py b/src/models/database_models/model.py index ad27682..fe7523f 100644 --- a/src/models/database_models/model.py +++ b/src/models/database_models/model.py @@ -17,7 +17,12 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship from src.models.configs_read.env import env_settings -engine = create_async_engine(f"postgresql+asyncpg://{env_settings.DB_USER}:{env_settings.DB_PASSWORD}@{env_settings.DB_HOST}:{env_settings.DB_PORT}/{env_settings.DB_POSTGRESS}") +engine = create_async_engine(f"postgresql+asyncpg://{env_settings.DB_USER}:{env_settings.DB_PASSWORD}@{env_settings.DB_HOST}:{env_settings.DB_PORT}/{env_settings.DB_POSTGRESS}", + pool_size=20, # сколько соединений держать открытыми постоянно + max_overflow=10, # сколько доп. соединений можно создать при пиковой нагрузке + pool_timeout=30, # сколько ждать свободное соединение, прежде чем упасть с ошибкой + pool_pre_ping=True, # проверять соединение перед использованием (ловит "протухшие" соединения) + ) '''remember as a boilerplate, or just cp/pst''' class Model(DeclarativeBase): diff --git a/src/web/protected_routes/auth_routes.py b/src/web/protected_routes/auth_routes.py index 44d74c4..f35d12b 100644 --- a/src/web/protected_routes/auth_routes.py +++ b/src/web/protected_routes/auth_routes.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Cookie, Depends, Request, Response +from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from src.cache.rate_limit import rate_limiter @@ -24,10 +24,15 @@ async def get_access_token(request: Request, response:Response, auth:CurrentUserService=Depends(auth_service), form_data:OAuth2PasswordRequestForm=Depends(), - _:None = Depends(rate_limiter.check_rate_limit) )->dict: - access_token, refresh_token=await auth.login(form_data_email=form_data.username, form_data_password=form_data.password, request=request) + client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown') + + try: + access_token, refresh_token=await auth.login(form_data_email=form_data.username, form_data_password=form_data.password, request=request) + except HTTPException: + await rate_limiter.rate_limit(client_ip) + raise response.set_cookie( key="refresh_token", diff --git a/tests/conftest.py b/tests/conftest.py index 663e14f..95c1382 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,9 @@ import pytest_asyncio +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine +from src.models.configs_read.env import env_settings from src.service.auth.jwt import HashService, JwtService +from src.service.users_crud.users_crud import CrudService @pytest_asyncio.fixture @@ -11,4 +14,17 @@ async def jwt_service()->JwtService: @pytest_asyncio.fixture async def hash_service()->HashService: hash_service=HashService() - return hash_service \ No newline at end of file + return hash_service + +@pytest_asyncio.fixture +async def crud_service(): + test_engine = create_async_engine(f"postgresql+asyncpg://{env_settings.DB_USER}:{env_settings.DB_PASSWORD}@{env_settings.DB_HOST}:{env_settings.DB_PORT}/{env_settings.DB_POSTGRESS}", + pool_size=20, + max_overflow=10, + pool_timeout=30, + pool_pre_ping=True + ) + crud_service = CrudService() + crud_service.crud_db_actions.Session = async_sessionmaker(bind=test_engine) + yield crud_service + await test_engine.dispose() \ No newline at end of file diff --git a/tests/e2e/test_auth.py b/tests/e2e/test_auth.py index 597680d..45618f9 100644 --- a/tests/e2e/test_auth.py +++ b/tests/e2e/test_auth.py @@ -12,8 +12,10 @@ class TestAuth: response = await requests_async.post(f"{target_url}/protected/token") response.raise_for_status() + assert exc_info.value.response.status_code != 403 assert exc_info.value.response.status_code != 401 + assert exc_info.value.response.status_code != 429 async def test_get_refresh_token_positive(self, target_url:str)->None: diff --git a/tests/integrated/conftest.py b/tests/integrated/conftest.py index 635927b..82172f4 100644 --- a/tests/integrated/conftest.py +++ b/tests/integrated/conftest.py @@ -1,22 +1,23 @@ import pytest_asyncio from fastapi import Request +from src.cache.redis_client import RedisClient from src.service.auth.auth import CurrentUserService -from src.service.users_crud.users_crud import CrudService @pytest_asyncio.fixture -async def current_user_service()->CurrentUserService: - current_user_service=CurrentUserService() - return current_user_service +async def current_user_service(monkeypatch): + + test_redis = RedisClient() + monkeypatch.setattr("src.service.auth.auth.redis_client", test_redis) + + service = CurrentUserService() + + yield service + await test_redis.aclose() @pytest_asyncio.fixture async def requests(mocker): fake_request = mocker.MagicMock(spec=Request) fake_request.headers = {"user-agent": "pytest-agent", "x-forwarded-for":"127.0.0.1"} return fake_request - -@pytest_asyncio.fixture -async def crud_service()->CrudService: - crud_service=CrudService() - return crud_service \ No newline at end of file diff --git a/tests/integrated/test_auth.py b/tests/integrated/test_auth.py index 7424d1e..16ac2a3 100644 --- a/tests/integrated/test_auth.py +++ b/tests/integrated/test_auth.py @@ -118,7 +118,8 @@ class TestAuth: with allure.step("create fake refresh token"): - token=await jwt_service.create_refresh_token({"sub":str(uuid4())}) + refresh_token=await jwt_service.create_refresh_token({"sub":str(uuid4())}) + access_token=await jwt_service.create_access_token({"sub":str(uuid4)}) with allure.step("patching db call functions"): @@ -126,7 +127,7 @@ class TestAuth: with allure.step("test logout with fake data"): - status=await current_user_service.logout(token[0]) + status=await current_user_service.logout(refresh_token[0], access_token) assert status is True @pytest.mark.parametrize("jti,db_result, expected_exception, expected_status",[ @@ -148,11 +149,11 @@ class TestAuth: with allure.step("create fake refresh token"): - token=await fake_create_refresh_token({"sub":str(uuid4()), "jti":jti, "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}) - + refresh_token=await fake_create_refresh_token({"sub":str(uuid4()), "jti":jti, "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}) + access_token=await fake_create_refresh_token({"sub":str(uuid4()), "jti":jti, "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(minutes=30)}) with allure.step("test logout with fake data"), pytest.raises(expected_exception) as exc_info: - await current_user_service.logout(token) + await current_user_service.logout(refresh_token, access_token) if expected_exception is HTTPException: assert exc_info.value.status_code==expected_status diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 3ee8c0e..e69de29 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,9 +0,0 @@ -import pytest_asyncio - -from src.service.users_crud.users_crud import CrudService - - -@pytest_asyncio.fixture -async def crud_service()->CrudService: - crud_service=CrudService() - return crud_service \ No newline at end of file